Maximize Score After N Operations
Hard · Backtracking · Explanation and code example
Bitmask DP (Bottom-Up)
How it works
Instead of recursion with memoization, we can fill the DP table iteratively. The key observation is that a state with more bits set depends on states with fewer bits set. By iterating from higher masks (more elements used) to lower masks (fewer elements used), we ensure that when we process a state, all states it transitions to are already computed.
The operation number for a given mask is determined by counting how many bits are set and dividing by 2, then adding 1 for the next operation.
Steps
- Precompute the
GCD[i][j]table for all pairs. - Create a
dparray of size2^ninitialized to0. - Iterate
maskfrom2^n - 1down to0:- Count the number of set bits. Skip if it is odd (invalid state).
- Calculate
op = bits / 2 + 1(the next operation number). - For each pair
(i, j)not in the current mask:- Compute
new_mask = mask | (1 << i) | (1 << j). - Update
dp[mask] = max(dp[mask], op * GCD[i][j] + dp[new_mask]).
- Compute
- Return
dp[0].
Complexity
- Time complexity:
O(n ^ 2 * (2 ^ n + log m)) - Space complexity:
O(n ^ 2 + 2 ^ n)
Where
nis the size of the arraynumsandmis the maximum element in the array.
Things to watch for
Forgetting to Multiply by Operation Number
Each operation's score is i * gcd(nums[a], nums[b]) where i is the operation number (1-indexed). Forgetting the multiplier or using 0-indexed operation numbers produces incorrect scores.
Using Wrong Bit Count for Operation Number
In bitmask DP, the operation number is derived from the number of set bits divided by 2, plus 1 (since each operation uses 2 elements). Off-by-one errors in this calculation cascade through all subsequent operations.
Not Precomputing GCD Values
Recomputing gcd(nums[i], nums[j]) inside the DP loop for every state transition leads to TLE on larger inputs. Precomputing all pairwise GCD values into a 2D table provides significant speedup.
Skipping Invalid Bitmask States
In bottom-up DP, states with an odd number of set bits are invalid (you cannot pair up an odd number of elements). Failing to skip these states wastes computation and can cause incorrect transitions.
Incorrect Bitmask Transition
When marking elements as used, the new mask should be mask | (1 << i) | (1 << j). Using XOR or other operations instead of OR can incorrectly toggle bits that are already set, corrupting the state.
Python code example
from typing import List
from math import gcd
class Solution:
def maxScore(self, nums: List[int]) -> int:
n = len(nums)
N = 1 << n
GCD = [[0] * n for _ in range(n)]
for i in range(n):
for j in range(i + 1, n):
GCD[i][j] = gcd(nums[i], nums[j])
dp = [0] * N
for mask in range(N - 1, -1, -1):
bits = bin(mask).count('1')
if bits % 2 == 1:
continue
op = bits // 2 + 1
for i in range(n):
if mask & (1 << i):
continue
for j in range(i + 1, n):
if mask & (1 << j):
continue
new_mask = mask | (1 << i) | (1 << j)
dp[mask] = max(dp[mask], op * GCD[i][j] + dp[new_mask])
return dp[0]
C++ code example
class Solution {
public:
int maxScore(vector<int>& nums) {
int n = nums.size();
int N = 1 << n;
vector<vector<int>> GCD(n, vector<int>(n, 0));
for (int i = 0; i < n; i++) {
for (int j = i + 1; j < n; j++) {
GCD[i][j] = std::gcd(nums[i], nums[j]);
}
}
vector<int> dp(N, 0);
for (int mask = N - 1; mask >= 0; mask--) {
int bits = __builtin_popcount(mask);
if (bits % 2 == 1) continue;
int op = bits / 2 + 1;
for (int i = 0; i < n; i++) {
if (mask & (1 << i)) continue;
for (int j = i + 1; j < n; j++) {
if (mask & (1 << j)) continue;
int newMask = mask | (1 << i) | (1 << j);
dp[mask] = max(dp[mask], op * GCD[i][j] + dp[newMask]);
}
}
}
return dp[0];
}
};
Source and license
Adapted from NeetCode’s source · MIT license. Node types are supplied by LeetCode where needed.