1799. Maximize Score After N Operations


Problem Description

In this LeetCode problem, we are given an array of positive integers named nums, which has an even number of elements (2 * n). Our task is to perform n operations on this array to maximize our score.

During each operation, we must:

  • Select two distinct elements from the array, let's call them x and y.
  • Calculate the score for the operation, which is the product of the operation index (1-indexed) and the greatest common divisor (GCD) of x and y (i.e., i * gcd(x, y)).
  • Remove both x and y from the array upon completion of the operation.

The goal is to find the strategy that gives us the highest total score after performing all n operations.

Intuition

The solution to this problem is based on using dynamic programming and bit manipulation to explore all possible pairings of the numbers in the array. Since the array size is 2 * n, we immediately know that the total number of states that we need to consider is 2^(2 * n) (each number can be either included or not in a potential solution, hence 2 states for each and we have 2 * n numbers).

However, we are interested only in pairs of numbers (since for each operation we must choose two elements), and this leads to an optimization. For every state represented by a bitmask (where the i-th bit represents whether the i-th number is included), we only need to consider the states where an even number of bits are set because each operation removes 2 numbers, corresponding to forming pairs.

The intuition behind the solution is as follows:

  1. Precompute the GCD for all pairs of numbers to avoid recalculating it during the dynamic programming process.

  2. Use a bitmask to represent the state of the array after each operation:

    • If 'k' represents the current bitmask, then k.bit_count() tells us how many numbers are currently in the subset, which must be even.
    • For each possible pair ('i', 'j'), where i and j are the indices of the elements in nums, we form a new state by turning off the bits at position i and j in the bitmask.
  3. The dynamic programming table f has a size of 1 << m, where m is the total number of elements in the original array. Each entry f[k] represents the maximum score that can be obtained with the subset represented by the bitmask k.

  4. The transition between states occurs by choosing pairs of numbers, removing them (creating a new state without these numbers), and updating the score with the value for the current operation.

  5. The solution progresses by incrementing the count of the operations (accessible via cnt // 2 which gives [1, 2, 3, ..., n]), and thus increasing the potential score for larger operations.

  6. Finally, the highest score after all operations is stored in f[-1], which represents the state where all numbers have been paired and removed.

By following this approach, we smartly analyze only the relevant subsets of nums, and efficiently calculate the maximum possible score without brute-forcing through all possible pairings.

Learn more about Math, Dynamic Programming, Backtracking and Bitmask patterns.

Not Sure What to Study? Take the 2-min Quiz to Find Your Missing Piece๏ผš

Depth first search can be used to find whether two components in a graph are connected.

Solution Approach

The implementation of the solution follows the dynamic programming approach discussed in the intuition section. Here's a step-by-step breakdown of the implementation:

  • Precomputation of GCDs: A 2D array g is created to store the precomputed GCDs of all possible pairs of elements in nums. This precomputation saves time since calculating the GCD can be expensive and we would otherwise repeat the calculations multiple times.

    1g = [[0] * m for _ in range(m)]
    2for i in range(m):
    3    for j in range(i + 1, m):
    4        g[i][j] = gcd(nums[i], nums[j])
  • Dynamic Programming Table Initialization: The f array is initialized to have 1 << m elements, each element representing the maximum score possible for each subset of the original array. Here, m is the size of the nums array (which is 2 * n).

    1f = [0] * (1 << m)
  • Iterating Over Subsets: The algorithm iterates over all possible subsets of nums represented by the bitmask k.

    1for k in range(1 << m):
  • Ensuring Pairs: Since we only need subsets with an even number of elements, we use a conditional check to proceed only when k.bit_count() is even:

    1if (cnt := k.bit_count()) % 2 == 0:
  • Iterating Over Pairs to Form the Next State: For each subset k, we iterate over all pairs (i, j) where i < j. If both nums[i] and nums[j] are part of the subset k, we calculate the new state by removing i and j from k and updating f[k] if we get a higher score.

    1for i in range(m):
    2    if k >> i & 1:
    3        for j in range(i + 1, m):
    4            if k >> j & 1:
    5                f[k] = max(
    6                    f[k],
    7                    f[k ^ (1 << i) ^ (1 << j)] + cnt // 2 * g[i][j],
    8                )
  • Bit Manipulation: The code uses bitwise operations to check if elements i and j are included in subset k (k >> i & 1 and k >> j & 1), and to form the new state by removing those elements from the subset (k ^ (1 << i) ^ (1 << j)).

  • Dynamic Programming Transition: The dynamic programming step is the calculation of the score and updating the state. For each pair, the optimal score is updated using the precomputed GCD by:

    1f[k ^ (1 << i) ^ (1 << j)] + cnt // 2 * g[i][j]

    This represents the score from the previous state plus the score obtained from the current pair of numbers, multiplied by the operation index.

  • Final Answer: After iterating through all possible subsets and updating the f table, the final answer represents the maximum score when all numbers have been used, which is stored in f[-1].

It is important to note that this solution leverages the bit representation of subsets to efficiently manage states and avoid recomputing values. This economy of computation is what allows the algorithm to find the solution within the time constraints of the problem.

Discover Your Strengths and Weaknesses: Take Our 2-Minute Quiz to Tailor Your Study Plan:

In a binary min heap, the maximum element can be found in:

Example Walkthrough

Let's consider a small example to illustrate the solution approach. Suppose nums = [3, 4, 2, 8], which means n = 2 (since the nums array has 2n elements). We aim to perform n = 2 operations to maximize our cumulative score.

Following the solution approach:

  1. Precomputation of GCDs: We first precompute the GCDs for all possible pairs of elements in nums. For our nums, we would calculate:

    • GCD(3, 4) = 1
    • GCD(3, 2) = 1
    • GCD(3, 8) = 1
    • GCD(4, 2) = 2
    • GCD(4, 8) = 4
    • GCD(2, 8) = 2
  2. Dynamic Programming Table Initialization: We initialize an array f of length 1 << m (which is 1 << 4 or 16), to represent all possible subsets of the original array.

  3. Iterating Over Subsets: We iterate over all possible subsets (0 to 15) using a bitmask to represent each subset.

  4. Ensuring Pairs: We only continue the iterative step for subsets with an even number of elements. For instance, for bitmask 1010 (which represents the subset {3, 2}), we find that it has an even number of elements set (bit_count is 2).

  5. Iterating Over Pairs to Form the Next State: For each even-sized subset, we check for pairs that are in the set and calculate the new score by considering the removal of these pairs. For our subset represented by 1010, we could consider removing 3 and 2 to create a state with no elements. The new score would be f[0] + 1 * GCD(3, 2), since cnt // 2 gives us the operation index 1.

  6. Bit Manipulation and Dynamic Programming Transition: We use bit manipulation to form new subsets and update f with the best score possible at each step. For example, after the first pair is processed in the previous step, the only remaining valid pair in a full subset is (4, 8). The transition in the f array would be calculated, and the score added to f[-1] would be f[0] + 2 * GCD(4, 8), where 2 is the operation index for the second operation.

  7. Final Answer: After going through all subsets and updating the f table, we would look at f[-1] for our final score. For our particular nums, with the operations performed, f[-1] would give us the maximum score possible after performing all n operations while considering the operation index and maximizing the GCD value each time.

This walkthrough demonstrates the use of dynamic programming and bit manipulation to consider and evaluate only the relevant subsets of the nums array, which allows calculating the maximum score with optimal time complexity.

Solution Implementation

1from math import gcd
2
3class Solution:
4    def max_score(self, nums: List[int]) -> int:
5        # The length of the nums list.
6        length = len(nums)
7      
8        # Dynamic programming array to keep track of maximum scores.
9        # The index will represent the state of which numbers have been paired.
10        dp = [0] * (1 << length)
11      
12        # Pre-calculate the gcd for all pairs and store them for reuse.
13        gcd_pairs = [[0] * length for _ in range(length)]
14        for i in range(length):
15            for j in range(i + 1, length):
16                gcd_pairs[i][j] = gcd(nums[i], nums[j])
17      
18        # Iterate through all possible states of pairs.
19        for state in range(1 << length):
20            # Count bits to determine how many numbers have been paired up.
21            pairs_count = bin(state).count("1")
22            # Continue only if pairs_count is even, since we can only pair up numbers if there's an even count.
23            if pairs_count % 2 == 0:
24                # Test all available pairs in the current state.
25                for i in range(length):
26                    if state & (1 << i):
27                        for j in range(i + 1, length):
28                            if state & (1 << j):
29                                # Form next state by removing a pair at indices i and j.
30                                next_state = state ^ (1 << i) ^ (1 << j)
31                                # Calculate the score for this pair at this count
32                                pair_score = (pairs_count // 2) * gcd_pairs[i][j]
33                                # Update the max score if this pairing leads to a better score.
34                                # The score is the sum of the score for the previous state and
35                                # pair_score for the current operation.
36                                dp[state] = max(dp[state], dp[next_state] + pair_score)
37      
38        # Return max score, which is stored at the last index of the dp array.
39        return dp[-1]
40
1class Solution {
2    public int maxScore(int[] nums) {
3        // Find the length of the nums array.
4        int size = nums.length;
5        // Initialize adjacency matrix to store the gcd of each pair.
6        int[][] gcdMatrix = new int[size][size];
7      
8        // Precompute the GCD (Greatest Common Divisor) for all pairs and store in gcdMatrix.
9        for (int i = 0; i < size; ++i) {
10            for (int j = i + 1; j < size; ++j) {
11                gcdMatrix[i][j] = gcd(nums[i], nums[j]);
12            }
13        }
14      
15        // Initialize a dp (dynamic programming) array to store the maximum score for each subset of indices.
16        int[] dp = new int[1 << size];
17        // Iterate over all possible subsets of nums.
18        for (int subsetMask = 0; subsetMask < (1 << size); ++subsetMask) {
19            // Count the number of elements in the current subset.
20            int count = Integer.bitCount(subsetMask);
21            // If the subset contains an even number of elements.
22            if (count % 2 == 0) {
23                // Iterate over all pairs of indices in the subset.
24                for (int i = 0; i < size; ++i) {
25                    if (((subsetMask >> i) & 1) == 1) {
26                        for (int j = i + 1; j < size; ++j) {
27                            if (((subsetMask >> j) & 1) == 1) {
28                                // Calculate the new subset by removing the two selected indices.
29                                int newSubsetMask = subsetMask ^ (1 << i) ^ (1 << j);
30                                // Update the dp array with the maximum score achievable by adding the current pair's score.
31                                dp[subsetMask] = Math.max(
32                                        dp[subsetMask], 
33                                        dp[newSubsetMask] + count / 2 * gcdMatrix[i][j]);
34                            }
35                        }
36                    }
37                }
38            }
39        }
40        // The answer will be in dp array at index corresponding to the subset containing all numbers.
41        return dp[(1 << size) - 1];
42    }
43
44    // Utility function to calculate the GCD of two numbers.
45    private int gcd(int a, int b) {
46        return b == 0 ? a : gcd(b, a % b);
47    }
48}
49
1#include <vector>
2#include <cstring> // for using memset
3#include <algorithm>  // for using std::max and std::fill_n
4
5using std::vector;
6using std::max;
7
8class Solution {
9public:
10    int maxScore(vector<int>& nums) {
11        int numCount = nums.size(); // Total number of elements in nums
12      
13        // Calculate gcd for all pairs and store in a 2D vector
14        vector<vector<int>> gcdValues(numCount, vector<int>(numCount, 0));
15        for (int i = 0; i < numCount; ++i) {
16            for (int j = i + 1; j < numCount; ++j) {
17                gcdValues[i][j] = gcd(nums[i], nums[j]);
18            }
19        }
20      
21        // dp is used to memoize the maximum score for a given subset of nums
22        vector<int> dp(1 << numCount, 0); // 1 << numCount creates bitmask for all subsets
23      
24        // Iterate over all subsets
25        for (int subset = 0; subset < (1 << numCount); ++subset) {
26            int bitCount = __builtin_popcount(subset); // Count number of set bits
27            // If bitCount is even, we can form pairs
28            if (bitCount % 2 == 0) {
29                for (int i = 0; i < numCount; ++i) {
30                    if (subset & (1 << i)) { // Check if the ith element is in the subset
31                        for (int j = i + 1; j < numCount; ++j) {
32                            if (subset & (1 << j)) { // Check if the jth element is in the subset
33                                // Calculate the new score for the new subset created after removing paired elements (i and j)
34                                int newSubset = subset ^ (1 << i) ^ (1 << j);
35                                int newScore = dp[newSubset] + (bitCount / 2) * gcdValues[i][j];
36                                // Update the maximum score for the current subset
37                                dp[subset] = max(dp[subset], newScore);
38                            }
39                        }
40                    }
41                }
42            }
43        }
44      
45        // Return the maximum score for the full set of nums
46        return dp[(1 << numCount) - 1]; // This bitmask represents the full set
47    }
48  
49private:
50    // Utility function to calculate GCD using Euclidean algorithm
51    int gcd(int a, int b) {
52        while (b != 0) {
53            int temp = b;
54            b = a % b;
55            a = temp;
56        }
57        return a;
58    }
59};
60
1function maxScore(nums: number[]): number {
2    const n = nums.length;
3    // f will keep the maximum scores for each subset of nums based on the bitmask.
4    const scores: number[] = new Array(1 << n).fill(0);
5  
6    // g will store the gcd results for all possible pairs in nums.
7    const gcdResults: number[][] = new Array(n).fill(0).map(() => new Array(n).fill(0));
8  
9    // Precalculate gcd for all pairs of elements.
10    for (let i = 0; i < n; ++i) {
11        for (let j = i + 1; j < n; ++j) {
12            gcdResults[i][j] = gcd(nums[i], nums[j]);
13        }
14    }
15  
16    // Iterate through all subsets represented by bitmasks.
17    for (let mask = 0; mask < 1 << n; ++mask) {
18        const count = bitCount(mask);
19      
20        // Calculate scores only for even-sized subsets because pairs are needed.
21        if (count % 2 === 0) {
22            for (let i = 0; i < n; ++i) {
23                // Check whether the ith element is included in the current subset.
24                if ((mask >> i) & 1) {
25                    for (let j = i + 1; j < n; ++j) {
26                        // Check whether the jth element is included in the current subset.
27                        if ((mask >> j) & 1) {
28                            // Calculate the score using a subset without i and j elements.
29                            const subsetScore = scores[mask ^ (1 << i) ^ (1 << j)] + (count / 2) * gcdResults[i][j];
30                            // Update the score of the current subset if it's improved.
31                            scores[mask] = Math.max(scores[mask], subsetScore);
32                        }
33                    }
34                }
35            }
36        }
37    }
38    // The maximum score will be located at the last index, representing the entire set.
39    return scores[(1 << n) - 1];
40}
41
42// Utility function to calculate the greatest common divisor of two numbers.
43function gcd(a: number, b: number): number {
44    return b ? gcd(b, a % b) : a;
45}
46
47// Function to count the number of set bits in an integer.
48function bitCount(bits: number): number {
49    bits = bits - ((bits >>> 1) & 0x55555555);
50    bits = (bits & 0x33333333) + ((bits >>> 2) & 0x33333333);
51    bits = (bits + (bits >>> 4)) & 0x0f0f0f0f;
52    bits = bits + (bits >>> 8);
53    bits = bits + (bits >>> 16);
54    return bits & 0x3f;
55}
56
Not Sure What to Study? Take the 2-min Quiz๏ผš

What is the best way of checking if an element exists in an unsorted array once in terms of time complexity? Select the best that applies.

Time and Space Complexity

The given code is a dynamic programming solution where f is used to store the intermediate results, and g is used to store the gcd calculations between pairs of numbers in nums.

Time Complexity:

The time complexity of the code is determined by the several nested loops:

  1. The outer loop iterates over the subsets of nums which has a complexity of O(2^m) where m is the length of nums.
  2. Inside the if block, two nested loops iterate through the pairs of numbers to update the f array. These two loops contribute O(m^2).
  3. The gcd calculations for each pair are done upfront and stored in g with a complexity of O(m^2).

The total time complexity is the product of the complexities of these operations since the gcd calculations are done outside of the main loop:

Time Complexity: O(m^2 + 2^m * m^2) Since the gcd calculations are not part of the critical path, they are not included in the exponentiation with respect to 2^m.

Simplifying, we have the worst-case time complexity as: O(2^m * m^2)

Space Complexity:

The space complexity is determined by the space used by the f and g arrays.

  1. The f array uses O(2^m) space as it has an entry for each subset of nums.
  2. The g array uses O(m^2) space as it stores gcd values for each unique pair of numbers in nums.

Space Complexity: O(2^m + m^2)

Since 2^m is the dominating term, the worst-case space complexity simplifies to: O(2^m)

Learn more about how to find time and space complexity quickly using problem constraints.

Fast Track Your Learning with Our Quick Skills Quiz:

How would you design a stack which has a function min that returns the minimum element in the stack, in addition to push and pop? All push, pop, min should have running time O(1).


Recommended Readings


Got a question?ย Ask the Teaching Assistantย anything you don't understand.

Still not clear? Ask in the Forum, ย Discordย orย Submitย the part you don't understand to our editors.

โ†
โ†‘TA ๐Ÿ‘จโ€๐Ÿซ