3836. Maximum Score Using Exactly K Pairs
Problem Description
You are given two integer arrays nums1 and nums2 of lengths n and m respectively, along with an integer k.
Your task is to choose exactly k pairs of indices (i₁, j₁), (i₂, j₂), ..., (iₖ, jₖ). These pairs must satisfy the following conditions:
- The indices chosen from
nums1must be strictly increasing:0 <= i₁ < i₂ < ... < iₖ < n. - The indices chosen from
nums2must also be strictly increasing:0 <= j₁ < j₂ < ... < jₖ < m.
In other words, both sequences of indices must follow the original order of their respective arrays, and no index can be reused within the same array.
For each chosen pair (i, j), you gain a score equal to the product nums1[i] * nums2[j].
The total score is the sum of the products of all k selected pairs:
score = nums1[i₁] * nums2[j₁] + nums1[i₂] * nums2[j₂] + ... + nums1[iₖ] * nums2[jₖ]
Return an integer representing the maximum achievable total score.
How We Pick the Algorithm
Why Dynamic Programming?
This problem maps to Dynamic Programming through a short path in the full flowchart.
Selecting exactly k pairs from two arrays with increasing indices to maximize total product sum is a 2D DP problem over both index dimensions.
Open in FlowchartIntuition
The key observation in this problem is that we are pairing elements from two arrays while preserving their original order in both. Whenever we form a pair (i, j), the next pair must use indices strictly greater than i in nums1 and strictly greater than j in nums2. This "increasing index" requirement is a strong hint that we can process both arrays from left to right and make decisions step by step.
Let's think about what choices we face when scanning through the two arrays. Suppose we are considering the i-th element of nums1 and the j-th element of nums2. At this moment, we essentially have three options:
- Skip the current element of
nums1: We decide not to usenums1[i-1]in any pair, so we move on with the elements before it. - Skip the current element of
nums2: We decide not to usenums2[j-1]in any pair, so we move on with the elements before it. - Pair them together: We use
nums1[i-1]andnums2[j-1]as a matched pair, gaining a score ofnums1[i-1] * nums2[j-1], and this consumes one of ourkallowed pairs.
Because each decision only depends on the elements we have already processed and the number of pairs we have already chosen, the problem naturally breaks down into smaller subproblems. This is the classic setup for dynamic programming.
To capture the state of a subproblem, we need three pieces of information: how many elements of nums1 we have considered (i), how many elements of nums2 we have considered (j), and how many pairs we have already formed (k). This leads us to define a three-dimensional state f[i][j][k], representing the maximum score achievable when we have looked at the first i elements of nums1, the first j elements of nums2, and chosen exactly k pairs.
Since we want to maximize the total score, each state takes the best value among its three possible transitions. We initialize f[0][0][0] = 0 (no elements considered, no pairs chosen, zero score) and set every other state to negative infinity to mark them as currently unreachable. The use of negative infinity is important: it ensures that invalid combinations (for example, claiming to have formed pairs without having actually chosen valid elements) cannot incorrectly contribute to the answer.
By filling in the table in increasing order of i, j, and k, every state is computed only after the states it depends on are ready. The final answer we seek is f[n][m][K], which represents using all available elements as candidates and selecting exactly K pairs to reach the maximum possible score.
Pattern Learn more about Dynamic Programming patterns.
Solution Approach
Solution 1: Dynamic Programming
We denote the lengths of arrays nums1 and nums2 as n and m respectively, and denote k in the problem as K.
We define a three-dimensional array f, where f[i][j][k] represents the maximum score of selecting exactly k index pairs from the first i elements of nums1 and the first j elements of nums2. Initially, f[0][0][0] = 0, and all other values of f[i][j][k] are set to negative infinity to indicate that they are unreachable states.
We can calculate f[i][j][k] through the following state transition equation:
f[i][j][k] = max(f[i-1][j][k], f[i][j-1][k], f[i-1][j-1][k-1] + nums1[i-1] * nums2[j-1])
The three cases correspond to:
f[i-1][j][k]: We do not select thei-th element ofnums1. The current best score carries over from considering one fewer element ofnums1.f[i][j-1][k]: We do not select thej-th element ofnums2. The current best score carries over from considering one fewer element ofnums2.f[i-1][j-1][k-1] + nums1[i-1] * nums2[j-1]: We select thei-th element ofnums1and thej-th element ofnums2together as a pair. This uses up one pair (so we look back atk-1pairs) and adds the productnums1[i-1] * nums2[j-1]to the previous best score fromf[i-1][j-1][k-1].
In the implementation, we build a (n + 1) × (m + 1) × (K + 1) table filled with negative infinity, then set f[0][0][0] = 0. We iterate over i from 0 to n, j from 0 to m, and k from 0 to K, in increasing order so that every state is computed only after all the states it depends on are ready.
For each state, we guard the transitions with boundary checks:
- The transition from
f[i-1][j][k]is only valid wheni > 0. - The transition from
f[i][j-1][k]is only valid whenj > 0. - The pairing transition from
f[i-1][j-1][k-1]is only valid wheni > 0,j > 0, andk > 0, since forming a pair requires at least one element from each array and at least one remaining slot.
These checks prevent out-of-range index access and ensure that only legitimate states are combined. The negative infinity initialization guarantees that any path which fails to form exactly the required number of valid pairs is never mistakenly counted as a feasible answer.
Finally, we return f[n][m][K], which gives the maximum score obtainable by considering all elements of both arrays and selecting exactly K pairs.
The time complexity is O(n × m × K), since we fill every entry of the three-dimensional table once with constant work per entry. The space complexity is also O(n × m × K) for storing the table.
Example Walkthrough
Let's trace through a small example to see how the dynamic programming solution works.
Input:
nums1 = [2, -1, 3](son = 3)nums2 = [4, 1](som = 2)k = 1(we must choose exactly 1 pair)
Since k = 1, we just want the single pair (i, j) that maximizes nums1[i] * nums2[j]. Let's confirm the DP arrives at the right answer.
State definition: f[i][j][k] = maximum score using the first i elements of nums1, the first j elements of nums2, having chosen exactly k pairs.
Initialization: f[0][0][0] = 0, everything else is -∞.
We only care about layers k = 0 and k = 1. The k = 0 layer is easy: choosing zero pairs always gives score 0, so f[i][j][0] = 0 for all i, j (each carries over from f[i-1][j][0] or f[i][j-1][0]).
Filling the k = 1 layer. The transition is:
f[i][j][1] = max( f[i-1][j][1], // skip nums1[i-1] f[i][j-1][1], // skip nums2[j-1] f[i-1][j-1][0] + nums1[i-1]*nums2[j-1] ) // pair them
Note f[i-1][j-1][0] = 0, so the pairing term is simply the product nums1[i-1] * nums2[j-1].
Let's compute each cell of f[i][j][1] (rows = i from 0→3, columns = j from 0→2):
f[i][j][1] | j=0 | j=1 | j=2 |
|---|---|---|---|
| i=0 | -∞ | -∞ | -∞ |
| i=1 | -∞ | 8 | 8 |
| i=2 | -∞ | 8 | 8 |
| i=3 | -∞ | 12 | 12 |
Walking through the interesting cells:
f[1][1][1]: pair option =nums1[0] * nums2[0] = 2 * 4 = 8. Skip options are-∞. → 8f[1][2][1]: candidates aref[0][2][1] = -∞,f[1][1][1] = 8, and pairnums1[0]*nums2[1] = 2*1 = 2. Max → 8f[2][1][1]: candidates aref[1][1][1] = 8,f[2][0][1] = -∞, and pairnums1[1]*nums2[0] = -1*4 = -4. Max → 8f[3][1][1]: candidates aref[2][1][1] = 8,f[3][0][1] = -∞, and pairnums1[2]*nums2[0] = 3*4 = 12. Max → 12f[3][2][1]: candidates aref[2][2][1] = 8,f[3][1][1] = 12, and pairnums1[2]*nums2[1] = 3*1 = 3. Max → 12
Answer: f[n][m][K] = f[3][2][1] = 12.
This matches our intuition: the best single pair is nums1[2] * nums2[0] = 3 * 4 = 12. The DP correctly explored all valid order-preserving pairings and the negative-infinity initialization kept unreachable states (like having a pair before any element was available) from contaminating the result.
Solution Implementation
1from typing import List
2from math import inf
3
4
5class Solution:
6 def maxScore(self, nums1: List[int], nums2: List[int], k: int) -> int:
7 len1, len2 = len(nums1), len(nums2)
8
9 # dp[i][j][c] = max score using the first i elements of nums1
10 # and the first j elements of nums2, having selected exactly c pairs.
11 # Initialize everything to -inf (invalid state).
12 dp = [
13 [[-inf] * (k + 1) for _ in range(len2 + 1)]
14 for _ in range(len1 + 1)
15 ]
16 # Base case: 0 elements considered, 0 pairs selected -> score 0.
17 dp[0][0][0] = 0
18
19 for i in range(len1 + 1):
20 for j in range(len2 + 1):
21 for c in range(k + 1):
22 # Option 1: skip the i-th element of nums1.
23 if i > 0:
24 dp[i][j][c] = max(dp[i][j][c], dp[i - 1][j][c])
25
26 # Option 2: skip the j-th element of nums2.
27 if j > 0:
28 dp[i][j][c] = max(dp[i][j][c], dp[i][j - 1][c])
29
30 # Option 3: pair nums1[i-1] with nums2[j-1] as the c-th pair.
31 if i > 0 and j > 0 and c > 0:
32 dp[i][j][c] = max(
33 dp[i][j][c],
34 dp[i - 1][j - 1][c - 1] + nums1[i - 1] * nums2[j - 1],
35 )
36
37 # Answer: all elements considered, exactly k pairs selected.
38 return dp[len1][len2][k]
391class Solution {
2 /**
3 * Selects exactly K index pairs (i, j) with i strictly increasing in nums1
4 * and j strictly increasing in nums2, maximizing the sum of products
5 * nums1[i] * nums2[j]. This is solved with a 3D dynamic programming table.
6 *
7 * @param nums1 the first array
8 * @param nums2 the second array
9 * @param k the exact number of pairs to select
10 * @return the maximum achievable score
11 */
12 public long maxScore(int[] nums1, int[] nums2, int k) {
13 int len1 = nums1.length;
14 int len2 = nums2.length;
15
16 // A sufficiently small value to represent "unreachable" states,
17 // divided to avoid overflow when adding products later.
18 long negInfinity = Long.MIN_VALUE / 4;
19
20 // dp[i][j][t] = best score using the first i elements of nums1
21 // and the first j elements of nums2, having picked exactly t pairs.
22 long[][][] dp = new long[len1 + 1][len2 + 1][k + 1];
23
24 // Initialize every state as unreachable.
25 for (int i = 0; i <= len1; i++) {
26 for (int j = 0; j <= len2; j++) {
27 Arrays.fill(dp[i][j], negInfinity);
28 }
29 }
30
31 // Base case: zero elements considered, zero pairs picked, score is 0.
32 dp[0][0][0] = 0;
33
34 for (int i = 0; i <= len1; i++) {
35 for (int j = 0; j <= len2; j++) {
36 for (int t = 0; t <= k; t++) {
37 // Option 1: skip the current element of nums1.
38 if (i > 0) {
39 dp[i][j][t] = Math.max(dp[i][j][t], dp[i - 1][j][t]);
40 }
41
42 // Option 2: skip the current element of nums2.
43 if (j > 0) {
44 dp[i][j][t] = Math.max(dp[i][j][t], dp[i][j - 1][t]);
45 }
46
47 // Option 3: pair nums1[i - 1] with nums2[j - 1] as the t-th pair.
48 if (i > 0 && j > 0 && t > 0) {
49 dp[i][j][t] = Math.max(dp[i][j][t],
50 dp[i - 1][j - 1][t - 1] + (long) nums1[i - 1] * nums2[j - 1]);
51 }
52 }
53 }
54 }
55
56 // Answer: all elements considered with exactly k pairs selected.
57 return dp[len1][len2][k];
58 }
59}
601class Solution {
2public:
3 long long maxScore(vector<int>& nums1, vector<int>& nums2, int K) {
4 int n = nums1.size();
5 int m = nums2.size();
6
7 // A sufficiently small value used to represent an unreachable state.
8 // Dividing by 4 prevents overflow when adding products later on.
9 const long long kNegInf = LLONG_MIN / 4;
10
11 // dp[i][j][k] = the maximum total score obtainable by considering
12 // the first i elements of nums1 and the first j elements of nums2,
13 // having formed exactly k matched pairs.
14 // Each pair contributes nums1[a] * nums2[b], where a and b are
15 // chosen in strictly increasing order (subsequence alignment).
16 vector<vector<vector<long long>>> dp(
17 n + 1,
18 vector<vector<long long>>(m + 1, vector<long long>(K + 1, kNegInf)));
19
20 // Base case: zero elements used and zero pairs formed yields score 0.
21 dp[0][0][0] = 0;
22
23 for (int i = 0; i <= n; ++i) {
24 for (int j = 0; j <= m; ++j) {
25 for (int k = 0; k <= K; ++k) {
26 // Option 1: skip the i-th element of nums1.
27 if (i > 0) {
28 dp[i][j][k] = max(dp[i][j][k], dp[i - 1][j][k]);
29 }
30
31 // Option 2: skip the j-th element of nums2.
32 if (j > 0) {
33 dp[i][j][k] = max(dp[i][j][k], dp[i][j - 1][k]);
34 }
35
36 // Option 3: match nums1[i-1] with nums2[j-1] to form a
37 // new pair, adding their product to a state with k-1 pairs.
38 if (i > 0 && j > 0 && k > 0) {
39 long long pairScore =
40 1LL * nums1[i - 1] * nums2[j - 1];
41 dp[i][j][k] = max(
42 dp[i][j][k], dp[i - 1][j - 1][k - 1] + pairScore);
43 }
44 }
45 }
46 }
47
48 // The answer is the best score using all elements with exactly K pairs.
49 return dp[n][m][K];
50 }
51};
521/**
2 * Computes the maximum score obtainable by selecting exactly K pairs of indices.
3 *
4 * The selection rule: we pick K indices i_1 < i_2 < ... < i_K from nums1 and
5 * K indices j_1 < j_2 < ... < j_K from nums2 (both strictly increasing, i.e.
6 * preserving the original order), then pair them up positionally. The score is
7 * the sum of nums1[i_t] * nums2[j_t] over all K chosen pairs.
8 *
9 * This is solved with a 3D dynamic programming table:
10 * dp[i][j][k] = the best score considering the first i elements of nums1 and
11 * the first j elements of nums2, having selected exactly k pairs.
12 *
13 * @param nums1 The first input array.
14 * @param nums2 The second input array.
15 * @param k The exact number of pairs to select.
16 * @returns The maximum achievable score.
17 */
18function maxScore(nums1: number[], nums2: number[], k: number): number {
19 const n: number = nums1.length;
20 const m: number = nums2.length;
21
22 // Sentinel value representing an unreachable / invalid state.
23 const NEG: number = -1e18;
24
25 // dp[i][j][t] holds the best score using the first i elements of nums1,
26 // the first j elements of nums2, with exactly t pairs chosen so far.
27 const dp: number[][][] = Array.from({ length: n + 1 }, () =>
28 Array.from({ length: m + 1 }, () =>
29 Array<number>(k + 1).fill(NEG),
30 ),
31 );
32
33 // Base case: zero elements consumed and zero pairs chosen yields score 0.
34 dp[0][0][0] = 0;
35
36 for (let i = 0; i <= n; i++) {
37 for (let j = 0; j <= m; j++) {
38 for (let t = 0; t <= k; t++) {
39 // Option 1: skip the current element of nums1.
40 if (i > 0) {
41 dp[i][j][t] = Math.max(dp[i][j][t], dp[i - 1][j][t]);
42 }
43
44 // Option 2: skip the current element of nums2.
45 if (j > 0) {
46 dp[i][j][t] = Math.max(dp[i][j][t], dp[i][j - 1][t]);
47 }
48
49 // Option 3: pair nums1[i - 1] with nums2[j - 1] as the t-th pair.
50 if (i > 0 && j > 0 && t > 0) {
51 dp[i][j][t] = Math.max(
52 dp[i][j][t],
53 dp[i - 1][j - 1][t - 1] + nums1[i - 1] * nums2[j - 1],
54 );
55 }
56 }
57 }
58 }
59
60 // The answer is the best score using all elements with exactly k pairs chosen.
61 return dp[n][m][k];
62}
63Time and Space Complexity
-
Time Complexity:
O(n × m × K). The algorithm uses three nested loops iterating overifrom0ton,jfrom0tom, andkfrom0toK, wherenandmare the lengths of arraysnums1andnums2respectively, andKiskin the problem. Each state transition inside the innermost loop performs constant-timemaxcomparisons and arithmetic operations, so the total time is proportional to the number of states(n + 1) × (m + 1) × (K + 1), givingO(n × m × K). -
Space Complexity:
O(n × m × K). A three-dimensional DP arrayfof size(n + 1) × (m + 1) × (K + 1)is allocated to store all intermediate states, which dominates the space usage, resulting inO(n × m × K).
Pattern Learn more about how to find time and space complexity quickly.
Common Pitfalls
Pitfall 1: Misusing the State Transition When Skipping Elements From Both Arrays Simultaneously
A frequent mistake is assuming you need a separate transition that skips both the i-th element of nums1 and the j-th element of nums2 at the same time, such as f[i-1][j-1][k]. Developers often add this thinking it covers a "missing" case.
Why it's a problem:
This transition is redundant and reveals a misunderstanding of how the DP decomposes. The state f[i-1][j-1][k] is already reachable through the existing chain: f[i-1][j-1][k] → f[i][j-1][k] → f[i][j][k] (or via the other ordering). The two single-skip transitions (f[i-1][j][k] and f[i][j-1][k]) together already cover every combination of skipping. Adding the double-skip is harmless to correctness but signals confusion, and in optimized rolling-array versions it can lead to reading stale values and producing wrong answers.
Solution:
Trust that the two independent skip transitions fully cover the "don't pair these specific elements" cases. Keep the transitions minimal:
# Skip nums1[i-1]
if i > 0:
dp[i][j][c] = max(dp[i][j][c], dp[i - 1][j][c])
# Skip nums2[j-1]
if j > 0:
dp[i][j][c] = max(dp[i][j][c], dp[i][j - 1][c])
Pitfall 2: Returning a -inf Value Without Recognizing Infeasibility
When k > min(n, m), it is impossible to select k valid pairs, so dp[len1][len2][k] remains -inf. Returning this raw value (or worse, converting it to a huge negative integer) can break downstream logic or violate the expected return type.
Why it's a problem:
The problem guarantees k <= min(n, m), but if you reuse this code in a context where that guarantee doesn't hold, returning float('-inf') instead of an int causes type inconsistencies and silent bugs.
Solution:
Validate feasibility explicitly when in doubt, or rely on the constraint:
if k > min(len1, len2):
return -1 # or raise an error, depending on contract
return dp[len1][len2][k]
Pitfall 3: Incorrect Base Case Initialization
A subtle error is initializing dp[i][0][0] = 0 and dp[0][j][0] = 0 for all i, j (thinking "zero pairs always scores zero"), instead of only dp[0][0][0] = 0.
Why it's a problem:
While selecting zero pairs does score zero, the propagation of the base case must happen through the transitions, not through pre-filling. Pre-filling the entire c = 0 plane is actually fine for correctness here (since skipping is always allowed), but the deeper issue arises if a developer mistakenly pre-fills c > 0 layers or sets non-zero-pair states to 0. That would incorrectly mark unreachable states as reachable with score 0, allowing the algorithm to "select" fewer than k pairs and still report a feasible answer.
Solution:
Initialize only dp[0][0][0] = 0 and let the recurrence propagate the zero-pair plane naturally:
dp = [[[-inf] * (k + 1) for _ in range(len2 + 1)] for _ in range(len1 + 1)]
dp[0][0][0] = 0 # The single, correct base case.
Pitfall 4: Memory Limit Exceeded Due to the Full 3D Table
The O(n × m × K) space can be prohibitive for large inputs, leading to a Memory Limit Exceeded error even when the time complexity is acceptable.
Why it's a problem:
When n, m, and K are all large, allocating (n+1) × (m+1) × (K+1) entries may exhaust available memory.
Solution:
Observe that dp[i][...] only depends on dp[i-1][...] and dp[i][...]. Roll the first dimension to reduce space to O(m × K):
from math import inf
from typing import List
class Solution:
def maxScore(self, nums1: List[int], nums2: List[int], k: int) -> int:
len1, len2 = len(nums1), len(nums2)
prev = [[-inf] * (k + 1) for _ in range(len2 + 1)]
prev[0][0] = 0
for i in range(1, len1 + 1):
cur = [[-inf] * (k + 1) for _ in range(len2 + 1)]
cur[0][0] = 0
for j in range(1, len2 + 1):
for c in range(k + 1):
# Skip nums1[i-1]: inherit from prev row.
best = max(prev[j][c], cur[j - 1][c])
# Pair them.
if c > 0:
best = max(best, prev[j - 1][c - 1] + nums1[i - 1] * nums2[j - 1])
cur[j][c] = best
prev = cur
return prev[len2][k]
This preserves the same logic while cutting one dimension of memory, making it far more robust for large-scale inputs.
Ready to land your dream job?
Unlock your dream job with a 5-minute quiz for a personalized study roadmap!
Get My RoadmapWhat's the output of running the following function using input 56?
1KEYBOARD = {
2 '2': 'abc',
3 '3': 'def',
4 '4': 'ghi',
5 '5': 'jkl',
6 '6': 'mno',
7 '7': 'pqrs',
8 '8': 'tuv',
9 '9': 'wxyz',
10}
11
12def letter_combinations_of_phone_number(digits):
13 def dfs(path, res):
14 if len(path) == len(digits):
15 res.append(''.join(path))
16 return
17
18 next_number = digits[len(path)]
19 for letter in KEYBOARD[next_number]:
20 path.append(letter)
21 dfs(path, res)
22 path.pop()
23
24 res = []
25 dfs([], res)
26 return res
271private static final Map<Character, char[]> KEYBOARD = Map.of(
2 '2', "abc".toCharArray(),
3 '3', "def".toCharArray(),
4 '4', "ghi".toCharArray(),
5 '5', "jkl".toCharArray(),
6 '6', "mno".toCharArray(),
7 '7', "pqrs".toCharArray(),
8 '8', "tuv".toCharArray(),
9 '9', "wxyz".toCharArray()
10);
11
12public static List<String> letterCombinationsOfPhoneNumber(String digits) {
13 List<String> res = new ArrayList<>();
14 dfs(new StringBuilder(), res, digits.toCharArray());
15 return res;
16}
17
18private static void dfs(StringBuilder path, List<String> res, char[] digits) {
19 if (path.length() == digits.length) {
20 res.add(path.toString());
21 return;
22 }
23 char next_digit = digits[path.length()];
24 for (char letter : KEYBOARD.get(next_digit)) {
25 path.append(letter);
26 dfs(path, res, digits);
27 path.deleteCharAt(path.length() - 1);
28 }
29}
301const KEYBOARD = {
2 '2': 'abc',
3 '3': 'def',
4 '4': 'ghi',
5 '5': 'jkl',
6 '6': 'mno',
7 '7': 'pqrs',
8 '8': 'tuv',
9 '9': 'wxyz',
10}
11
12function letter_combinations_of_phone_number(digits) {
13 let res = [];
14 dfs(digits, [], res);
15 return res;
16}
17
18function dfs(digits, path, res) {
19 if (path.length === digits.length) {
20 res.push(path.join(''));
21 return;
22 }
23 let next_number = digits.charAt(path.length);
24 for (let letter of KEYBOARD[next_number]) {
25 path.push(letter);
26 dfs(digits, path, res);
27 path.pop();
28 }
29}
30Recommended Readings
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
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Recursion If you prefer videos here's a video that explains recursion in a fun and easy way Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask him
Want a Structured Path to Master System Design Too? Don’t Miss This!