Jump into concepts and practice - no test required
or
Recommended
Test this pattern10 questions across easy, medium, and hard to know if this pattern is strong
▶
Steps
setup
Initialize Trie and Insert Word Masks
Start by creating an empty trie root node. Prepare to insert bitmask representations of words with up to 7 unique letters.
💡 Building the trie is essential to efficiently represent all word letter subsets for quick lookup during puzzle queries.
Line:self.root = TrieNode()
💡 The trie root node is the starting point for all word insertions and puzzle traversals.
insert
Insert mask for word 'aaaa' into trie
Convert 'aaaa' to a bitmask representing letter 'a' only, then insert this mask into the trie by creating nodes for letter 'a'.
💡 Inserting word masks builds trie paths that represent unique letter sets of words, enabling fast subset checks later.
Line:for i in range(26):
bit = (mask >> i) & 1
if bit:
if i not in node.children:
node.children[i] = TrieNode()
node = node.children[i]
node.count += 1
💡 The trie now has a path for letter 'a' with count incremented at the leaf node.
insert
Insert mask for word 'asas' into trie
Convert 'asas' to bitmask for letters 'a' and 's', then insert nodes for 'a' and 's' in the trie, incrementing count at the leaf.
💡 Words with multiple letters create deeper trie branches representing their letter subsets.
Line:for i in range(26):
bit = (mask >> i) & 1
if bit:
if i not in node.children:
node.children[i] = TrieNode()
node = node.children[i]
node.count += 1
💡 The trie now has a branch for 'a' leading to a child node for 's', representing 'as' subset.
insert
Insert mask for word 'able' into trie
Convert 'able' to bitmask for letters 'a','b','l','e', insert nodes for these letters sequentially in trie, increment count at leaf.
💡 Inserting words with multiple distinct letters creates longer trie paths representing their letter combinations.
Line:for i in range(26):
bit = (mask >> i) & 1
if bit:
if i not in node.children:
node.children[i] = TrieNode()
node = node.children[i]
node.count += 1
💡 The trie now contains a path for 'a'->'b'->'e'->'l' representing 'able' letters.
traverse
Start DFS for puzzle 'aboveyz'
Convert puzzle letters to bitmask and identify first letter bit. Begin recursive DFS from trie root to count valid words including first letter 'a'.
💡 DFS explores trie paths restricted to puzzle letters, ensuring only valid word subsets are counted.
💡 DFS starts with no letters matched yet, ready to explore valid branches.
traverse
DFS explores child node for letter 'a' (bit 0)
Since 'a' is in puzzle and is the first letter, recurse into child node for 'a' with has_first=True.
💡 Including the puzzle's first letter in the path is mandatory for valid words.
Line:for i, child in node.children.items():
if (puzzle_mask & (1 << i)) != 0:
res += self.dfs(child, puzzle_mask, first_bit, has_first or (i == first_bit))
💡 The path now includes the required first letter, enabling counting of valid words below.
compare
DFS counts words ending at node for 'a'
At node for 'a', count words ending here since has_first is true. Add count to result.
💡 Words ending at this node are valid if the first letter is included in the path.
Line:res = node.count if has_first else 0
💡 Words like 'aaaa' end here and are counted for this puzzle.
traverse
DFS explores child node for letter 's' (bit 18) from 'a'
Check if 's' is in puzzle letters; since it is not in 'aboveyz', skip this branch (prune).
💡 Pruning avoids exploring branches with letters not in the puzzle, improving efficiency.
Line:if (puzzle_mask & (1 << i)) != 0:
res += self.dfs(child, puzzle_mask, first_bit, has_first or (i == first_bit))
💡 Pruning prevents unnecessary recursion down invalid paths.
traverse
DFS explores child node for letter 'b' (bit 1) from 'a'
Letter 'b' is in puzzle letters, recurse into child node for 'b' with has_first true.
💡 Continuing recursion along puzzle letters explores deeper word subsets.
Line:res += self.dfs(child, puzzle_mask, first_bit, has_first or (i == first_bit))
💡 The path now includes letters 'a' and 'b', still valid for puzzle.
traverse
DFS explores child node for letter 'e' (bit 4) from 'b'
Letter 'e' is in puzzle letters, recurse into child node for 'e' with has_first true.
💡 Recursion continues down valid letter paths to find word endings.
Line:res += self.dfs(child, puzzle_mask, first_bit, has_first or (i == first_bit))
💡 The path now includes letters 'a','b','e', matching part of 'able'.
traverse
DFS explores child node for letter 'l' (bit 11) from 'e'
Letter 'l' is in puzzle letters, recurse into child node for 'l' with has_first true.
💡 Reaching leaf nodes with counts indicates valid words found.
Line:res += self.dfs(child, puzzle_mask, first_bit, has_first or (i == first_bit))
💡 The path now represents full letter set of 'able'.
compare
Count words ending at node for 'l'
Add count of words ending at this node to result since has_first is true.
💡 This count contributes to the total valid words for the puzzle.
Line:res = node.count if has_first else 0
💡 Words like 'able' are counted here as valid for the puzzle.
backtracking
Backtrack and finish DFS for puzzle 'aboveyz'
Return accumulated count of valid words for puzzle 'aboveyz' after exploring all valid trie branches.
💡 Backtracking aggregates counts from all valid paths to produce the final answer for the puzzle.
Line:return res
💡 The final count for 'aboveyz' is 1, matching the expected output.
traverse
Start DFS for puzzle 'abslute'
Convert puzzle letters to bitmask and identify first letter bit. Begin DFS from trie root to count valid words including first letter 'a'.
💡 Each puzzle triggers a fresh DFS traversal constrained by its letters.
💡 DFS will explore trie branches matching puzzle letters to find valid words.
traverse
DFS explores child node for letter 'a' (bit 0) for 'abslute'
Recurse into child node for 'a' since it is in puzzle letters and is the first letter.
💡 Including the puzzle's first letter is mandatory for valid words.
Line:for i, child in node.children.items():
if (puzzle_mask & (1 << i)) != 0:
res += self.dfs(child, puzzle_mask, first_bit, has_first or (i == first_bit))
💡 The path now includes the required first letter.
traverse
DFS explores child node for letter 'b' (bit 1) from 'a' for 'abslute'
Recurse into child node for 'b' since it is in puzzle letters.
💡 Recursion explores valid letter branches to find word endings.
Line:res += self.dfs(child, puzzle_mask, first_bit, has_first or (i == first_bit))
💡 The path now includes letters 'a' and 'b'.
traverse
DFS explores child node for letter 'e' (bit 4) from 'b' for 'abslute'
Recurse into child node for 'e' since it is in puzzle letters.
💡 Continuing recursion to find valid word endings.
Line:res += self.dfs(child, puzzle_mask, first_bit, has_first or (i == first_bit))
💡 The path now includes letters 'a','b','e'.
traverse
DFS explores child node for letter 'l' (bit 11) from 'e' for 'abslute'
Recurse into child node for 'l' since it is in puzzle letters.
💡 Reaching leaf nodes indicates valid words found.
Line:res += self.dfs(child, puzzle_mask, first_bit, has_first or (i == first_bit))
💡 The path now represents full letter set of 'able'.
compare
Count words ending at node for 'l' for 'abslute'
Add count of words ending at this node to result since has_first is true.
💡 This count contributes to the total valid words for the puzzle.
Line:res = node.count if has_first else 0
💡 Words like 'able' are counted here as valid for the puzzle.
reconstruct
Final aggregation and output
After processing all puzzles, collect and return the list of counts of valid words for each puzzle.
💡 The final output shows how many words match each puzzle's constraints.
Line:return res
💡 The algorithm efficiently counts valid words using trie and bitmask pruning.
from typing import List
class TrieNode:
def __init__(self):
self.count = 0
self.children = {}
class Solution:
def __init__(self):
self.root = TrieNode() # STEP 1
def insert(self, mask):
node = self.root
for i in range(26): # STEP 2-4
bit = (mask >> i) & 1
if bit:
if i not in node.children:
node.children[i] = TrieNode()
node = node.children[i]
node.count += 1
def dfs(self, node, puzzle_mask, first_bit, has_first):
res = node.count if has_first else 0 # STEP 7,12,19
for i, child in node.children.items():
if (puzzle_mask & (1 << i)) != 0: # STEP 6,9,15,16,17,18
res += self.dfs(child, puzzle_mask, first_bit, has_first or (i == first_bit))
return res # STEP 13,20
def findNumOfValidWords(self, words: List[str], puzzles: List[str]) -> List[int]:
for word in words: # STEP 2-4
mask = 0
for ch in set(word):
mask |= 1 << (ord(ch) - ord('a'))
if bin(mask).count('1') <= 7:
self.insert(mask)
res = []
for puzzle in puzzles: # STEP 5,14
first_bit = ord(puzzle[0]) - ord('a')
puzzle_mask = 0
for ch in puzzle:
puzzle_mask |= 1 << (ord(ch) - ord('a'))
res.append(self.dfs(self.root, puzzle_mask, first_bit, False))
return res
if __name__ == "__main__":
words = ["aaaa","asas","able","ability","actt","actor","access"]
puzzles = ["aboveyz","abrodyz","abslute","absoryz","actresz","gaswxyz"]
sol = Solution()
print(sol.findNumOfValidWords(words, puzzles)) # STEP 20
📊
Number of Valid Words for Each Puzzle - Watch the Algorithm Execute, Step by Step
Watching the trie construction and recursive traversal step-by-step reveals how bitmasking and pruning efficiently solve a complex subset problem.
✓ Trie structure efficiently encodes all word letter subsets for fast lookup.
This insight is hard to see from code alone because the trie is implicit and built incrementally.
✓ Recursive DFS prunes branches not in puzzle letters, drastically reducing search space.
Visualizing pruning clarifies why the algorithm is efficient despite exponential subsets.
✓ Including the puzzle's first letter in the path is mandatory and tracked via has_first flag.
This subtle condition is critical and often overlooked when reading code without visualization.
Practice
(1/5)
1. Consider the following Python code implementing combination sum with reuse allowed:
from typing import List
def combinationSum(candidates: List[int], target: int) -> List[List[int]]:
candidates.sort()
result = []
def backtrack(index, path, target):
if target == 0:
result.append(path[:])
return
if index == len(candidates) or target < 0:
return
max_use = target // candidates[index]
for count in range(max_use + 1):
backtrack(index + 1, path + [candidates[index]] * count, target - candidates[index] * count)
backtrack(0, [], target)
return result
print(combinationSum([2,3,6,7], 7))
What is the exact output of this code?
easy
A. [[7]]
B. [[2, 2, 3], [7]]
C. [[2, 2, 3]]
D. [[7], [2, 2, 3]]
Solution
Step 1: Trace backtrack calls for candidates=[2,3,6,7], target=7
Sorted candidates: [2,3,6,7]. The function tries counts of 2 from 0 to 3 (max_use=3), then recurses on next index. Valid combinations found are [7] and [2,2,3].
Step 2: Check order of results appended
Backtracking explores index 0 with counts 0..3, then index 1, so [7] is found after [2,2,3]. The result list appends [7] first, then [2,2,3]. But since the code appends when target==0, the order is [[7], [2, 2, 3]].
Final Answer:
Option D -> Option D
Quick Check:
Output matches expected combinations with correct order [OK]
Hint: Order depends on recursion path, not sorted output [OK]
Common Mistakes:
Assuming output is sorted by combination length or lex order
Missing one valid combination
2. You are given an unsorted array of positive integers. Your task is to find the largest subset such that for every pair (Si, Sj) in the subset, either Si divides Sj or Sj divides Si. Which algorithmic approach guarantees finding the optimal solution efficiently?
easy
A. Greedy approach by always picking the smallest element and adding divisible elements afterwards
B. Use a brute force backtracking to generate all subsets and check divisibility
C. Sort the array and use dynamic programming to build the longest chain where each element divides the next
D. Use a graph traversal to find the largest clique where edges represent divisibility
Solution
Step 1: Understand problem constraints
The problem requires a subset where every pair is divisible in one direction, which suggests a chain-like structure.
Step 2: Identify suitable algorithm
Sorting the array allows divisibility checks to be done efficiently in increasing order, enabling dynamic programming to build the longest divisible chain.
Final Answer:
Option C -> Option C
Quick Check:
Sorting + DP is classic for largest divisible subset [OK]
Greedy fails because local choices don't guarantee global maximum
Brute force is correct but inefficient, not practical
Graph clique approach is incorrect because divisibility is not symmetric
3. You need to generate all possible subsets of a given set of distinct integers. Which approach guarantees generating every subset exactly once with a time complexity proportional to the number of subsets times the size of each subset?
easy
A. Greedy algorithm that picks elements based on their value order
B. Enumerate all bitmasks from 0 to 2^n - 1, selecting elements where bits are set
C. Dynamic programming to count subsets without generating them
D. Sorting the array and using two pointers to find pairs
Solution
Step 1: Understand the problem
We want to generate all subsets of a set, which are 2^n in number.
Step 2: Identify the approach that enumerates all subsets
Bitmask enumeration from 0 to 2^n - 1 maps each bit to an element's inclusion, guaranteeing all subsets exactly once.
Final Answer:
Option B -> Option B
Quick Check:
Bitmask enumeration covers all subsets systematically [OK]
Hint: Bitmask from 0 to 2^n-1 enumerates all subsets [OK]
Common Mistakes:
Thinking greedy or sorting can generate all subsets efficiently
4. The following code attempts to count subsets with sum K using space-optimized DP. Identify the line that contains a subtle bug that can cause incorrect results.
def count_subsets_space_optimized(arr, K):
dp = [0] * (K + 1)
dp[0] = 1
for num in arr:
for j in range(num, K + 1):
dp[j] += dp[j - num]
return dp[K]
medium
A. for j in range(num, K + 1):
B. dp[0] = 1
C. dp = [0] * (K + 1)
D. dp[j] += dp[j - num]
Solution
Step 1: Analyze iteration order
The inner loop iterates forward from num to K, which causes dp[j] to use updated dp values from the same iteration, leading to overcounting.
Step 2: Correct iteration direction
To avoid overcounting, the inner loop must iterate backward from K down to num.
Final Answer:
Option A -> Option A
Quick Check:
Forward iteration in DP causes incorrect counts -> bug in loop range [OK]
Hint: Inner loop must iterate backward to avoid reuse of updated dp values [OK]
Common Mistakes:
Using forward iteration in space-optimized DP
Misunderstanding dp update dependencies
5. What is the time complexity of generating all subsets of an array of size n using the bit manipulation approach shown below?
medium
A. O(2^n * n) because for each of the 2^n masks, we check n bits to build the subset
B. O(2^n) because there are 2^n subsets and each subset is generated in constant time
C. O(n^2) because of nested loops over n elements
D. O(n * 2^n) because each of the 2^n subsets requires iterating over n elements
Solution
Step 1: Identify outer loop complexity
The outer loop runs 2^n times, once per subset mask.
Step 2: Identify inner loop complexity
For each mask, the inner loop iterates n times to check each bit.
Final Answer:
Option D -> Option D
Quick Check:
Total time is 2^n * n due to mask and bit checks [OK]
Hint: Each subset requires checking n bits -> O(n*2^n) [OK]
Common Mistakes:
Assuming subset generation is O(2^n) ignoring bit checks