1420. Build Array Where You Can Find The Maximum Exactly K Comparisons
Problem Description
You need to build an array of exactly n
positive integers where each element is between 1 and m
(inclusive). The goal is to construct arrays that will have a specific "search cost" of exactly k
when the following algorithm is applied:
The algorithm tracks a maximum value (initially -1) and counts how many times this maximum gets updated as it scans through the array from left to right. Each time an element is encountered that is strictly greater than the current maximum, the maximum is updated and a counter is incremented. The final counter value is the "search cost".
For example, if the array is [1, 3, 2, 5, 4]
:
- Start with
maximum = -1
,search_cost = 0
- Element 1: Since
1 > -1
, updatemaximum = 1
, incrementsearch_cost = 1
- Element 3: Since
3 > 1
, updatemaximum = 3
, incrementsearch_cost = 2
- Element 2: Since
2 < 3
, no update - Element 5: Since
5 > 3
, updatemaximum = 5
, incrementsearch_cost = 3
- Element 4: Since
4 < 5
, no update - Final
search_cost = 3
Your task is to count how many different arrays of length n
with elements in range [1, m]
will produce exactly k
as the search cost. Since the answer can be very large, return it modulo 10^9 + 7
.
The constraints are:
- The array must have exactly
n
integers - Each element must be between 1 and
m
(inclusive) - The search cost after applying the algorithm must equal exactly
k
Intuition
The key insight is that the search cost represents the number of times we encounter a new maximum while scanning the array. This happens exactly when we see an element that is strictly greater than all previous elements.
Let's think about what information we need to track. At any position in the array, we need to know:
- How many positions we've filled so far
- How many times we've updated the maximum (our current search cost)
- What the current maximum value is
This naturally leads to a dynamic programming approach with state dp[i][c][j]
representing the number of ways to build the first i
elements of the array such that:
- We've had exactly
c
updates to the maximum - The current maximum value is
j
For the base case, when we have just one element (i = 1
), we must have exactly one update (c = 1
), and the maximum can be any value from 1 to m
. So dp[1][1][j] = 1
for all valid j
.
Now, when we add a new element at position i
, we have two choices:
-
The new element doesn't update the maximum: If our current maximum is
j
, we can place any value from 1 toj
at positioni
. This gives usj
choices, and the search cost remains the same. So we adddp[i-1][c][j] * j
todp[i][c][j]
. -
The new element becomes the new maximum: If we want the new element to be
j
and update the maximum, the previous maximum must have been some valuej0 < j
. This increases the search cost by 1. So we sum updp[i-1][c-1][j0]
for allj0 < j
and add it todp[i][c][j]
.
Finally, the answer is the sum of dp[n][k][j]
for all possible maximum values j
from 1 to m
, since we want exactly n
elements with exactly k
updates to the maximum, regardless of what the final maximum value is.
The modulo operation is applied throughout to prevent integer overflow since the counts can become very large.
Learn more about Dynamic Programming and Prefix Sum patterns.
Solution Approach
The solution uses a 3D dynamic programming approach to count the number of valid arrays.
State Definition:
dp[i][c][j]
= number of ways to build the firsti
elements where we have exactlyc
updates to the maximum and the current maximum isj
Initialization:
dp = [[[0] * (m + 1) for _ in range(k + 1)] for _ in range(n + 1)]
We create a 3D DP table with dimensions (n+1) × (k+1) × (m+1)
initialized to 0.
Base Case:
for i in range(1, m + 1):
dp[1][1][i] = 1
When we have only one element, it must be the maximum (causing 1 update). Any value from 1 to m
can be that single element.
Transition:
For each position i
from 2 to n
, for each possible search cost c
from 1 to min(k, i)
(we can't have more updates than elements), and for each possible maximum value j
from 1 to m
:
-
Case 1 - No update to maximum:
dp[i][c][j] = dp[i - 1][c][j] * j
If the maximum remains
j
, we can place any value from 1 toj
at positioni
. This gives usj
choices, multiplied by the number of ways to reach state(i-1, c, j)
. -
Case 2 - Update the maximum:
for j0 in range(1, j): dp[i][c][j] += dp[i - 1][c - 1][j0]
If position
i
has valuej
and updates the maximum, the previous maximum must have been somej0 < j
. We sum all possibilities where the previous state was(i-1, c-1, j0)
.
Modulo Operation:
dp[i][c][j] %= mod
Applied after each calculation to prevent overflow.
Final Answer:
ans = 0
for i in range(1, m + 1):
ans += dp[n][k][i]
ans %= mod
Sum up all ways to build n
elements with exactly k
updates, regardless of the final maximum value.
Edge Case:
if k == 0: return 0
If k = 0
, no updates to the maximum are allowed, which is impossible since we start with maximum = -1 and must have at least one positive integer.
Time Complexity: O(n × k × m²)
- We iterate through all states and for each state with maximum j
, we check all previous maximums j0 < j
.
Space Complexity: O(n × k × m)
for the DP table.
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through a small example with n = 3
, m = 3
, k = 2
. We want to count arrays of length 3 with values in [1,3] that have exactly 2 maximum updates.
Step 1: Initialize DP table
- Create
dp[4][3][4]
(dimensions are n+1, k+1, m+1) - Set base case:
dp[1][1][1] = 1
,dp[1][1][2] = 1
,dp[1][1][3] = 1
- This means: with 1 element, we have 1 update, and that element can be 1, 2, or 3
Step 2: Fill position i = 2
For c = 1
(only 1 update so far):
dp[2][1][1]
: max is 1, no update →dp[1][1][1] * 1 = 1 * 1 = 1
- Array pattern: [1, 1]
dp[2][1][2]
: max is 2, no update →dp[1][1][2] * 2 = 1 * 2 = 2
- Array patterns: [2, 1], [2, 2]
dp[2][1][3]
: max is 3, no update →dp[1][1][3] * 3 = 1 * 3 = 3
- Array patterns: [3, 1], [3, 2], [3, 3]
For c = 2
(2 updates):
dp[2][2][1]
: Can't happen (max 1 can't be an update from previous)dp[2][2][2]
: Update from max=1 →dp[1][1][1] = 1
- Array pattern: [1, 2]
dp[2][2][3]
: Update from max=1 or max=2 →dp[1][1][1] + dp[1][1][2] = 1 + 1 = 2
- Array patterns: [1, 3], [2, 3]
Step 3: Fill position i = 3
For c = 2
(exactly 2 updates - what we want):
dp[3][2][1]
: max is 1, no update →dp[2][2][1] * 1 = 0 * 1 = 0
dp[3][2][2]
:- No update:
dp[2][2][2] * 2 = 1 * 2 = 2
- Update from max=1:
dp[2][1][1] = 1
- Total:
2 + 1 = 3
- Array patterns: [1, 2, 1], [1, 2, 2], [1, 1, 2]
- No update:
dp[3][2][3]
:- No update:
dp[2][2][3] * 3 = 2 * 3 = 6
- Update from max=1:
dp[2][1][1] = 1
- Update from max=2:
dp[2][1][2] = 2
- Total:
6 + 1 + 2 = 9
- Array patterns include: [1, 3, 1], [1, 3, 2], [1, 3, 3], [2, 3, 1], [2, 3, 2], [2, 3, 3], [1, 1, 3], [2, 1, 3], [2, 2, 3]
- No update:
Step 4: Calculate final answer
ans = dp[3][2][1] + dp[3][2][2] + dp[3][2][3] = 0 + 3 + 9 = 12
Therefore, there are 12 different arrays of length 3 with values in [1,3] that have exactly 2 maximum updates.
To verify, here are some example arrays with search cost 2:
- [1, 2, 1]: Updates at positions 1 (max=1) and 2 (max=2)
- [1, 3, 2]: Updates at positions 1 (max=1) and 2 (max=3)
- [2, 3, 1]: Updates at positions 1 (max=2) and 2 (max=3)
Solution Implementation
1class Solution:
2 def numOfArrays(self, n: int, m: int, k: int) -> int:
3 # Edge case: if search cost is 0, no valid arrays exist
4 if k == 0:
5 return 0
6
7 # dp[length][cost][max_val] = number of arrays of given length
8 # with given search cost and maximum value
9 dp = [[[0] * (m + 1) for _ in range(k + 1)] for _ in range(n + 1)]
10 MOD = 10**9 + 7
11
12 # Base case: arrays of length 1 have search cost 1
13 # and their maximum value is the single element itself
14 for max_value in range(1, m + 1):
15 dp[1][1][max_value] = 1
16
17 # Build up the dp table for arrays of increasing length
18 for length in range(2, n + 1):
19 # Search cost cannot exceed the array length
20 for search_cost in range(1, min(k + 1, length + 1)):
21 for max_value in range(1, m + 1):
22 # Case 1: append element <= current max (doesn't increase search cost)
23 # We can append any value from 1 to max_value
24 dp[length][search_cost][max_value] = (
25 dp[length - 1][search_cost][max_value] * max_value
26 ) % MOD
27
28 # Case 2: append element > previous max (increases search cost by 1)
29 # Sum over all possible previous maximum values less than current
30 for prev_max in range(1, max_value):
31 dp[length][search_cost][max_value] = (
32 dp[length][search_cost][max_value] +
33 dp[length - 1][search_cost - 1][prev_max]
34 ) % MOD
35
36 # Sum up all valid arrays of length n with search cost k
37 # across all possible maximum values
38 result = 0
39 for max_value in range(1, m + 1):
40 result = (result + dp[n][k][max_value]) % MOD
41
42 return result
43
1class Solution {
2 private static final int MOD = (int) 1e9 + 7;
3
4 public int numOfArrays(int n, int m, int k) {
5 // Edge case: if search cost is 0, no valid arrays exist
6 if (k == 0) {
7 return 0;
8 }
9
10 // Dynamic programming table
11 // dp[length][searchCost][maxValue] = number of arrays of given length
12 // with given search cost and maximum value
13 long[][][] dp = new long[n + 1][k + 1][m + 1];
14
15 // Base case: arrays of length 1 have search cost 1
16 // and their maximum value is the single element itself
17 for (int maxValue = 1; maxValue <= m; maxValue++) {
18 dp[1][1][maxValue] = 1;
19 }
20
21 // Fill the DP table for arrays of increasing length
22 for (int length = 2; length <= n; length++) {
23 // Search cost cannot exceed the array length
24 for (int searchCost = 1; searchCost <= Math.min(length, k); searchCost++) {
25 // Try all possible maximum values
26 for (int maxValue = 1; maxValue <= m; maxValue++) {
27 // Case 1: The new element doesn't increase the maximum
28 // We can append any value from 1 to maxValue
29 dp[length][searchCost][maxValue] =
30 (dp[length - 1][searchCost][maxValue] * maxValue) % MOD;
31
32 // Case 2: The new element becomes the new maximum
33 // Previous maximum must be less than current maxValue
34 for (int prevMaxValue = 1; prevMaxValue < maxValue; prevMaxValue++) {
35 dp[length][searchCost][maxValue] =
36 (dp[length][searchCost][maxValue] +
37 dp[length - 1][searchCost - 1][prevMaxValue]) % MOD;
38 }
39 }
40 }
41 }
42
43 // Sum up all valid arrays of length n with search cost k
44 // regardless of their maximum value
45 long totalCount = 0;
46 for (int maxValue = 1; maxValue <= m; maxValue++) {
47 totalCount = (totalCount + dp[n][k][maxValue]) % MOD;
48 }
49
50 return (int) totalCount;
51 }
52}
53
1class Solution {
2public:
3 int numOfArrays(int n, int m, int k) {
4 // Edge case: if search cost is 0, no valid arrays exist
5 if (k == 0) return 0;
6
7 const int MOD = 1e9 + 7;
8 using ll = long long;
9
10 // dp[length][searchCost][maxValue] = number of arrays of given length
11 // with exactly searchCost search operations and maximum value of maxValue
12 vector<vector<vector<ll>>> dp(n + 1, vector<vector<ll>>(k + 1, vector<ll>(m + 1, 0)));
13
14 // Base case: arrays of length 1 with search cost 1
15 // Any value from 1 to m can be the maximum (and only) element
16 for (int maxVal = 1; maxVal <= m; ++maxVal) {
17 dp[1][1][maxVal] = 1;
18 }
19
20 // Fill the DP table for arrays of length 2 to n
21 for (int length = 2; length <= n; ++length) {
22 // Search cost can be at most min(length, k)
23 for (int searchCost = 1; searchCost <= min(length, k); ++searchCost) {
24 // Try each possible maximum value
25 for (int maxVal = 1; maxVal <= m; ++maxVal) {
26 // Case 1: The new element doesn't increase the maximum
27 // We can add any element from 1 to maxVal without changing search cost
28 dp[length][searchCost][maxVal] = (dp[length - 1][searchCost][maxVal] * maxVal) % MOD;
29
30 // Case 2: The new element becomes the new maximum
31 // Previous maximum must be less than current maxVal
32 // This increases search cost by 1
33 for (int prevMax = 1; prevMax < maxVal; ++prevMax) {
34 dp[length][searchCost][maxVal] = (dp[length][searchCost][maxVal] +
35 dp[length - 1][searchCost - 1][prevMax]) % MOD;
36 }
37 }
38 }
39 }
40
41 // Sum up all valid arrays of length n with exactly k search operations
42 // They can have any maximum value from 1 to m
43 ll totalCount = 0;
44 for (int maxVal = 1; maxVal <= m; ++maxVal) {
45 totalCount = (totalCount + dp[n][k][maxVal]) % MOD;
46 }
47
48 return static_cast<int>(totalCount);
49 }
50};
51
1function numOfArrays(n: number, m: number, k: number): number {
2 // Edge case: if search cost is 0, no valid arrays exist
3 if (k === 0) return 0;
4
5 const MOD = 1e9 + 7;
6
7 // dp[length][searchCost][maxValue] = number of arrays of given length
8 // with exactly searchCost search operations and maximum value of maxValue
9 const dp: number[][][] = Array(n + 1).fill(null).map(() =>
10 Array(k + 1).fill(null).map(() =>
11 Array(m + 1).fill(0)
12 )
13 );
14
15 // Base case: arrays of length 1 with search cost 1
16 // Any value from 1 to m can be the maximum (and only) element
17 for (let maxVal = 1; maxVal <= m; maxVal++) {
18 dp[1][1][maxVal] = 1;
19 }
20
21 // Fill the DP table for arrays of length 2 to n
22 for (let length = 2; length <= n; length++) {
23 // Search cost can be at most min(length, k)
24 for (let searchCost = 1; searchCost <= Math.min(length, k); searchCost++) {
25 // Try each possible maximum value
26 for (let maxVal = 1; maxVal <= m; maxVal++) {
27 // Case 1: The new element doesn't increase the maximum
28 // We can add any element from 1 to maxVal without changing search cost
29 dp[length][searchCost][maxVal] = (dp[length - 1][searchCost][maxVal] * maxVal) % MOD;
30
31 // Case 2: The new element becomes the new maximum
32 // Previous maximum must be less than current maxVal
33 // This increases search cost by 1
34 for (let prevMax = 1; prevMax < maxVal; prevMax++) {
35 dp[length][searchCost][maxVal] = (dp[length][searchCost][maxVal] +
36 dp[length - 1][searchCost - 1][prevMax]) % MOD;
37 }
38 }
39 }
40 }
41
42 // Sum up all valid arrays of length n with exactly k search operations
43 // They can have any maximum value from 1 to m
44 let totalCount = 0;
45 for (let maxVal = 1; maxVal <= m; maxVal++) {
46 totalCount = (totalCount + dp[n][k][maxVal]) % MOD;
47 }
48
49 return totalCount;
50}
51
Time and Space Complexity
Time Complexity: O(n * k * m^2)
The algorithm uses dynamic programming with three nested loops:
- The outermost loop iterates through array positions from
2
ton
:O(n)
iterations - The second loop iterates through the number of search costs from
1
tomin(k, i)
:O(k)
iterations in the worst case - The third loop iterates through maximum values from
1
tom
:O(m)
iterations - Inside the third loop, there's another loop that iterates from
1
toj-1
to accumulate values from previous states:O(m)
iterations in the worst case
Therefore, the total time complexity is O(n * k * m * m) = O(n * k * m^2)
.
Space Complexity: O(n * k * m)
The algorithm uses a 3D DP array dp[n+1][k+1][m+1]
where:
- The first dimension represents the array length:
n+1
elements - The second dimension represents the search cost:
k+1
elements - The third dimension represents the maximum value:
m+1
elements
The total space required is O((n+1) * (k+1) * (m+1)) = O(n * k * m)
.
Note that the space complexity could be optimized to O(k * m)
since each state dp[i][c][j]
only depends on dp[i-1][*][*]
, allowing us to use only two 2D arrays instead of a full 3D array.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Incorrect Base Case Initialization
A common mistake is initializing the base case incorrectly or incompletely. Some developers might write:
# Wrong: Only initializing dp[1][1][1] = 1 dp[1][1][1] = 1
This assumes only value 1 can be the first element, but actually any value from 1 to m can be the first element.
Solution: Initialize all possible maximum values for length 1:
for max_value in range(1, m + 1):
dp[1][1][max_value] = 1
2. Forgetting Modulo Operations
Since the result can be very large, forgetting to apply modulo at intermediate steps can cause integer overflow:
# Wrong: Only applying modulo at the end dp[length][search_cost][max_value] = ( dp[length - 1][search_cost][max_value] * max_value + sum_of_previous_states ) # Missing modulo here!
Solution: Apply modulo after every arithmetic operation:
dp[length][search_cost][max_value] = ( dp[length - 1][search_cost][max_value] * max_value ) % MOD # ... later additions also need modulo dp[length][search_cost][max_value] = ( dp[length][search_cost][max_value] + dp[length - 1][search_cost - 1][prev_max] ) % MOD
3. Off-by-One Errors in Loop Bounds
Getting the loop bounds wrong, especially for the search cost:
# Wrong: search_cost can go up to k+1
for search_cost in range(1, k + 2): # This will cause index out of bounds
Also, search cost cannot exceed the array length:
# Wrong: not considering that search_cost <= length
for search_cost in range(1, k + 1): # Should be min(k + 1, length + 1)
Solution: Use correct bounds that respect both the maximum search cost k and the array length:
for search_cost in range(1, min(k + 1, length + 1)):
4. Misunderstanding the State Transition Logic
A subtle error is miscounting when adding an element that doesn't update the maximum:
# Wrong: Adding 1 for each way instead of multiplying by max_value dp[length][search_cost][max_value] = dp[length - 1][search_cost][max_value] + max_value
When the maximum stays the same, we can choose any value from 1 to max_value for the new position, so we multiply, not add.
Solution: Multiply by the number of valid choices:
dp[length][search_cost][max_value] = ( dp[length - 1][search_cost][max_value] * max_value ) % MOD
5. Not Handling Edge Cases
Failing to check if k = 0 or if k > n:
# Missing edge case handling
def numOfArrays(self, n: int, m: int, k: int) -> int:
# Directly starting DP without checking invalid cases
dp = [[[0] * (m + 1) for _ in range(k + 1)] for _ in range(n + 1)]
Solution: Add edge case checks at the beginning:
if k == 0: return 0 # No valid arrays with 0 search cost if k > n: return 0 # Can't have more updates than elements
How many ways can you arrange the three letters A, B and C?
Recommended 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
Prefix Sum The prefix sum is an incredibly powerful and straightforward technique Its primary goal is to allow for constant time range sum queries on an array What is Prefix Sum The prefix sum of an array at index i is the sum of all numbers from index 0 to i By
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
Want a Structured Path to Master System Design Too? Don’t Miss This!