1079. Letter Tile Possibilities
Problem Description
You are given n
tiles, where each tile has one letter printed on it, represented by the string tiles[i]
.
Your task is to find the total number of possible non-empty sequences of letters that can be formed using these tiles. Each tile can be used at most once in any sequence.
For example, if you have tiles "AAB":
- Length 1 sequences: "A", "B" (2 possibilities)
- Length 2 sequences: "AA", "AB", "BA" (3 possibilities)
- Length 3 sequences: "AAB", "ABA", "BAA" (3 possibilities)
- Total: 2 + 3 + 3 = 8 possible sequences
The solution uses a depth-first search approach with backtracking. It maintains a counter of available tiles and explores all possible sequences by:
- Trying each available tile as the next character in the sequence
- Recursively building longer sequences with the remaining tiles
- Backtracking to try different combinations
The key insight is that for each recursive call, we:
- Count the current sequence (add 1 to answer)
- Try extending the sequence with each available tile type
- Decrease the count of that tile, recurse, then restore it (backtracking)
- Sum up all possibilities from different branches
The Counter
keeps track of how many of each letter tile we have available at any point during the recursion.
Flowchart Walkthrough
First, let's pin down the algorithm using the Flowchart. Here's a step-by-step walkthrough:
Is it a graph?
- No: The problem involves arranging letter tiles into sequences, not nodes and edges as in graph problems.
Need to solve for kth smallest/largest?
- No: We need to count all possible sequences, not find a specific kth element.
Involves Linked Lists?
- No: The problem works with a string of tiles, not linked list data structures.
Does the problem have small constraints?
- Yes: The problem involves generating sequences from a limited set of tiles. The string
tiles
has length at most 7 (as per typical constraints), and we need to explore all possible arrangements. This is a combinatorial problem with factorial-like growth, which is only feasible with small inputs.
Brute force / Backtracking?
- Yes: Since we need to explore all possible sequences of different lengths using the available tiles, backtracking is the perfect approach. We systematically try each available tile, place it in our sequence, recursively build longer sequences with remaining tiles, then backtrack to try other combinations.
Conclusion: The flowchart correctly leads us to use a Backtracking approach. This aligns perfectly with the solution, which uses DFS with backtracking to enumerate all possible non-empty sequences by:
- Trying each available tile type
- Reducing its count and exploring further possibilities
- Restoring the count (backtracking) to try other branches
- Counting all valid sequences encountered during the exploration
Intuition
When faced with finding all possible sequences from a set of tiles, we need to think about how sequences are built step by step. Each sequence starts empty, then we add one tile at a time until we've used the tiles we want.
The key insight is that this is fundamentally a choice problem at each step: which tile should we use next? Since we want ALL possible sequences, we need to explore every valid choice systematically.
Consider tiles "AAB". To build sequences:
- We can start with 'A' (2 available) or 'B' (1 available)
- If we choose 'A', we now have "A" as a valid sequence (count it!)
- From here, we can extend with another 'A' (1 remaining) or 'B' (1 remaining)
- Each extension gives us a new valid sequence to count
This naturally suggests a recursive approach where at each level:
- We count the current sequence we've built
- We try extending it with each type of available tile
- We explore all possibilities from that extended state
The challenge is handling duplicate tiles efficiently. Instead of tracking individual tiles (which would create duplicate work for identical tiles), we track the count of each unique letter. This way, two 'A' tiles are interchangeable, avoiding redundant computation.
The backtracking pattern emerges because after exploring all sequences that start with a particular choice, we need to "undo" that choice (restore the tile) to explore other branches. This ensures we systematically cover all possibilities without missing any or counting duplicates.
The recursive structure naturally handles sequences of all lengths - from single tiles up to using all available tiles - because each recursive call represents adding one more tile to our current sequence.
Learn more about Backtracking patterns.
Solution Approach
The implementation uses a depth-first search (DFS) with backtracking to systematically generate and count all possible sequences.
Data Structure Choice:
We use a Counter
to track the frequency of each letter in the tiles. This is crucial because it:
- Treats identical letters as interchangeable (avoiding duplicate sequences)
- Allows efficient tracking of available tiles during recursion
- Enables easy increment/decrement operations for backtracking
Core Algorithm:
The dfs
function takes the current state (counter of available tiles) and returns the total count of sequences that can be formed from those tiles.
Step-by-step Implementation:
-
Initialize the counter:
cnt = Counter(tiles)
creates a frequency map of all available tiles. -
Recursive DFS function:
def dfs(cnt: Counter) -> int: ans = 0
This accumulator will store the total count of valid sequences.
-
Iterate through each unique tile type:
for i, x in cnt.items(): if x > 0:
For each letter that still has tiles available, we consider using it.
-
Count and explore:
ans += 1 # Count the sequence ending with this tile cnt[i] -= 1 # Use one tile of this type ans += dfs(cnt) # Recursively count longer sequences cnt[i] += 1 # Backtrack: restore the tile
The magic happens here:
ans += 1
: Every time we pick a tile, we form a valid sequence (the current path from root to this node)cnt[i] -= 1
: Temporarily remove the tile from available poolans += dfs(cnt)
: Recursively explore all sequences that can be built after using this tilecnt[i] += 1
: Restore the tile for exploring other branches (backtracking)
Why this works:
- Each recursive call represents a state where we've built a partial sequence
- The
ans += 1
counts the current sequence - The recursive call
dfs(cnt)
explores all possible extensions - Backtracking ensures we explore all branches of the decision tree
- Using a counter prevents counting duplicate sequences (e.g., using the first 'A' vs the second 'A' in "AAB" produces the same sequence)
Time Complexity: O(N! × N) in the worst case where all tiles are unique, as we explore all permutations and each recursive call iterates through the counter.
Space Complexity: O(N) for the recursion stack and counter storage.
Ready to land your dream job?
Unlock your dream job with a 3-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's trace through the algorithm with tiles = "AAB" to see how it counts all 8 possible sequences.
Initial State: Counter = {'A': 2, 'B': 1}
We start our DFS from the root. The function will explore each available tile type:
Level 1 - Choose first tile:
-
Choose 'A' (2 available):
- Count this sequence: "A" ✓ (total = 1)
- Update counter: {'A': 1, 'B': 1}
- Recurse to Level 2...
Level 2 - After choosing 'A':
- Choose another 'A' (1 available):
- Count sequence: "AA" ✓ (total = 2)
- Update counter: {'A': 0, 'B': 1}
- Recurse to Level 3...
- Choose 'B' (1 available):
- Count sequence: "AAB" ✓ (total = 3)
- Update counter: {'A': 0, 'B': 0}
- No more tiles, return 0
- Backtrack: restore 'B'
- Return to Level 2
- Backtrack: restore second 'A'
- Choose 'B' (1 available):
- Count sequence: "AB" ✓ (total = 4)
- Update counter: {'A': 1, 'B': 0}
- Recurse to Level 3...
- Choose 'A' (1 available):
- Count sequence: "ABA" ✓ (total = 5)
- Update counter: {'A': 0, 'B': 0}
- No more tiles, return 0
- Backtrack: restore 'A'
- Return to Level 2
- Backtrack: restore 'B'
- Return to Level 1
- Backtrack: restore first 'A'
-
Choose 'B' (1 available):
- Count this sequence: "B" ✓ (total = 6)
- Update counter: {'A': 2, 'B': 0}
- Recurse to Level 2...
Level 2 - After choosing 'B':
- Choose 'A' (2 available):
- Count sequence: "BA" ✓ (total = 7)
- Update counter: {'A': 1, 'B': 0}
- Recurse to Level 3...
- Choose another 'A' (1 available):
- Count sequence: "BAA" ✓ (total = 8)
- Update counter: {'A': 0, 'B': 0}
- No more tiles, return 0
- Backtrack: restore 'A'
- Return to Level 2
- Backtrack: restore 'A'
- Return to Level 1
- Backtrack: restore 'B'
Final Result: 8 sequences found: "A", "AA", "AAB", "AB", "ABA", "B", "BA", "BAA"
The key insight is that each time we pick a tile, we:
- Count the current sequence (the path from root to current node)
- Try extending with each remaining tile type
- Backtrack to explore alternative branches
This ensures we count every possible non-empty sequence exactly once.
Solution Implementation
1from collections import Counter
2from typing import Dict
3
4class Solution:
5 def numTilePossibilities(self, tiles: str) -> int:
6 def dfs(char_count: Dict[str, int]) -> int:
7 # Initialize result counter for this recursion level
8 result = 0
9
10 # Try using each available character
11 for char, count in char_count.items():
12 if count > 0:
13 # Count the current sequence (using this character)
14 result += 1
15
16 # Use one instance of this character
17 char_count[char] -= 1
18
19 # Recursively count all possible sequences that can be formed
20 # with the remaining characters
21 result += dfs(char_count)
22
23 # Backtrack: restore the character count
24 char_count[char] += 1
25
26 return result
27
28 # Create a frequency counter for all characters in tiles
29 char_count = Counter(tiles)
30
31 # Start DFS to count all possible non-empty sequences
32 return dfs(char_count)
33
1class Solution {
2 /**
3 * Calculate the number of possible tile sequences that can be formed
4 * @param tiles String containing tile letters
5 * @return Number of possible non-empty sequences
6 */
7 public int numTilePossibilities(String tiles) {
8 // Count frequency of each letter (A-Z)
9 int[] letterCount = new int[26];
10 for (char c : tiles.toCharArray()) {
11 letterCount[c - 'A']++;
12 }
13
14 // Use DFS to explore all possible sequences
15 return dfs(letterCount);
16 }
17
18 /**
19 * Depth-first search to count all possible sequences
20 * @param letterCount Array containing count of each letter
21 * @return Number of sequences that can be formed with remaining letters
22 */
23 private int dfs(int[] letterCount) {
24 int result = 0;
25
26 // Try using each available letter
27 for (int i = 0; i < letterCount.length; i++) {
28 if (letterCount[i] > 0) {
29 // Count current letter as a valid sequence
30 result++;
31
32 // Use this letter and explore further possibilities
33 letterCount[i]--;
34 result += dfs(letterCount);
35
36 // Backtrack: restore the letter count
37 letterCount[i]++;
38 }
39 }
40
41 return result;
42 }
43}
44
1class Solution {
2public:
3 int numTilePossibilities(string tiles) {
4 // Count frequency of each letter (A-Z)
5 int letterCount[26]{};
6 for (char c : tiles) {
7 ++letterCount[c - 'A'];
8 }
9
10 // Define recursive DFS function to count all possible sequences
11 function<int(int*)> dfs = [&](int* count) -> int {
12 int result = 0;
13
14 // Try using each available letter
15 for (int i = 0; i < 26; ++i) {
16 if (count[i] > 0) {
17 // Count this single letter as one possibility
18 ++result;
19
20 // Use this letter and recursively count longer sequences
21 --count[i];
22 result += dfs(count);
23
24 // Backtrack: restore the letter count
25 ++count[i];
26 }
27 }
28
29 return result;
30 };
31
32 // Start DFS with initial letter counts
33 return dfs(letterCount);
34 }
35};
36
1/**
2 * Counts the number of possible non-empty sequences that can be formed from the given tiles
3 * @param tiles - A string representing the available tiles (uppercase letters A-Z)
4 * @returns The total number of possible tile arrangements
5 */
6function numTilePossibilities(tiles: string): number {
7 // Initialize frequency array for 26 uppercase letters (A-Z)
8 const letterFrequency: number[] = new Array(26).fill(0);
9
10 // Count the frequency of each letter in the tiles string
11 for (const letter of tiles) {
12 const index: number = letter.charCodeAt(0) - 'A'.charCodeAt(0);
13 letterFrequency[index]++;
14 }
15
16 /**
17 * Depth-first search to count all possible arrangements
18 * Uses backtracking to explore all valid combinations
19 * @param frequency - Current frequency array of available letters
20 * @returns Number of possible arrangements from current state
21 */
22 const countArrangements = (frequency: number[]): number => {
23 let totalArrangements: number = 0;
24
25 // Try using each available letter
26 for (let letterIndex = 0; letterIndex < 26; letterIndex++) {
27 if (frequency[letterIndex] > 0) {
28 // Count this single letter as one arrangement
29 totalArrangements++;
30
31 // Use this letter and explore further arrangements
32 frequency[letterIndex]--;
33 totalArrangements += countArrangements(frequency);
34
35 // Backtrack: restore the letter for other branches
36 frequency[letterIndex]++;
37 }
38 }
39
40 return totalArrangements;
41 };
42
43 // Start the DFS with the initial frequency array
44 return countArrangements(letterFrequency);
45}
46
Time and Space Complexity
Time Complexity: O(n! * n)
where n
is the length of the tiles string.
The algorithm explores all possible permutations of the tiles. In the worst case where all tiles are unique:
- At the first level of recursion, we have
n
choices - At the second level, we have
n-1
choices - This continues until we have 1 choice
- Total number of recursive calls is approximately
n!
- Each recursive call iterates through the counter which can have up to
n
unique characters - Therefore, the time complexity is
O(n! * n)
When there are duplicate tiles, the actual number of permutations is less than n!
due to pruning, but the worst-case complexity remains O(n! * n)
.
Space Complexity: O(n)
where n
is the length of the tiles string.
The space complexity consists of:
- The Counter object which stores at most
n
unique characters:O(n)
- The recursion stack depth which can go up to
n
levels deep (when we use all tiles):O(n)
- Each recursive call maintains its local variables but doesn't create new data structures
- Overall space complexity is
O(n)
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Attempting to Generate Actual Sequences Instead of Counting
A common mistake is trying to build and store all possible sequences in a list or set, then returning the count. This approach is inefficient and can lead to memory issues.
Incorrect Approach:
def numTilePossibilities(self, tiles: str) -> int:
sequences = set()
def dfs(current_seq, remaining):
if current_seq:
sequences.add(current_seq)
for i in range(len(remaining)):
dfs(current_seq + remaining[i], remaining[:i] + remaining[i+1:])
dfs("", tiles)
return len(sequences)
Problems with this approach:
- Memory inefficient: stores all sequences
- Time inefficient: string concatenation and slicing operations
- May generate duplicates that need to be filtered
Correct Solution: Count sequences without generating them, as shown in the original solution.
2. Forgetting to Count the Current Sequence
A critical pitfall is forgetting the ans += 1
line that counts the current valid sequence before recursing deeper.
Incorrect Implementation:
def dfs(cnt: Counter) -> int:
ans = 0
for i, x in cnt.items():
if x > 0:
# Missing: ans += 1 (forgot to count current sequence!)
cnt[i] -= 1
ans += dfs(cnt) # Only counting deeper sequences
cnt[i] += 1
return ans
Why this fails: This would only count sequences of maximum length, missing all intermediate-length sequences. For "AAB", it would miss counting "A", "B", "AA", "AB", "BA".
Correct Solution: Always add 1 before recursing to count the sequence formed by taking the current tile.
3. Not Using a Frequency Counter (Treating Identical Letters as Distinct)
Using indices or treating each tile as unique leads to counting duplicate sequences.
Incorrect Approach:
def numTilePossibilities(self, tiles: str) -> int:
used = [False] * len(tiles)
def dfs(depth):
if depth == 0:
return 0
ans = 0
for i in range(len(tiles)):
if not used[i]:
ans += 1
used[i] = True
ans += dfs(depth + 1)
used[i] = False
return ans
return dfs(0)
Problem: This treats "AAB" as having three distinct tiles (A₁, A₂, B), so it would count "A₁B" and "A₂B" as different sequences, when they're actually the same "AB".
Correct Solution: Use a Counter to group identical letters together, ensuring each unique sequence is counted exactly once.
4. Incorrect Backtracking (Not Restoring State)
Forgetting to restore the counter after recursion breaks the backtracking mechanism.
Incorrect Implementation:
def dfs(cnt: Counter) -> int:
ans = 0
for i, x in cnt.items():
if x > 0:
ans += 1
cnt[i] -= 1
ans += dfs(cnt)
# Missing: cnt[i] += 1 (forgot to restore!)
return ans
Why this fails: Without restoring the count, subsequent iterations in the loop won't have access to tiles that were used in earlier iterations, leading to incorrect counts.
Correct Solution: Always restore the state (cnt[i] += 1
) after the recursive call to ensure proper exploration of all branches.
What does the following code do?
1def f(arr1, arr2):
2 i, j = 0, 0
3 new_arr = []
4 while i < len(arr1) and j < len(arr2):
5 if arr1[i] < arr2[j]:
6 new_arr.append(arr1[i])
7 i += 1
8 else:
9 new_arr.append(arr2[j])
10 j += 1
11 new_arr.extend(arr1[i:])
12 new_arr.extend(arr2[j:])
13 return new_arr
14
1public static List<Integer> f(int[] arr1, int[] arr2) {
2 int i = 0, j = 0;
3 List<Integer> newArr = new ArrayList<>();
4
5 while (i < arr1.length && j < arr2.length) {
6 if (arr1[i] < arr2[j]) {
7 newArr.add(arr1[i]);
8 i++;
9 } else {
10 newArr.add(arr2[j]);
11 j++;
12 }
13 }
14
15 while (i < arr1.length) {
16 newArr.add(arr1[i]);
17 i++;
18 }
19
20 while (j < arr2.length) {
21 newArr.add(arr2[j]);
22 j++;
23 }
24
25 return newArr;
26}
27
1function f(arr1, arr2) {
2 let i = 0, j = 0;
3 let newArr = [];
4
5 while (i < arr1.length && j < arr2.length) {
6 if (arr1[i] < arr2[j]) {
7 newArr.push(arr1[i]);
8 i++;
9 } else {
10 newArr.push(arr2[j]);
11 j++;
12 }
13 }
14
15 while (i < arr1.length) {
16 newArr.push(arr1[i]);
17 i++;
18 }
19
20 while (j < arr2.length) {
21 newArr.push(arr2[j]);
22 j++;
23 }
24
25 return newArr;
26}
27
Recommended Readings
Backtracking Template Prereq DFS with States problems dfs_with_states Combinatorial search problems Combinatorial search problems involve finding groupings and assignments of objects that satisfy certain conditions Finding all permutations combinations subsets and solving Sudoku are classic combinatorial problems The time complexity of combinatorial problems often grows rapidly with the size of
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask
Want a Structured Path to Master System Design Too? Don’t Miss This!