Builds on Bitmask Subsets and 0/1 Knapsack, which you haven’t finished yet.
Bitmask DP (Shortest Path Visiting All Nodes, TSP)
Put nums2 in the order that makes its XOR sum with nums1 as small as possible, and return that smallest sum. nums1 keeps its order, and both arrays have the same length n.
The XOR sum of two arrays lined up position by position adds nums1[i] XOR nums2[i] over every position i. For instance, [1,2] against [3,1] gives (1 XOR 3) + (2 XOR 1) = 2 + 3 = 5.
nums2 to [3,2] pairs 1 with 3 (XOR 2) and 2 with 2 (XOR 0): 2. Keeping [2,3] gives 3 + 1 = 4.Bitmask DP
about 20 items ➔ bitmask of used items ➔ dp[mask]| 1 | def minimum_xor_sum(nums1: list[int], nums2: list[int]) -> int: |
| 2 | """Minimum XOR Sum of Two Arrays: bitmask DP over which numbers of nums2 are already paired. |
| 3 | Time: O(N * 2^N) | Space: O(2^N) |
| 4 | """ |
| 5 | n = len(nums2) |
| 6 | # dp: dp[mask] is the least XOR sum pairing the first popcount(mask) numbers of nums1 with the nums2 set in mask |
| 7 | dp = [float("inf")] * (1 << n) |
| 8 | dp[0] = 0 |
| 9 | |
| 10 | for mask in range(1 << n): |
| 11 | # 1. Find The Next nums1 Index From The Mask: |
| 12 | # The count of set bits is how many nums1 numbers are already paired, so the next one is nums1[i]. |
| 13 | i = bin(mask).count("1") |
| 14 | if i == n: |
| 15 | continue |
| 16 | for j in range(n): |
| 17 | if not mask & (1 << j): |
| 18 | # 2. Pair nums1[i] With A Free nums2[j]: |
| 19 | # mask | (1 << j) is always larger than mask, so this same upward loop reaches it later |
| 20 | # with dp[mask] already final; keep the smaller total found for that larger state. |
| 21 | nxt = mask | (1 << j) |
| 22 | dp[nxt] = min(dp[nxt], dp[mask] + (nums1[i] ^ nums2[j])) |
| 23 | |
| 24 | return dp[(1 << n) - 1] |
dp[mask] is read it is final: the least XOR sum pairing the first bin(mask).count("1") numbers of nums1 with the nums2 numbers whose bits are set in mask.When n is small, keep one best answer per set of used items and write the set as the bits of an integer: dp[mask] is the least XOR sum after pairing the first i numbers of nums1 with the nums2 numbers whose bits are set in mask, where i = bin(mask).count("1"). From each mask, pair nums1[i] with every free nums2[j] (not mask & (1 << j)) and keep the smaller total in dp[mask | (1 << j)]. This is correct because the rest of the pairing depends only on which nums2 numbers are used, not on the order they were used in, so the n! orders collapse into 2^n states; and mask | (1 << j) is always larger than mask, so an upward loop over mask finishes every state before it is read. The trap is tracking the nums1 index yourself: pairing any nums1[i] with the mask does not record which nums1 numbers are already used, so one can be paired twice. i must come from the mask.
See it run
Bitmask DP on nums1 = [1, 2], nums2 = [2, 3]
mask 00dp[mask] 0
Each column is a mask. The top label is its bits (bit 0 is the last digit), and the set row lists the nums2 numbers already paired.
maskthe mask being readnxtthe larger mask being writtenanswerthe final result
dp[00] = 0, because pairing no numbers costs nothing. Every other entry is inf.
Gotcha 1 of 5
- The trap
Gotcha 1: Looping over every
iinstead ofi = bin(mask).count("1")Mistakefor i in range(n):Fixi = bin(mask).count("1")What happens:
maskrecords whichnums2numbers are used. It does not record whichnums1numbers are used. The count of set bits is how manynums1numbers are paired, so the next one isnums1[i]. A loop over everyican pair the samenums1number again and again.Example: nums1 = [2, 4, 8], nums2 = [1, 1, 1]Mistake: returns 9Fix: returns 17
- There are up to 40 items, as in Closest Subsequence Sum (LC 1755): 2^40 masks are far too many for one table, which is the signal to split the items into two halves. See Meet in the Middle
- Every subset itself must be listed, as in Subsets (LC 78), not one best value per set of used items: no set's answer is built from a smaller set's answer. See Subsets
Deep Dive
Why the order of pairing does not matter
A bitmask is an integer that uses each bit as a yes or no flag. In minimum_xor_sum, bit j of mask is 1 when nums2[j] is already paired.
Pairing is done one nums1 number at a time, from nums1[0] to the end. After some pairs are made, the remaining cost depends on one fact only: which nums2 numbers are still free. It does not depend on which of them went to which nums1 number, because those pairs are already paid for in dp[mask].
So all orders that use the same set of nums2 numbers reach the same state. There are n! orders, but only 2^n sets. This is the saving. For n = 20, n! is 2,432,902,008,176,640,000 and 2^n is 1,048,576.
How the mask also tells you the nums1 index
The loop never stores which nums1 numbers are paired. It does not need to, because the numbers are paired in order. If bin(mask).count("1") is i, then nums1[0] to nums1[i - 1] are paired and the next one is nums1[i].
This is why the card's invariant reads: dp[mask] pairs the first bin(mask).count("1") numbers of nums1 with the nums2 numbers whose bits are set in mask. The state has one input only, mask, and the index comes from it.
Example: nums1 = [1, 2] and nums2 = [2, 3]. The costs are 1 ^ 2 = 3, 1 ^ 3 = 2, 2 ^ 2 = 0 and 2 ^ 3 = 1. dp[0b00] = 0. From mask = 0b00, i = 0, so dp[0b01] = 3 and dp[0b10] = 2. From mask = 0b01, i = 1, so pairing nums1[1] with nums2[1] gives dp[0b11] = 3 + 1 = 4. From mask = 0b10, pairing nums1[1] with nums2[0] gives 2 + 0 = 2. The final table is [0, 3, 2, 2], and the answer dp[0b11] is 2.
Why an upward loop over mask is a valid order
The transition writes dp[mask | (1 << j)] for a bit j that is not yet set. Setting a bit adds 1 << j to the integer, so the new mask is always larger than mask.
A loop that goes from 0 upward therefore reaches every smaller mask before a larger one. When the loop arrives at a mask, every transition that writes into it has already run, so dp[mask] is final. This is the same fill-order argument as in a table, with the integer value of the mask as the order.
A loop that goes downward reads each larger mask while it still holds infinity, and the answer stays at infinity.
The cost: states times work per state
There are 2^n states, one for each subset of nums2. Each state tries every free bit j, which is at most n transitions of constant work. The time is O(N * 2^N). The table dp holds 2^n numbers, so the space is O(2^N).
LeetCode limits n to 14 for Minimum XOR Sum of Two Arrays (LC 1879). That gives 14 * 214 = 229,376 transitions, while the brute force over all pairings needs 14!, which is 87,178,291,200.
This is the reason for the trigger "about 20 items or fewer". At n = 20 the table has 1,048,576 entries and about 20,971,520 transitions. At n = 40 the table would not fit in memory.
What changes when the cost depends on the last item
Some problems, such as visiting every place once, charge a cost between two consecutive items. Then the set of used items is not enough, because the next cost depends on which item came last. The state becomes dp[mask][last], and the time becomes O(N^2 * 2^N).
In Find the Shortest Superstring (LC 943), the cost of appending a string depends on the string before it, so the state keeps last. In Minimum XOR Sum of Two Arrays (LC 1879), the cost of a pair depends only on the two numbers in the pair, so mask alone is enough.
60-Second Pitch
"The brute force tries all n! pairings. The remaining cost depends only on which numbers of nums2 are still free, so I keep one best answer per set of used numbers. I store the set as a bitmask. dp[mask] is the least XOR sum for the first popcount(mask) numbers of nums1. The next number to pair is nums1[i], with i taken from the mask. Each transition sets one more bit, so the new mask is larger. An upward loop over mask makes every value final before it is read. There are 2^n states, each with up to n transitions. That gives O(N * 2^N) time and O(2^N) space, which is fine for n up to about 20."
Common Questions
- Why is bitmask DP only used when n is small?
The table has
2^nentries, and each entry does up tonwork. Atn = 20that is about 21 million transitions, which is fine. Atn = 40the table has more than a trillion entries and does not fit in memory. So the constraint onnis the main signal for this method.- Why can the nums1 index be taken from the mask?
The numbers of
nums1are paired in order, one at each step. Afterkpairs, exactlykbits are set, andnums1[0]tonums1[k - 1]are used. Sobin(mask).count("1")is the index of the next number. Without this ordering, the mask would not be enough.- How do you check and set a bit?
To test bit
j, writemask & (1 << j). It is non-zero when the bit is set. To set it, writemask | (1 << j). To clear it, writemask & ~(1 << j). In C++ and JavaScript, put parentheses aroundmask & (1 << j)before comparing it with 0.- What changes for a travelling-salesman style problem?
The cost between two items depends on the last item visited. So the state becomes
dp[mask][last], and each state tries every next item. The time is O(N^2 * 2^N) and the space is O(N * 2^N). Find the Shortest Superstring (LC 943) is an example of this form.- How would you recover the actual pairing, not only its cost?
Store, for each
nxt, thejthat gave the smaller value. Then start at the full mask and move backward: the storedjis thenums2index paired withnums1[popcount(mask) - 1], and the previous mask is the current mask with bitjcleared. Repeat until the mask is 0.
SWE Reasoning
Spot it
- The input has a small size, about 20 items or fewer. The constraint often says
n <= 14,n <= 16orn <= 20. - The question asks for the best way to assign, pair, group or visit every item exactly once.
- The answer after some choices depends on which items are used, not on the order in which they were used.
- The brute force tries all
n!orders, and many of them reach the same set of used items. - A greedy rule or a sort does not give the best answer, because each choice changes which choices remain.
The invariant, step by step
dphas2^nentries, all set to infinity, exceptdp[0] = 0: no pairs cost nothing.- The loop reaches
mask. It computesi = bin(mask).count("1"). Exactlyinumbers ofnums2are paired, and they were paired withnums1[0]tonums1[i - 1]. - If
i == n, every number is paired and nothing more is added. - Otherwise it tries each bit
jthat is not set inmask. This pairsnums1[i]with the freenums2[j]. - It sets
nxt = mask | (1 << j)and keeps the smaller ofdp[nxt]anddp[mask] + (nums1[i] ^ nums2[j]). nxtis larger thanmask, so the loop reaches it later. By then every smaller mask that can lead tonxthas written its value, sodp[nxt]is final when it is read.- The answer is
dp[(1 << n) - 1], where every bit is set.
Traps
- Looping over every
i. The mask records the usednums2numbers only. If the code tries everynums1[i]from each mask, onenums1number can be paired many times. Fornums1 = [2, 4, 8]andnums2 = [1, 1, 1], the result is 9 instead of 17. - Downward loop over
mask. The loop reads larger masks before they are written. The answer stays infinity. Fornums1 = [1, 2]andnums2 = [2, 3], it returnsinfinstead of 2. - Missing parentheses in C++. In C++ and JavaScript,
mask & 1 << j == 0is read asmask & (1 << j == 0), which is always 0. Write(mask & (1 << j)) == 0. - Wrong size. The table needs
1 << nentries. Usingnentries raises an index error, and using2 ** nwith a largencan use more memory than you have. Checknfirst. - Forgetting the last item. When the cost depends on the previous item,
dp[mask]loses that fact and the result is wrong. Addlastto the state.
Practice
Builds on Bitmask Subsets and 0/1 Knapsack, which you haven’t finished yet.
Bitmask DP (Shortest Path Visiting All Nodes, TSP)
Put nums2 in the order that makes its XOR sum with nums1 as small as possible, and return that smallest sum. nums1 keeps its order, and both arrays have the same length n.
The XOR sum of two arrays lined up position by position adds nums1[i] XOR nums2[i] over every position i. For instance, [1,2] against [3,1] gives (1 XOR 3) + (2 XOR 1) = 2 + 3 = 5.
nums2 to [3,2] pairs 1 with 3 (XOR 2) and 2 with 2 (XOR 0): 2. Keeping [2,3] gives 3 + 1 = 4.Bitmask DP
about 20 items ➔ bitmask of used items ➔ dp[mask]| 1 | def minimum_xor_sum(nums1: list[int], nums2: list[int]) -> int: |
| 2 | """Minimum XOR Sum of Two Arrays: bitmask DP over which numbers of nums2 are already paired. |
| 3 | Time: O(N * 2^N) | Space: O(2^N) |
| 4 | """ |
| 5 | n = len(nums2) |
| 6 | # dp: dp[mask] is the least XOR sum pairing the first popcount(mask) numbers of nums1 with the nums2 set in mask |
| 7 | dp = [float("inf")] * (1 << n) |
| 8 | dp[0] = 0 |
| 9 | |
| 10 | for mask in range(1 << n): |
| 11 | # 1. Find The Next nums1 Index From The Mask: |
| 12 | # The count of set bits is how many nums1 numbers are already paired, so the next one is nums1[i]. |
| 13 | i = bin(mask).count("1") |
| 14 | if i == n: |
| 15 | continue |
| 16 | for j in range(n): |
| 17 | if not mask & (1 << j): |
| 18 | # 2. Pair nums1[i] With A Free nums2[j]: |
| 19 | # mask | (1 << j) is always larger than mask, so this same upward loop reaches it later |
| 20 | # with dp[mask] already final; keep the smaller total found for that larger state. |
| 21 | nxt = mask | (1 << j) |
| 22 | dp[nxt] = min(dp[nxt], dp[mask] + (nums1[i] ^ nums2[j])) |
| 23 | |
| 24 | return dp[(1 << n) - 1] |
dp[mask] is read it is final: the least XOR sum pairing the first bin(mask).count("1") numbers of nums1 with the nums2 numbers whose bits are set in mask.When n is small, keep one best answer per set of used items and write the set as the bits of an integer: dp[mask] is the least XOR sum after pairing the first i numbers of nums1 with the nums2 numbers whose bits are set in mask, where i = bin(mask).count("1"). From each mask, pair nums1[i] with every free nums2[j] (not mask & (1 << j)) and keep the smaller total in dp[mask | (1 << j)]. This is correct because the rest of the pairing depends only on which nums2 numbers are used, not on the order they were used in, so the n! orders collapse into 2^n states; and mask | (1 << j) is always larger than mask, so an upward loop over mask finishes every state before it is read. The trap is tracking the nums1 index yourself: pairing any nums1[i] with the mask does not record which nums1 numbers are already used, so one can be paired twice. i must come from the mask.
See it run
Bitmask DP on nums1 = [1, 2], nums2 = [2, 3]
mask 00dp[mask] 0
Each column is a mask. The top label is its bits (bit 0 is the last digit), and the set row lists the nums2 numbers already paired.
maskthe mask being readnxtthe larger mask being writtenanswerthe final result
dp[00] = 0, because pairing no numbers costs nothing. Every other entry is inf.
Gotcha 1 of 5
- The trap
Gotcha 1: Looping over every
iinstead ofi = bin(mask).count("1")Mistakefor i in range(n):Fixi = bin(mask).count("1")What happens:
maskrecords whichnums2numbers are used. It does not record whichnums1numbers are used. The count of set bits is how manynums1numbers are paired, so the next one isnums1[i]. A loop over everyican pair the samenums1number again and again.Example: nums1 = [2, 4, 8], nums2 = [1, 1, 1]Mistake: returns 9Fix: returns 17
- There are up to 40 items, as in Closest Subsequence Sum (LC 1755): 2^40 masks are far too many for one table, which is the signal to split the items into two halves. See Meet in the Middle
- Every subset itself must be listed, as in Subsets (LC 78), not one best value per set of used items: no set's answer is built from a smaller set's answer. See Subsets
Deep Dive
Why the order of pairing does not matter
A bitmask is an integer that uses each bit as a yes or no flag. In minimum_xor_sum, bit j of mask is 1 when nums2[j] is already paired.
Pairing is done one nums1 number at a time, from nums1[0] to the end. After some pairs are made, the remaining cost depends on one fact only: which nums2 numbers are still free. It does not depend on which of them went to which nums1 number, because those pairs are already paid for in dp[mask].
So all orders that use the same set of nums2 numbers reach the same state. There are n! orders, but only 2^n sets. This is the saving. For n = 20, n! is 2,432,902,008,176,640,000 and 2^n is 1,048,576.
How the mask also tells you the nums1 index
The loop never stores which nums1 numbers are paired. It does not need to, because the numbers are paired in order. If bin(mask).count("1") is i, then nums1[0] to nums1[i - 1] are paired and the next one is nums1[i].
This is why the card's invariant reads: dp[mask] pairs the first bin(mask).count("1") numbers of nums1 with the nums2 numbers whose bits are set in mask. The state has one input only, mask, and the index comes from it.
Example: nums1 = [1, 2] and nums2 = [2, 3]. The costs are 1 ^ 2 = 3, 1 ^ 3 = 2, 2 ^ 2 = 0 and 2 ^ 3 = 1. dp[0b00] = 0. From mask = 0b00, i = 0, so dp[0b01] = 3 and dp[0b10] = 2. From mask = 0b01, i = 1, so pairing nums1[1] with nums2[1] gives dp[0b11] = 3 + 1 = 4. From mask = 0b10, pairing nums1[1] with nums2[0] gives 2 + 0 = 2. The final table is [0, 3, 2, 2], and the answer dp[0b11] is 2.
Why an upward loop over mask is a valid order
The transition writes dp[mask | (1 << j)] for a bit j that is not yet set. Setting a bit adds 1 << j to the integer, so the new mask is always larger than mask.
A loop that goes from 0 upward therefore reaches every smaller mask before a larger one. When the loop arrives at a mask, every transition that writes into it has already run, so dp[mask] is final. This is the same fill-order argument as in a table, with the integer value of the mask as the order.
A loop that goes downward reads each larger mask while it still holds infinity, and the answer stays at infinity.
The cost: states times work per state
There are 2^n states, one for each subset of nums2. Each state tries every free bit j, which is at most n transitions of constant work. The time is O(N * 2^N). The table dp holds 2^n numbers, so the space is O(2^N).
LeetCode limits n to 14 for Minimum XOR Sum of Two Arrays (LC 1879). That gives 14 * 214 = 229,376 transitions, while the brute force over all pairings needs 14!, which is 87,178,291,200.
This is the reason for the trigger "about 20 items or fewer". At n = 20 the table has 1,048,576 entries and about 20,971,520 transitions. At n = 40 the table would not fit in memory.
What changes when the cost depends on the last item
Some problems, such as visiting every place once, charge a cost between two consecutive items. Then the set of used items is not enough, because the next cost depends on which item came last. The state becomes dp[mask][last], and the time becomes O(N^2 * 2^N).
In Find the Shortest Superstring (LC 943), the cost of appending a string depends on the string before it, so the state keeps last. In Minimum XOR Sum of Two Arrays (LC 1879), the cost of a pair depends only on the two numbers in the pair, so mask alone is enough.
60-Second Pitch
"The brute force tries all n! pairings. The remaining cost depends only on which numbers of nums2 are still free, so I keep one best answer per set of used numbers. I store the set as a bitmask. dp[mask] is the least XOR sum for the first popcount(mask) numbers of nums1. The next number to pair is nums1[i], with i taken from the mask. Each transition sets one more bit, so the new mask is larger. An upward loop over mask makes every value final before it is read. There are 2^n states, each with up to n transitions. That gives O(N * 2^N) time and O(2^N) space, which is fine for n up to about 20."
Common Questions
- Why is bitmask DP only used when n is small?
The table has
2^nentries, and each entry does up tonwork. Atn = 20that is about 21 million transitions, which is fine. Atn = 40the table has more than a trillion entries and does not fit in memory. So the constraint onnis the main signal for this method.- Why can the nums1 index be taken from the mask?
The numbers of
nums1are paired in order, one at each step. Afterkpairs, exactlykbits are set, andnums1[0]tonums1[k - 1]are used. Sobin(mask).count("1")is the index of the next number. Without this ordering, the mask would not be enough.- How do you check and set a bit?
To test bit
j, writemask & (1 << j). It is non-zero when the bit is set. To set it, writemask | (1 << j). To clear it, writemask & ~(1 << j). In C++ and JavaScript, put parentheses aroundmask & (1 << j)before comparing it with 0.- What changes for a travelling-salesman style problem?
The cost between two items depends on the last item visited. So the state becomes
dp[mask][last], and each state tries every next item. The time is O(N^2 * 2^N) and the space is O(N * 2^N). Find the Shortest Superstring (LC 943) is an example of this form.- How would you recover the actual pairing, not only its cost?
Store, for each
nxt, thejthat gave the smaller value. Then start at the full mask and move backward: the storedjis thenums2index paired withnums1[popcount(mask) - 1], and the previous mask is the current mask with bitjcleared. Repeat until the mask is 0.
SWE Reasoning
Spot it
- The input has a small size, about 20 items or fewer. The constraint often says
n <= 14,n <= 16orn <= 20. - The question asks for the best way to assign, pair, group or visit every item exactly once.
- The answer after some choices depends on which items are used, not on the order in which they were used.
- The brute force tries all
n!orders, and many of them reach the same set of used items. - A greedy rule or a sort does not give the best answer, because each choice changes which choices remain.
The invariant, step by step
dphas2^nentries, all set to infinity, exceptdp[0] = 0: no pairs cost nothing.- The loop reaches
mask. It computesi = bin(mask).count("1"). Exactlyinumbers ofnums2are paired, and they were paired withnums1[0]tonums1[i - 1]. - If
i == n, every number is paired and nothing more is added. - Otherwise it tries each bit
jthat is not set inmask. This pairsnums1[i]with the freenums2[j]. - It sets
nxt = mask | (1 << j)and keeps the smaller ofdp[nxt]anddp[mask] + (nums1[i] ^ nums2[j]). nxtis larger thanmask, so the loop reaches it later. By then every smaller mask that can lead tonxthas written its value, sodp[nxt]is final when it is read.- The answer is
dp[(1 << n) - 1], where every bit is set.
Traps
- Looping over every
i. The mask records the usednums2numbers only. If the code tries everynums1[i]from each mask, onenums1number can be paired many times. Fornums1 = [2, 4, 8]andnums2 = [1, 1, 1], the result is 9 instead of 17. - Downward loop over
mask. The loop reads larger masks before they are written. The answer stays infinity. Fornums1 = [1, 2]andnums2 = [2, 3], it returnsinfinstead of 2. - Missing parentheses in C++. In C++ and JavaScript,
mask & 1 << j == 0is read asmask & (1 << j == 0), which is always 0. Write(mask & (1 << j)) == 0. - Wrong size. The table needs
1 << nentries. Usingnentries raises an index error, and using2 ** nwith a largencan use more memory than you have. Checknfirst. - Forgetting the last item. When the cost depends on the previous item,
dp[mask]loses that fact and the result is wrong. Addlastto the state.