We create a dp array of size 8 (2^3) initialized to zero, then set dp[0] = 1 to represent one way to assign no caps.
💡 Starting with dp[0] = 1 means there is exactly one way to assign caps to no one - the empty assignment.
Line:dp = [0] * (1 << n)
dp[0] = 1
💡 This initialization sets the base case for building up assignments incrementally.
setup
Map caps to persons
We build a list cap_to_persons where each cap index stores which persons like that cap.
💡 This mapping lets us quickly find which persons can be assigned a given cap during DP updates.
Line:cap_to_persons = [[] for _ in range(101)]
for i, caps in enumerate(person_caps):
for c in caps:
cap_to_persons[c].append(i)
💡 Preprocessing caps to persons enables efficient iteration over relevant persons per cap.
fill_cells
Process cap 1: assign to persons 1 and 2
For cap 1, persons 1 and 2 like it. We iterate dp masks backward and update dp for assigning cap 1 to these persons if unassigned.
💡 Backward iteration prevents double counting assignments in the same cap iteration.
Line:for mask in range((1 << n) - 1, -1, -1):
if dp[mask] == 0:
continue
for person in cap_to_persons[1]:
if (mask & (1 << person)) == 0:
dp[mask | (1 << person)] = (dp[mask | (1 << person)] + dp[mask]) % MOD
💡 We add ways to assign cap 1 to unassigned persons, increasing dp counts for new masks.
fill_cells
Process cap 2: assign to person 2
For cap 2, only person 2 likes it. We update dp by assigning cap 2 to person 2 where unassigned.
💡 We continue building dp by considering each cap and possible assignments.
Line:for mask in range((1 << n) - 1, -1, -1):
if dp[mask] == 0:
continue
for person in cap_to_persons[2]:
if (mask & (1 << person)) == 0:
dp[mask | (1 << person)] = (dp[mask | (1 << person)] + dp[mask]) % MOD
💡 dp updates accumulate ways to assign caps uniquely to persons.
fill_cells
Process cap 5: assign to persons 0 and 1
For cap 5, persons 0 and 1 like it. We update dp by assigning cap 5 to these persons if unassigned.
💡 This step shows how multiple persons liking the same cap expand assignment possibilities.
Line:for mask in range((1 << n) - 1, -1, -1):
if dp[mask] == 0:
continue
for person in cap_to_persons[5]:
if (mask & (1 << person)) == 0:
dp[mask | (1 << person)] = (dp[mask | (1 << person)] + dp[mask]) % MOD
💡 dp grows as we assign cap 5 to different unassigned persons, increasing ways count.
fill_cells
Process cap 100: assign to persons 0 and 1
For cap 100, persons 0 and 1 like it. We update dp by assigning cap 100 to these persons if unassigned.
💡 This step shows how the last cap extends assignment counts further.
Line:for mask in range((1 << n) - 1, -1, -1):
if dp[mask] == 0:
continue
for person in cap_to_persons[100]:
if (mask & (1 << person)) == 0:
dp[mask | (1 << person)] = (dp[mask | (1 << person)] + dp[mask]) % MOD
💡 dp accumulates all ways to assign caps uniquely to all persons.
prune
Skip caps with no interested persons
For caps 3,4,6,...,99 that no person likes, we skip processing to save time.
💡 Skipping irrelevant caps keeps the algorithm efficient.
Line:for cap in range(1, 101):
if not cap_to_persons[cap]:
continue
💡 Not all caps need processing; only those liked by at least one person matter.
fill_cells
Final dp array after processing all caps
After processing all caps, dp[7] holds the total number of ways to assign caps uniquely to all 3 persons.
💡 The dp array now contains counts for every subset of assigned persons.
Line:return dp[(1 << n) - 1]
💡 The final dp value at full mask is the answer to the problem.
reconstruct
Read final answer
We return dp[7], which equals 4, representing the total number of unique cap assignments.
💡 Reading the final dp cell gives the solution without enumerating assignments.
Line:return dp[(1 << n) - 1]
💡 The bitmask DP efficiently counts all valid assignments.
traverse
Summary of dp array meaning
Each dp index corresponds to a subset of persons assigned caps; the value is the count of ways to assign caps to that subset.
💡 Understanding dp indices as subsets clarifies how the algorithm builds solutions incrementally.
💡 Bitmask DP compactly represents all assignment states and transitions.
MOD = 10**9 + 7
def count_ways_space_optimized(person_caps):
n = len(person_caps) # STEP 1
dp = [0] * (1 << n) # STEP 1
dp[0] = 1 # STEP 1
cap_to_persons = [[] for _ in range(101)] # STEP 2
for i, caps in enumerate(person_caps): # STEP 2
for c in caps: # STEP 2
cap_to_persons[c].append(i) # STEP 2
for cap in range(1, 101): # STEP 3-9
if not cap_to_persons[cap]: # STEP 7
continue
for mask in range((1 << n) - 1, -1, -1): # STEP 3-9
if dp[mask] == 0: # STEP 3-9
continue
for person in cap_to_persons[cap]: # STEP 3-9
if (mask & (1 << person)) == 0: # STEP 3-9
dp[mask | (1 << person)] = (dp[mask | (1 << person)] + dp[mask]) % MOD # STEP 3-9
return dp[(1 << n) - 1] # STEP 8-9
if __name__ == '__main__':
person_caps = [[5, 100], [1, 5, 100], [1, 2]]
print(count_ways_space_optimized(person_caps))
📊
Count Ways to Assign Unique Cap - Watch the Algorithm Execute, Step by Step
Watching this step-by-step shows how bitmask DP efficiently counts assignments without enumerating all permutations explicitly.
Step 1/10
·Active fill★Answer cell
Item 0 - wt:0 val:0
i\w
0
1
2
3
4
5
6
7
i=0
1
0
0
0
0
0
0
0
dp[0] = 1 (base case)
Item 0 - wt:0 val:0
i\w
0
1
2
3
4
5
6
7
i=0
1
0
0
0
0
0
0
0
cap_to_persons mapping ready
Item 0 - wt:1 val:0
i\w
0
1
2
3
4
5
6
7
i=0
1
0
1
0
1
0
0
0
dp[0] used to assign cap 1
Item 0 - wt:2 val:0
i\w
0
1
2
3
4
5
6
7
i=0
1
0
1
0
2
0
0
0
dp[0] used to assign cap 2
Item 0 - wt:5 val:0
i\w
0
1
2
3
4
5
6
7
i=0
1
1
2
1
2
0
0
0
dp[0] used to assign cap 5
Item 0 - wt:100 val:0
i\w
0
1
2
3
4
5
6
7
i=0
1
2
2
3
2
1
1
4
dp[0] used to assign cap 100
Item 0 - wt:0 val:0
i\w
0
1
2
3
4
5
6
7
i=0
1
2
2
3
2
1
1
4
Skipping irrelevant caps
Item 0 - wt:0 val:0
i\w
0
1
2
3
4
5
6
7
i=0
1
2
2
3
2
1
1
4
Answer: dp[7] = 4
Item 0 - wt:0 val:4
i\w
0
1
2
3
4
5
6
7
i=0
1
2
2
3
2
1
1
4
Final answer cell
Item 0 - wt:0 val:0
i\w
0
1
2
3
4
5
6
7
i=0
1
2
2
3
2
1
1
4
dp array overview
Key Takeaways
✓ Bitmask DP efficiently counts unique assignments by representing subsets of assigned persons as bitmasks.
This insight is hard to see from code alone because the bitmask representation and backward iteration are subtle but crucial.
✓ Backward iteration over dp masks prevents double counting when assigning the same cap to multiple persons.
Seeing the order of iteration visually clarifies why forward iteration would produce incorrect counts.
✓ Preprocessing caps to persons allows skipping irrelevant caps, improving efficiency without affecting correctness.
The trace shows how skipping empty caps focuses computation only where needed.
Practice
(1/5)
1. Consider the following Python code implementing the optimized LIS algorithm. What is the value of the variable tails after processing the input [3, 1, 2]?
tails always sorted and minimal last elements [OK]
Hint: tails stores smallest possible ends of subsequences [OK]
Common Mistakes:
Appending instead of replacing in tails
Misunderstanding bisect_left index
Confusing tails with actual subsequence
2. You are given an integer array and need to find how many distinct longest strictly increasing subsequences it contains. Which approach guarantees an optimal solution that efficiently counts all such subsequences without enumerating them explicitly?
easy
A. A greedy algorithm that picks the first increasing subsequence found and counts its length
B. A pure recursive backtracking approach that tries all subsequences to count the longest ones
C. Sorting the array and counting increasing subsequences using combinatorial formulas
D. Dynamic programming that tracks length and count of LIS ending at each index with O(n²) time complexity
Solution
Step 1: Understand problem requirements
The problem requires counting all longest increasing subsequences, not just finding one or their length.
Step 2: Evaluate approaches
Greedy fails because it finds only one LIS, recursion is exponential and inefficient, sorting loses original order, but DP with length and count arrays correctly tracks LIS counts in O(n²).
Final Answer:
Option D -> Option D
Quick Check:
DP with length and count arrays is standard for counting LIS [OK]
Hint: DP with length and count arrays counts LIS correctly [OK]
Common Mistakes:
Assuming greedy finds all LIS
Thinking sorting helps count LIS
Using pure recursion without pruning
3. What is the time complexity of the optimized LIS algorithm using binary search on an input array of length n?
medium
A. O(n log n)
B. O(n)
C. O(n²)
D. O(log n)
Solution
Step 1: Identify outer loop complexity
The algorithm iterates over all n elements once, so at least O(n).
Step 2: Identify inner operation complexity
For each element, binary search on tails array takes O(log n) time.
Final Answer:
Option A -> Option A
Quick Check:
n iterations x log n binary search -> O(n log n) [OK]
Hint: Binary search inside loop gives n log n [OK]
Common Mistakes:
Assuming O(n²) because of nested loops
Ignoring binary search complexity
Confusing with brute force exponential
4. The following code attempts to solve the Travelling Salesman Problem using bottom-up bitmask DP. Identify the bug that causes incorrect results on some inputs.
def tsp_bottom_up_buggy(dist):
n = len(dist)
ALL_VISITED = (1 << n) - 1
dp = [[float('inf')] * n for _ in range(1 << n)]
# BUG: dp[1][0] not initialized
for mask in range(1 << n):
for i in range(n):
if mask & (1 << i):
for j in range(n):
if mask & (1 << j) and i != j:
prev_mask = mask ^ (1 << i)
dp[mask][i] = min(dp[mask][i], dp[prev_mask][j] + dist[j][i])
ans = float('inf')
for i in range(1, n):
ans = min(ans, dp[ALL_VISITED][i] + dist[i][0])
return ans
medium
A. Missing base case initialization dp[1][0] = 0
B. Incorrect calculation of prev_mask = mask ^ (1 << i)
C. Line initializing dp array with float('inf')
D. Final loop over i in range(1, n) missing city 0
Solution
Step 1: Check base case initialization
The DP table must have dp[1][0] = 0 to represent starting at city 0 with only city 0 visited.
Step 2: Consequence of missing base case
Without this, all dp states remain infinite, so no valid path cost is computed, causing incorrect results.
Final Answer:
Option A -> Option A
Quick Check:
Base case initialization is essential for DP correctness [OK]
Hint: Always initialize base case dp for starting city [OK]
Common Mistakes:
Forgetting dp[1][0] initialization leads to infinite costs
5. Suppose pairs can be reused multiple times in the chain (i.e., you can select the same pair multiple times if it fits). Which modification to the algorithm correctly finds the longest chain length under this new constraint?
hard
A. Use the same greedy approach sorting by end time, but allow pairs with start >= current_end
B. Use a bottom-up DP without sorting, checking all pairs for chain extension
C. Sort pairs by start time and greedily select pairs with earliest start repeatedly
D. Use dynamic programming with memoization to explore all sequences allowing reuse