Find the Count of Monotonic Pairs I
HardPrompt
You are given an array of positive integers nums of length n.
We call a pair of non-negative integer arrays (arr1, arr2) monotonic if:
- The lengths of both arrays are
n. arr1is monotonically non-decreasing, in other words,arr1[0] <= arr1[1] <= ... <= arr1[n - 1].arr2is monotonically non-increasing, in other words,arr2[0] >= arr2[1] >= ... >= arr2[n - 1].arr1[i] + arr2[i] == nums[i]for all0 <= i <= n - 1.
Return the count of monotonic pairs.
Since the answer may be very large, return it modulo 109 + 7.
Example 1:
Input: nums = [2,3,2]
Output: 4
Explanation:
The good pairs are:
([0, 1, 1], [2, 2, 1])([0, 1, 2], [2, 2, 0])([0, 2, 2], [2, 1, 0])([1, 2, 2], [1, 1, 0])
Example 2:
Input: nums = [5,5,5,5]
Output: 126
Constraints:
1 <= n == nums.length <= 20001 <= nums[i] <= 50
Approaches
3 approaches with complexity analysis and trade-offs.
This approach optimizes the naive DP solution by observing that the inner loop is calculating a sum over a range. Such summations can be computed efficiently using prefix sums. By pre-calculating the prefix sums for each row of the DP table, we can determine the value of any dp[i][j] in constant time, which significantly improves the overall time complexity.
Algorithm
- The DP state
dp[i][j]is the same as the naive approach. - The transition
dp[i][j] = sum(dp[i-1][k])is over a contiguous range ofk. - The upper bound for
kislimit = min(nums[i-1], j - max(0, nums[i] - nums[i-1])). - We can precompute prefix sums for each row
i-1of the DP table into an arrayps[i-1]. dp[i][j]can then be found in O(1) time usingps[i-1][limit].- This removes the innermost loop from the naive DP approach.
Walkthrough
The key insight is that the transition dp[i][j] = sum(dp[i-1][k]) is performed for k in the range [0, limit], where limit = min(nums[i-1], j - max(0, nums[i] - nums[i-1])). This is a prefix sum.
We can maintain a parallel 2D array, ps, where ps[i][x] stores the sum of dp[i][k] for k from 0 to x. After computing the i-1-th row of the dp table, we can compute the i-1-th row of the ps table in O(M) time. Then, when computing the i-th row of dp, each dp[i][j] can be found in O(1) by looking up the precomputed value in ps[i-1], effectively eliminating one loop from the previous approach.
Algorithm Steps:
- Create a DP table
dp[n][M+1]and a prefix sum tableps[n][M+1]. - Base Case (i=0): Fill
dp[0]andps[0]as before. - Transitions (i > 0): For
ifrom1ton-1:- For each
jfrom0tonums[i]:- Calculate the upper bound for
k:limit = min(nums[i-1], j - max(0, nums[i] - nums[i-1])). - If
limit >= 0, setdp[i][j] = ps[i-1][limit].
- Calculate the upper bound for
- Compute the prefix sums for the new
dp[i]row and store them inps[i].
- For each
- Final Result: The answer is
ps[n-1][nums[n-1]].
class Solution { public int countMonotonicPairs(int[] nums) { int n = nums.length; int maxVal = 0; for (int num : nums) { maxVal = Math.max(maxVal, num); } long[][] dp = new long[n][maxVal + 1]; long[][] ps = new long[n][maxVal + 1]; int MOD = 1_000_000_007; // Base case: i = 0 for (int j = 0; j <= nums[0]; j++) { dp[0][j] = 1; } ps[0][0] = dp[0][0]; for (int j = 1; j <= maxVal; j++) { ps[0][j] = (ps[0][j - 1] + dp[0][j]) % MOD; } // Fill DP table for i > 0 for (int i = 1; i < n; i++) { long diff = (long)nums[i] - nums[i-1]; for (int j = 0; j <= nums[i]; j++) { long k_upper_bound = j - Math.max(0, diff); long limit = Math.min(nums[i-1], k_upper_bound); if (limit >= 0) { dp[i][j] = ps[i-1][(int)limit]; } } ps[i][0] = dp[i][0]; for (int j = 1; j <= maxVal; j++) { ps[i][j] = (ps[i][j - 1] + dp[i][j]) % MOD; } } return (int) ps[n - 1][nums[n - 1]]; }}Complexity
Time
O(n * M). For each of the `n` states, we perform O(M) work to compute the DP values and their prefix sums.
Space
O(n * M), for the DP and prefix sum tables.
Trade-offs
Pros
Much faster than the naive DP approach, with a time complexity of O(n * M).
Handles the given constraints very efficiently.
Cons
Requires additional space for the prefix sum table, although the asymptotic space complexity remains the same.
This approach uses dynamic programming to solve the problem. We define a 2D DP table, dp[i][j], to store the number of valid monotonic arrays arr1 of length i+1 (from index 0 to i) where the last element arr1[i] is equal to j. We build this table iteratively for each index i from 0 to n-1 by checking all valid transitions from the previous state.
Algorithm
- Define a 2D DP array
dp[i][j]to store the number of validarr1prefixes of lengthi+1wherearr1[i] = j. - The state transition relies on the conditions derived from the problem:
arr1[i+1] >= max(arr1[i], arr1[i] + nums[i+1] - nums[i]). - Initialize the base case for
i=0:dp[0][j] = 1for0 <= j <= nums[0]. - Iterate from
i = 1ton-1:- For each possible value
jofarr1[i](from0tonums[i]):- Iterate through all possible values
kofarr1[i-1](from0tonums[i-1]):- If
kcan transition toj, adddp[i-1][k]todp[i][j].
- If
- Iterate through all possible values
- For each possible value
- The final answer is the sum of
dp[n-1][j]for all validj.
Walkthrough
First, we rephrase the problem's conditions in terms of arr1 only. The condition arr1[i] + arr2[i] == nums[i] and arr2 being non-increasing implies nums[i] - arr1[i] >= nums[i+1] - arr1[i+1], which simplifies to arr1[i+1] >= arr1[i] + nums[i+1] - nums[i]. Combined with arr1 being non-decreasing (arr1[i+1] >= arr1[i]), we get a single condition for the transition from arr1[i] to arr1[i+1]: arr1[i+1] >= max(arr1[i], arr1[i] + nums[i+1] - nums[i]).
We can define a DP state dp[i][j] as the number of ways to form a valid prefix arr1[0...i] such that arr1[i] = j. To compute dp[i][j], we sum up dp[i-1][k] for all values k that could have been arr1[i-1]. This involves a nested loop structure, leading to a cubic-like complexity.
Algorithm Steps:
- Let
Mbe the maximum value innums. Create a DP tabledp[n][M+1]. - Base Case (i=0): For
jfrom0tonums[0], setdp[0][j] = 1, as any of these values is a valid start. - Transitions (i > 0): For
ifrom1ton-1, and for eachjfrom0tonums[i]:- Iterate
kfrom0tonums[i-1]. - If
jis a valid successor tok(i.e.,j >= kandj >= k + nums[i] - nums[i-1]), adddp[i-1][k]todp[i][j], taking the result modulo10^9 + 7.
- Iterate
- Final Result: Sum up all values in
dp[n-1]up to indexnums[n-1]to get the total count.
class Solution { public int countMonotonicPairs(int[] nums) { int n = nums.length; int maxVal = 0; for (int num : nums) { maxVal = Math.max(maxVal, num); } long[][] dp = new long[n][maxVal + 1]; int MOD = 1_000_000_007; // Base case: i = 0 for (int j = 0; j <= nums[0]; j++) { dp[0][j] = 1; } // Fill DP table for i > 0 for (int i = 1; i < n; i++) { for (int j = 0; j <= nums[i]; j++) { long diff = (long)nums[i] - nums[i-1]; for (int k = 0; k <= nums[i-1]; k++) { if (j >= k && j >= k + diff) { dp[i][j] = (dp[i][j] + dp[i-1][k]) % MOD; } } } } // Calculate the final result long totalCount = 0; for (int j = 0; j <= nums[n - 1]; j++) { totalCount = (totalCount + dp[n - 1][j]) % MOD; } return (int) totalCount; }}Complexity
Time
O(n * M^2), where `n` is the length of `nums` and `M` is the maximum value in `nums`. We have three nested loops: over `i` (size `n`), `j` (size `M`), and `k` (size `M`).
Space
O(n * M), where `n` is the length of `nums` and `M` is the maximum value in `nums`. This is for the 2D DP table.
Trade-offs
Pros
It's a straightforward DP solution that correctly models the problem's state transitions.
More efficient than a pure recursive or backtracking approach.
Cons
The time complexity of O(n * M^2) can be too slow if M is large, although it passes for the given constraints.
The space complexity of O(n * M) can be substantial for large n.
This is the most efficient approach, building upon the prefix sum optimization to also reduce space complexity. We notice that to compute the DP values for the current index i, we only need the prefix sums from the previous index i-1. This allows us to discard the full 2D DP table and use only a few 1D arrays to store the states for the previous and current rows, reducing space complexity from O(n * M) to O(M).
Algorithm
- The logic is identical to the prefix sum optimization.
- Observe that computing
dp[i]only requiresps[i-1](the prefix sums ofdp[i-1]). - Instead of using 2D arrays
dp[n][M]andps[n][M], we use 1D arraysdpandpsof sizeM. - In each iteration
i,psholds the prefix sums from iterationi-1. We use it to compute anext_dparray. - After
next_dpis computed, it becomes the newdpfor the next iteration, and we updatepsbased on it.
Walkthrough
We can optimize the space usage of the previous approach. The calculation for row i only depends on row i-1. Therefore, we don't need to store all n rows. We can use two 1D arrays: one to store the DP values of the previous state (dp) and another for its prefix sums (ps). In each step of the iteration over i, we compute the DP values for the current state into a temporary array (next_dp) using the ps array of the previous state. Then, we replace dp with next_dp and update the ps array for the next iteration.
Algorithm Steps:
- Initialize 1D arrays
dp[M+1]andps[M+1]. - Base Case (i=0): Populate
dpfori=0and compute its prefix sums intops. - Transitions (i > 0): For
ifrom1ton-1:- Create a new array
next_dp[M+1]. - For each
jfrom0tonums[i], calculatenext_dp[j]using thepsarray (which holds data fori-1). - After the loop, set
dp = next_dp. - Recompute the
psarray from the newdparray.
- Create a new array
- Final Result: After the main loop finishes, the answer is the value in
ps[nums[n-1]].
class Solution { public int countMonotonicPairs(int[] nums) { int n = nums.length; int maxVal = 0; for (int num : nums) { maxVal = Math.max(maxVal, num); } int MOD = 1_000_000_007; long[] dp = new long[maxVal + 1]; long[] ps = new long[maxVal + 1]; // Base case: i = 0 for (int j = 0; j <= nums[0]; j++) { dp[j] = 1; } ps[0] = dp[0]; for (int j = 1; j <= maxVal; j++) { ps[j] = (ps[j - 1] + dp[j]) % MOD; } // Fill DP table for i > 0 for (int i = 1; i < n; i++) { long[] next_dp = new long[maxVal + 1]; long diff = (long)nums[i] - nums[i-1]; for (int j = 0; j <= nums[i]; j++) { long k_upper_bound = j - Math.max(0, diff); long limit = Math.min(nums[i-1], k_upper_bound); if (limit >= 0) { next_dp[j] = ps[(int)limit]; } } dp = next_dp; ps[0] = dp[0]; for (int j = 1; j <= maxVal; j++) { ps[j] = (ps[j - 1] + dp[j]) % MOD; } } return (int) ps[nums[n - 1]]; }}Complexity
Time
O(n * M). The time complexity remains the same as the previous approach, as the core computation is unchanged.
Space
O(M), where `M` is the maximum value in `nums`. We only need a few arrays of size `M+1` regardless of `n`.
Trade-offs
Pros
Optimal time complexity for this DP formulation.
Optimal space complexity, making it very memory-efficient.
Cons
The implementation can be slightly more complex due to managing and updating the arrays representing the current and previous states.
Solutions
Solution
class Solution { public int countOfPairs ( int [] nums ) { final int mod = ( int ) 1 e9 + 7 ; int n = nums . length ; int m = Arrays . stream ( nums ). max (). getAsInt (); int [][] f = new int [ n ][ m + 1 ]; for ( int j = 0 ; j <= nums [ 0 ]; ++ j ) { f [ 0 ][ j ] = 1 ; } int [] g = new int [ m + 1 ]; for ( int i = 1 ; i < n ; ++ i ) { g [ 0 ] = f [ i - 1 ][ 0 ]; for ( int j = 1 ; j <= m ; ++ j ) { g [ j ] = ( g [ j - 1 ] + f [ i - 1 ][ j ]) % mod ; } for ( int j = 0 ; j <= nums [ i ]; ++ j ) { int k = Math . min ( j , j + nums [ i - 1 ] - nums [ i ]); if ( k >= 0 ) { f [ i ][ j ] = g [ k ]; } } } int ans = 0 ; for ( int j = 0 ; j <= nums [ n - 1 ]; ++ j ) { ans = ( ans + f [ n - 1 ][ j ]) % mod ; } return ans ; } }class Solution { public: int countOfPairs ( vector < int >& nums ) { const int mod = 1e9 + 7 ; int n = nums . size (); int m = * max_element ( nums . begin (), nums . end ()); vector < vector < int >> f ( n , vector < int > ( m + 1 )); for ( int j = 0 ; j <= nums [ 0 ]; ++ j ) { f [ 0 ][ j ] = 1 ; } vector < int > g ( m + 1 ); for ( int i = 1 ; i < n ; ++ i ) { g [ 0 ] = f [ i - 1 ][ 0 ]; for ( int j = 1 ; j <= m ; ++ j ) { g [ j ] = ( g [ j - 1 ] + f [ i - 1 ][ j ]) % mod ; } for ( int j = 0 ; j <= nums [ i ]; ++ j ) { int k = min ( j , j + nums [ i - 1 ] - nums [ i ]); if ( k >= 0 ) { f [ i ][ j ] = g [ k ]; } } } int ans = 0 ; for ( int j = 0 ; j <= nums [ n - 1 ]; ++ j ) { ans = ( ans + f [ n - 1 ][ j ]) % mod ; } return ans ; } };class Solution : def countOfPairs ( self , nums : List [ int ]) -> int : mod = 10 ** 9 + 7 n , m = len ( nums ), max ( nums ) f = [[ 0 ] * ( m + 1 ) for _ in range ( n )] for j in range ( nums [ 0 ] + 1 ): f [ 0 ][ j ] = 1 for i in range ( 1 , n ): s = list ( accumulate ( f [ i - 1 ])) for j in range ( nums [ i ] + 1 ): k = min ( j , j + nums [ i - 1 ] - nums [ i ]) if k >= 0 : f [ i ][ j ] = s [ k ] % mod return sum ( f [ - 1 ][: nums [ - 1 ] + 1 ]) % modVideo walkthrough
Newsletter
One sharp idea, every week
System design and interview prep — short enough to finish.
No spam. Unsubscribe anytime.
Practice
Same difficulty — related problems to reinforce the pattern.