3250. Find the Count of Monotonic Pairs I
Problem Description
You are given an array nums
of positive integers with length n
.
The problem asks you to find pairs of non-negative integer arrays (arr1, arr2)
that satisfy specific conditions. These pairs are called monotonic pairs if:
- Both
arr1
andarr2
have the same lengthn
as the input array arr1
is monotonically non-decreasing, meaningarr1[0] <= arr1[1] <= ... <= arr1[n-1]
arr2
is monotonically non-increasing, meaningarr2[0] >= arr2[1] >= ... >= arr2[n-1]
- For every index
i
, the sumarr1[i] + arr2[i]
must equalnums[i]
Your task is to count how many such valid monotonic pairs exist. Since the answer can be very large, return the result modulo 10^9 + 7
.
For example, if nums = [2, 3, 2]
, you need to find all possible ways to split each element into two parts such that:
- The first parts form a non-decreasing sequence
- The second parts form a non-increasing sequence
- Each pair of parts sums to the corresponding element in
nums
The solution uses dynamic programming where f[i][j]
represents the number of valid monotonic pairs for the first i+1
elements where arr1[i] = j
. The key insight is that for each position, we need to ensure both monotonic properties are maintained while respecting the constraint that each pair sums to the corresponding value in nums
.
Intuition
To solve this problem, let's think about what we're actually doing: we're splitting each number nums[i]
into two parts - one goes into arr1[i]
and the other goes into arr2[i]
. Since arr1[i] + arr2[i] = nums[i]
, if we choose arr1[i] = j
, then arr2[i] = nums[i] - j
.
The key observation is that our choices are constrained by the monotonic properties:
- For
arr1
to be non-decreasing:arr1[i-1] <= arr1[i]
- For
arr2
to be non-increasing:arr2[i-1] >= arr2[i]
This means if we pick arr1[i] = j
, we need arr1[i-1] <= j
and arr2[i-1] >= nums[i] - j
.
Since arr2[i-1] = nums[i-1] - arr1[i-1]
, the second constraint becomes:
nums[i-1] - arr1[i-1] >= nums[i] - j
Rearranging: arr1[i-1] <= j + nums[i-1] - nums[i]
So for a valid choice of arr1[i] = j
, the previous value arr1[i-1]
must satisfy:
arr1[i-1] <= min(j, j + nums[i-1] - nums[i])
This naturally leads to a dynamic programming approach where we track how many ways we can build valid pairs up to position i
with arr1[i] = j
. For each position and value, we count all valid previous states that can lead to the current state.
The use of prefix sums in the solution is an optimization technique - instead of summing up all valid previous states for each j
separately, we precompute cumulative sums to quickly get the count of all states where arr1[i-1] <= k
for any k
.
Learn more about Math, Dynamic Programming, Combinatorics and Prefix Sum patterns.
Solution Approach
We implement a dynamic programming solution with prefix sum optimization. Let's define f[i][j]
as the number of valid monotonic pairs for the subarray [0, ..., i]
where arr1[i] = j
.
Base Case:
For the first element (i = 0
), we can choose any value from 0
to nums[0]
for arr1[0]
. Each choice is valid since there are no previous elements to constrain us. Therefore:
f[0][j] = 1
for all0 <= j <= nums[0]
Transition:
For i > 0
, to calculate f[i][j]
, we need to count all valid previous states f[i-1][j']
where:
j' <= j
(to ensurearr1
is non-decreasing)j' <= j + nums[i-1] - nums[i]
(to ensurearr2
is non-increasing)
Combining these constraints: j' <= min(j, j + nums[i-1] - nums[i])
Therefore: f[i][j] = sum(f[i-1][j'])
for all valid j'
Implementation Details:
-
Initialize the DP table: Create a 2D array
f
of sizen × (m+1)
wherem = max(nums)
to cover all possible values ofarr1[i]
. -
Fill base case: Set
f[0][j] = 1
forj
from0
tonums[0]
. -
Build the DP table: For each position
i
from1
ton-1
:- First, compute prefix sums of the previous row:
s = accumulate(f[i-1])
- For each possible value
j
from0
tonums[i]
:- Calculate the upper bound:
k = min(j, j + nums[i-1] - nums[i])
- If
k >= 0
, setf[i][j] = s[k] % mod
- Calculate the upper bound:
- First, compute prefix sums of the previous row:
-
Compute the answer: Sum all valid states for the last position:
sum(f[n-1][0:nums[n-1]+1]) % mod
The prefix sum optimization reduces the time complexity from O(n × m²)
to O(n × m)
by avoiding repeated summations. Instead of summing f[i-1][0]
through f[i-1][k]
for each j
, we precompute these cumulative sums once per row.
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 the solution with nums = [2, 3, 2]
.
We need to find all ways to split each number into two arrays such that:
arr1
is non-decreasingarr2
is non-increasingarr1[i] + arr2[i] = nums[i]
for alli
Step 1: Initialize DP table
Create f[i][j]
where f[i][j]
= number of ways to build valid pairs up to index i
with arr1[i] = j
.
Step 2: Base case (i = 0, nums[0] = 2)
For the first element, we can choose any value from 0 to 2 for arr1[0]
:
- If
arr1[0] = 0
, thenarr2[0] = 2
- If
arr1[0] = 1
, thenarr2[0] = 1
- If
arr1[0] = 2
, thenarr2[0] = 0
All choices are valid, so f[0][0] = f[0][1] = f[0][2] = 1
.
Step 3: Process i = 1 (nums[1] = 3)
For each possible arr1[1] = j
(where 0 ≤ j ≤ 3
), we check which previous states are valid.
The constraint is: arr1[0] ≤ min(j, j + nums[0] - nums[1]) = min(j, j + 2 - 3) = min(j, j - 1)
j = 0
: Needarr1[0] ≤ min(0, -1) = -1
. No valid previous states.f[1][0] = 0
j = 1
: Needarr1[0] ≤ min(1, 0) = 0
. Onlyarr1[0] = 0
works.f[1][1] = f[0][0] = 1
j = 2
: Needarr1[0] ≤ min(2, 1) = 1
. Botharr1[0] = 0
andarr1[0] = 1
work.f[1][2] = f[0][0] + f[0][1] = 2
j = 3
: Needarr1[0] ≤ min(3, 2) = 2
. All previous states work.f[1][3] = f[0][0] + f[0][1] + f[0][2] = 3
Step 4: Process i = 2 (nums[2] = 2)
The constraint is: arr1[1] ≤ min(j, j + nums[1] - nums[2]) = min(j, j + 3 - 2) = min(j, j + 1) = j
j = 0
: Needarr1[1] ≤ 0
. No valid states (f[1][0] = 0).f[2][0] = 0
j = 1
: Needarr1[1] ≤ 1
. Onlyarr1[1] = 1
works.f[2][1] = f[1][1] = 1
j = 2
: Needarr1[1] ≤ 2
. Botharr1[1] = 1
andarr1[1] = 2
work.f[2][2] = f[1][1] + f[1][2] = 1 + 2 = 3
Step 5: Final answer
Sum all valid final states: f[2][0] + f[2][1] + f[2][2] = 0 + 1 + 3 = 4
The 4 valid monotonic pairs are:
arr1 = [0,1,1]
,arr2 = [2,2,1]
arr1 = [0,2,2]
,arr2 = [2,1,0]
arr1 = [1,2,2]
,arr2 = [1,1,0]
arr1 = [2,3,2]
,arr2 = [0,0,0]
(Note: This is actually invalid as arr1 is not non-decreasing)
Let me recalculate step 4:
j = 2
: Needarr1[1] ≤ 2
. We havef[1][1] = 1
andf[1][2] = 2
, sof[2][2] = 3
The final answer is 4 valid monotonic pairs.
Solution Implementation
1class Solution:
2 def countOfPairs(self, nums: List[int]) -> int:
3 MOD = 10**9 + 7
4 n = len(nums)
5 max_value = max(nums)
6
7 # dp[i][j] represents the number of valid ways to form arr1[0...i]
8 # where arr1[i] = j
9 dp = [[0] * (max_value + 1) for _ in range(n)]
10
11 # Base case: for the first element, arr1[0] can be any value from 0 to nums[0]
12 # This means arr2[0] = nums[0] - arr1[0], which must be non-negative
13 for value in range(nums[0] + 1):
14 dp[0][value] = 1
15
16 # Fill the DP table for remaining positions
17 for i in range(1, n):
18 # Calculate prefix sums for the previous row to optimize range sum queries
19 prefix_sum = list(accumulate(dp[i - 1]))
20
21 # For current position i, try all possible values for arr1[i]
22 for j in range(nums[i] + 1):
23 # Calculate the maximum valid value for arr1[i-1]
24 # Constraint: arr2[i-1] <= arr2[i]
25 # arr2[i-1] = nums[i-1] - arr1[i-1]
26 # arr2[i] = nums[i] - arr1[i] = nums[i] - j
27 # So: nums[i-1] - arr1[i-1] <= nums[i] - j
28 # Therefore: arr1[i-1] >= j - (nums[i] - nums[i-1])
29 # Also: arr1[i-1] <= j (monotonic non-decreasing constraint)
30 max_prev_value = min(j, j + nums[i - 1] - nums[i])
31
32 # Sum all valid transitions from previous state
33 if max_prev_value >= 0:
34 dp[i][j] = prefix_sum[max_prev_value] % MOD
35
36 # Sum all valid ways for the last position where arr1[n-1] can be 0 to nums[n-1]
37 return sum(dp[-1][:nums[-1] + 1]) % MOD
38
1class Solution {
2 public int countOfPairs(int[] nums) {
3 final int MOD = (int) 1e9 + 7;
4 int n = nums.length;
5 int maxValue = Arrays.stream(nums).max().getAsInt();
6
7 // dp[i][j] represents the number of valid ways to form arrays up to index i
8 // where the first array (non-decreasing) has value j at position i
9 int[][] dp = new int[n][maxValue + 1];
10
11 // Base case: initialize first element
12 // For the first position, any value from 0 to nums[0] is valid
13 for (int value = 0; value <= nums[0]; value++) {
14 dp[0][value] = 1;
15 }
16
17 // prefixSum array for optimization
18 // Used to calculate range sums efficiently
19 int[] prefixSum = new int[maxValue + 1];
20
21 // Process each position starting from the second element
22 for (int i = 1; i < n; i++) {
23 // Build prefix sum array from previous row
24 prefixSum[0] = dp[i - 1][0];
25 for (int j = 1; j <= maxValue; j++) {
26 prefixSum[j] = (prefixSum[j - 1] + dp[i - 1][j]) % MOD;
27 }
28
29 // Calculate valid combinations for current position
30 for (int currentValue = 0; currentValue <= nums[i]; currentValue++) {
31 // Calculate the maximum valid value at previous position
32 // Based on the constraint that the second array (non-increasing) must be valid
33 int maxPrevValue = Math.min(currentValue, currentValue + nums[i - 1] - nums[i]);
34
35 // If there's a valid range, sum all possibilities
36 if (maxPrevValue >= 0) {
37 dp[i][currentValue] = prefixSum[maxPrevValue];
38 }
39 }
40 }
41
42 // Calculate the final answer by summing all valid endings
43 int result = 0;
44 for (int value = 0; value <= nums[n - 1]; value++) {
45 result = (result + dp[n - 1][value]) % MOD;
46 }
47
48 return result;
49 }
50}
51
1class Solution {
2public:
3 int countOfPairs(vector<int>& nums) {
4 const int MOD = 1e9 + 7;
5 int n = nums.size();
6 int maxValue = *max_element(nums.begin(), nums.end());
7
8 // dp[i][j] represents the number of valid ways to form arrays up to index i
9 // where the non-decreasing array has value j at position i
10 vector<vector<int>> dp(n, vector<int>(maxValue + 1, 0));
11
12 // Base case: initialize first element
13 // The first element can take any value from 0 to nums[0]
14 for (int value = 0; value <= nums[0]; ++value) {
15 dp[0][value] = 1;
16 }
17
18 // prefixSum array for optimization
19 // Used to calculate range sums efficiently
20 vector<int> prefixSum(maxValue + 1, 0);
21
22 // Process each position starting from the second element
23 for (int i = 1; i < n; ++i) {
24 // Build prefix sum array from previous row
25 prefixSum[0] = dp[i - 1][0];
26 for (int j = 1; j <= maxValue; ++j) {
27 prefixSum[j] = (prefixSum[j - 1] + dp[i - 1][j]) % MOD;
28 }
29
30 // Calculate dp values for current position
31 for (int currentValue = 0; currentValue <= nums[i]; ++currentValue) {
32 // Calculate the maximum valid value at previous position
33 // Based on the constraint that both arrays must satisfy their properties
34 int maxPrevValue = min(currentValue, currentValue + nums[i - 1] - nums[i]);
35
36 // Only update if we have a valid previous value
37 if (maxPrevValue >= 0) {
38 dp[i][currentValue] = prefixSum[maxPrevValue];
39 }
40 }
41 }
42
43 // Calculate the total count by summing all valid endings
44 int totalCount = 0;
45 for (int value = 0; value <= nums[n - 1]; ++value) {
46 totalCount = (totalCount + dp[n - 1][value]) % MOD;
47 }
48
49 return totalCount;
50 }
51};
52
1/**
2 * Counts the number of valid pairs based on given constraints
3 * @param nums - Input array of numbers
4 * @returns The count of valid pairs modulo 10^9 + 7
5 */
6function countOfPairs(nums: number[]): number {
7 const MOD: number = 1e9 + 7;
8 const arrayLength: number = nums.length;
9 const maxValue: number = Math.max(...nums);
10
11 // dp[i][j] represents the number of ways to form valid sequences
12 // up to index i with value j at position i
13 const dp: number[][] = Array.from(
14 { length: arrayLength },
15 () => Array(maxValue + 1).fill(0)
16 );
17
18 // Initialize base case: all values from 0 to nums[0] are valid for first position
19 for (let value = 0; value <= nums[0]; value++) {
20 dp[0][value] = 1;
21 }
22
23 // prefixSum array to optimize cumulative sum calculations
24 const prefixSum: number[] = Array(maxValue + 1).fill(0);
25
26 // Process each position starting from index 1
27 for (let i = 1; i < arrayLength; i++) {
28 // Build prefix sum array from previous row
29 prefixSum[0] = dp[i - 1][0];
30 for (let j = 1; j <= maxValue; j++) {
31 prefixSum[j] = (prefixSum[j - 1] + dp[i - 1][j]) % MOD;
32 }
33
34 // Calculate valid values for current position
35 for (let currentValue = 0; currentValue <= nums[i]; currentValue++) {
36 // Calculate the maximum valid value from previous position
37 // based on the constraint between consecutive elements
38 const maxPrevValue: number = Math.min(
39 currentValue,
40 currentValue + nums[i - 1] - nums[i]
41 );
42
43 // If valid previous values exist, update dp table
44 if (maxPrevValue >= 0) {
45 dp[i][currentValue] = prefixSum[maxPrevValue];
46 }
47 }
48 }
49
50 // Sum up all valid sequences ending at the last position
51 let result: number = 0;
52 for (let finalValue = 0; finalValue <= nums[arrayLength - 1]; finalValue++) {
53 result = (result + dp[arrayLength - 1][finalValue]) % MOD;
54 }
55
56 return result;
57}
58
Time and Space Complexity
The time complexity is O(n × m)
, where n
is the length of the array nums
and m
is the maximum value in the array nums
.
Breaking down the time complexity:
- Finding the maximum value:
O(n)
- Initializing the 2D array
f
:O(n × m)
- First loop for
f[0][j]
:O(m)
- Main nested loops:
- Outer loop runs
n-1
times - For each iteration,
accumulate(f[i-1])
takesO(m)
time - Inner loop runs up to
nums[i] + 1
times, which is at mostO(m)
- Total for main loops:
O(n × m)
- Outer loop runs
- Final sum operation:
O(m)
Overall time complexity: O(n × m)
The space complexity is O(n × m)
.
Space complexity analysis:
- The 2D array
f
has dimensionsn × (m + 1)
, requiringO(n × m)
space - The accumulate list
s
requiresO(m)
additional space - Other variables use
O(1)
space
Overall space complexity: O(n × m)
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Incorrect Constraint Derivation for arr2's Monotonic Property
One of the most common mistakes is incorrectly deriving the constraint for maintaining arr2's non-increasing property. Many developers mistakenly write:
Incorrect reasoning:
- Since arr2 must be non-increasing:
arr2[i-1] >= arr2[i]
- This gives us:
nums[i-1] - arr1[i-1] >= nums[i] - j
- Leading to:
arr1[i-1] <= j + nums[i-1] - nums[i]
✗
Correct reasoning:
- The inequality should be flipped because we're looking at what values of
arr1[i-1]
are valid - From
arr2[i-1] >= arr2[i]
, we get:nums[i-1] - arr1[i-1] >= nums[i] - j
- Rearranging:
arr1[i-1] <= j + nums[i-1] - nums[i]
✓
2. Off-by-One Errors in Range Boundaries
Common mistake:
# Incorrect: using nums[i] as the upper bound instead of nums[i] + 1
for j in range(nums[i]): # Missing the value nums[i] itself
dp[i][j] = ...
Solution:
# Correct: include nums[i] as a valid value for arr1[i]
for j in range(nums[i] + 1): # Range should be [0, nums[i]]
dp[i][j] = ...
3. Forgetting to Handle Negative Index in Prefix Sum
Common mistake:
max_prev_value = min(j, j + nums[i-1] - nums[i])
dp[i][j] = prefix_sum[max_prev_value] % MOD # Can crash if max_prev_value < 0
Solution:
max_prev_value = min(j, j + nums[i-1] - nums[i])
if max_prev_value >= 0: # Check for valid index
dp[i][j] = prefix_sum[max_prev_value] % MOD
else:
dp[i][j] = 0 # No valid transitions
4. Modulo Operation Applied Incorrectly
Common mistake:
# Applying modulo only at the final answer
return sum(dp[-1][:nums[-1] + 1]) % MOD # Can overflow before modulo
Solution:
# Apply modulo during accumulation to prevent overflow result = 0 for value in dp[-1][:nums[-1] + 1]: result = (result + value) % MOD return result
5. Memory Optimization Pitfall
While the 2D DP solution works, a common optimization attempt using only two 1D arrays can introduce bugs:
Problematic optimization:
prev = [0] * (max_value + 1) curr = [0] * (max_value + 1) # ... computation ... prev = curr # This creates a reference, not a copy!
Correct optimization:
prev = [0] * (max_value + 1) curr = [0] * (max_value + 1) # ... computation ... prev, curr = curr, prev # Swap references # OR prev = curr[:] # Create a copy
You are given an array of intervals where intervals[i] = [start_i, end_i]
represent the start and end of the ith
interval. You need to merge all overlapping intervals and return an array of the non-overlapping intervals that cover all the intervals in the input.
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!