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
andy
. - Calculate the score for the operation, which is the product of the operation index (1-indexed) and the greatest common divisor (GCD) of
x
andy
(i.e.,i * gcd(x, y)
). - Remove both
x
andy
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.
Flowchart Walkthrough
First, let's use the algorithm flowchart to deduce the problem-solving approach for LeetCode 1799. Maximize Score After N Operations. We'll analyze each step using the Flowchart. Here's a step-by-step walkthrough:
Is it a graph?
- No: This problem does not involve graph structures like nodes and edges directly.
Need to solve for kth smallest/largest?
- No: The problem is about maximizing the score, not finding a specific order such as kth smallest or largest.
Involves Linked Lists?
- No: This problem does not involve any linked list operations.
Does the problem have small constraints?
- Yes: Given the complexity of pairing numbers in N operations, it likely has constraints small enough to consider exhaustive approaches.
Brute force / Backtracking?
- Yes: To maximize score, exploring all pairings of integers using backtracking is appropriate. We need to try out different combinations and choose the pairing strategy that yields the maximum score.
Conclusion: The flowchart analysis suggests using a Backtracking approach to explore all possible pairings of numbers in order to maximize the score, aligning perfectly with the requirements of LeetCode 1799.
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:
-
Precompute the GCD for all pairs of numbers to avoid recalculating it during the dynamic programming process.
-
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
andj
are the indices of the elements innums
, we form a new state by turning off the bits at positioni
andj
in the bitmask.
- If 'k' represents the current bitmask, then
-
The dynamic programming table
f
has a size of1 << m
, wherem
is the total number of elements in the original array. Each entryf[k]
represents the maximum score that can be obtained with the subset represented by the bitmaskk
. -
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.
-
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. -
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.
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 innums
. This precomputation saves time since calculating the GCD can be expensive and we would otherwise repeat the calculations multiple times.g = [[0] * m for _ in range(m)] for i in range(m): for j in range(i + 1, m): g[i][j] = gcd(nums[i], nums[j])
-
Dynamic Programming Table Initialization: The
f
array is initialized to have1 << m
elements, each element representing the maximum score possible for each subset of the original array. Here,m
is the size of thenums
array (which is2 * n
).f = [0] * (1 << m)
-
Iterating Over Subsets: The algorithm iterates over all possible subsets of
nums
represented by the bitmaskk
.for 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:if (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
) wherei < j
. If bothnums[i]
andnums[j]
are part of the subsetk
, we calculate the new state by removingi
andj
fromk
and updatingf[k]
if we get a higher score.for i in range(m): if k >> i & 1: for j in range(i + 1, m): if k >> j & 1: f[k] = max( f[k], f[k ^ (1 << i) ^ (1 << j)] + cnt // 2 * g[i][j], )
-
Bit Manipulation: The code uses bitwise operations to check if elements
i
andj
are included in subsetk
(k >> i & 1
andk >> 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:
f[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 inf[-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.
Ready to land your dream job?
Unlock your dream job with a 2-minute evaluator for a personalized learning plan!
Start EvaluatorExample 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:
-
Precomputation of GCDs: We first precompute the GCDs for all possible pairs of elements in
nums
. For ournums
, 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
-
Dynamic Programming Table Initialization: We initialize an array
f
of length1 << m
(which is1 << 4
or 16), to represent all possible subsets of the original array. -
Iterating Over Subsets: We iterate over all possible subsets (0 to 15) using a bitmask to represent each subset.
-
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). -
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 removing3
and2
to create a state with no elements. The new score would bef[0] + 1 * GCD(3, 2)
, sincecnt // 2
gives us the operation index1
. -
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 thef
array would be calculated, and the score added tof[-1]
would bef[0] + 2 * GCD(4, 8)
, where 2 is the operation index for the second operation. -
Final Answer: After going through all subsets and updating the
f
table, we would look atf[-1]
for our final score. For our particularnums
, with the operations performed,f[-1]
would give us the maximum score possible after performing alln
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
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:
- The outer loop iterates over the subsets of
nums
which has a complexity ofO(2^m)
wherem
is the length ofnums
. - Inside the
if
block, two nested loops iterate through the pairs of numbers to update thef
array. These two loops contributeO(m^2)
. - The
gcd
calculations for each pair are done upfront and stored ing
with a complexity ofO(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.
- The
f
array usesO(2^m)
space as it has an entry for each subset ofnums
. - The
g
array usesO(m^2)
space as it stores gcd values for each unique pair of numbers innums
.
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.
Consider the classic dynamic programming of longest increasing subsequence:
Find the length of the longest subsequence of a given sequence such that all elements of the subsequence are sorted in increasing order.
For example, the length of LIS for [50, 3, 10, 7, 40, 80]
is 4
and LIS is
[3, 7, 40, 80]
.
What is the recurrence relation?
Recommended Readings
Math for Technical Interviews How much math do I need to know for technical interviews The short answer is about high school level math Computer science is often associated with math and some universities even place their computer science department under the math faculty However the reality is that you
What is Dynamic Programming Prerequisite DFS problems dfs_intro Backtracking problems backtracking Memoization problems memoization_intro Pruning problems backtracking_pruning Dynamic programming is an algorithmic optimization technique that breaks down a complicated problem into smaller overlapping sub problems in a recursive manner and uses solutions to the sub problems to construct a solution
Backtracking Template Prereq DFS with States problems dfs_with_states Combinatorial search problems Combinatorial search problems involve finding groupings and assignments of objects that satisfy certain conditions Finding all permutations combinations subsets and solving Sudoku are classic combinatorial problems The time complexity of combinatorial problems often grows rapidly with the size of
Want a Structured Path to Master System Design Too? Don’t Miss This!