3251. Find the Count of Monotonic Pairs II
Problem Description
You have an array nums
of positive integers with length n
. Your task is to find how many ways you can split this array into two arrays (arr1
and arr2
) that satisfy specific monotonic properties.
A pair of non-negative integer arrays (arr1, arr2)
is considered monotonic if:
- Both arrays have the same length
n
as the original array arr1
is non-decreasing:arr1[0] <= arr1[1] <= ... <= arr1[n-1]
arr2
is non-increasing:arr2[0] >= arr2[1] >= ... >= arr2[n-1]
- At each position
i
, the sum equals the original value:arr1[i] + arr2[i] = nums[i]
For example, if nums = [2, 3, 2]
, one valid monotonic pair could be:
arr1 = [0, 1, 2]
(non-decreasing)arr2 = [2, 2, 0]
(non-increasing)- At each position:
0+2=2
,1+2=3
,2+0=2
The problem asks you to count all possible monotonic pairs that can be formed. Since the number of valid pairs can be very large, return the result modulo 10^9 + 7
.
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 a valid split:
- Since
arr1
must be non-decreasing, ifarr1[i] = j
, thenarr1[i-1] <= j
- Since
arr2
must be non-increasing andarr2[i] = nums[i] - j
, we neednums[i] - j <= nums[i-1] - arr1[i-1]
These constraints help determine which previous states can lead to each current state, allowing us to build up the solution incrementally using prefix sums for optimization.
Intuition
The key observation is that once we fix the value of arr1[i]
, the value of arr2[i]
is automatically determined since arr1[i] + arr2[i] = nums[i]
. So instead of tracking both arrays, we only need to track the possible values of arr1
.
Let's think about what makes a valid split. If we choose arr1[i] = j
, then arr2[i] = nums[i] - j
. For this to be valid:
arr1
must be non-decreasing, soarr1[i-1] <= j
arr2
must be non-increasing, soarr2[i-1] >= arr2[i]
, which meansnums[i-1] - arr1[i-1] >= nums[i] - j
Rearranging the second constraint: arr1[i-1] <= j + nums[i-1] - nums[i]
Combining both constraints, we get: arr1[i-1] <= min(j, j + nums[i-1] - nums[i])
This tells us that if we want arr1[i] = j
, then arr1[i-1]
can be any value from 0
to min(j, j + nums[i-1] - nums[i])
.
This naturally leads to a dynamic programming approach where we track the number of ways to form valid splits ending at each position with specific values of arr1[i]
. We define f[i][j]
as the number of valid monotonic pairs for the first i+1
elements where arr1[i] = j
.
For the base case, when i = 0
, we can choose any value from 0
to nums[0]
for arr1[0]
(ensuring arr2[0] = nums[0] - arr1[0]
is non-negative).
For transitions, the number of ways to reach state f[i][j]
is the sum of all valid previous states f[i-1][j']
where j' <= min(j, j + nums[i-1] - nums[i])
.
Since we're summing over a range of previous states, we can optimize this using prefix sums to avoid recalculating the same sums repeatedly. This reduces the time complexity from O(n × m²) to O(n × m), where m is the maximum value in nums
.
Learn more about Math, Dynamic Programming, Combinatorics and Prefix Sum patterns.
Solution Approach
The solution implements dynamic programming with prefix sum optimization as outlined in the reference approach.
We define f[i][j]
to represent the number of monotonic array pairs for the subarray [0, ..., i]
where arr1[i] = j
. The final answer will be sum(f[n-1][j])
for all valid j
.
Initialization:
- Create a 2D DP table
f
with dimensionsn × (m + 1)
, wheren = len(nums)
andm = max(nums)
- For the base case when
i = 0
, setf[0][j] = 1
for all0 ≤ j ≤ nums[0]
, since we can choose any value from 0 tonums[0]
forarr1[0]
State Transition:
For each position i
from 1 to n-1
:
-
First, compute the prefix sum array
s
from the previous row:s = list(accumulate(f[i-1]))
- This allows us to quickly query the sum of
f[i-1][0]
tof[i-1][k]
ass[k]
- This allows us to quickly query the sum of
-
For each possible value
j
from 0 tonums[i]
:- Calculate the maximum valid previous value:
k = min(j, j + nums[i-1] - nums[i])
- If
k ≥ 0
, thenf[i][j] = s[k] % mod
- This represents the sum of all
f[i-1][j']
wherej' ≤ k
- Calculate the maximum valid previous value:
The constraint k = min(j, j + nums[i-1] - nums[i])
ensures:
arr1[i-1] ≤ j
(non-decreasing property ofarr1
)arr2[i-1] ≥ arr2[i]
(non-increasing property ofarr2
)
Computing the Answer:
The final answer is the sum of all valid splits ending at position n-1
:
sum(f[-1][:nums[-1] + 1]) % mod
This sums up f[n-1][j]
for all j
from 0 to nums[n-1]
, representing all possible values of arr1[n-1]
that result in valid monotonic pairs.
Time Complexity: O(n × m) where n is the length of nums and m is the maximum value in nums Space Complexity: O(n × 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 the solution with nums = [2, 3, 2]
.
Setup:
n = 3
,m = max(nums) = 3
- We create a DP table
f
of size3 × 4
(to accommodate values 0 to 3) mod = 10^9 + 7
Step 1: Initialize base case (i = 0)
For nums[0] = 2
, we can choose arr1[0]
to be any value from 0 to 2:
- If
arr1[0] = 0
, thenarr2[0] = 2 - 0 = 2
✓ - If
arr1[0] = 1
, thenarr2[0] = 2 - 1 = 1
✓ - If
arr1[0] = 2
, thenarr2[0] = 2 - 2 = 0
✓
So f[0] = [1, 1, 1, 0]
(indices represent j = 0, 1, 2, 3)
Step 2: Process i = 1 (nums[1] = 3)
First, compute prefix sum: s = [1, 2, 3, 3]
(cumulative sum of f[0]
)
For each possible j
from 0 to 3:
-
j = 0:
k = min(0, 0 + 2 - 3) = min(0, -1) = -1
- Since k < 0,
f[1][0] = 0
-
j = 1:
k = min(1, 1 + 2 - 3) = min(1, 0) = 0
f[1][1] = s[0] = 1
- This counts:
arr1[0] = 0, arr1[1] = 1
-
j = 2:
k = min(2, 2 + 2 - 3) = min(2, 1) = 1
f[1][2] = s[1] = 2
- This counts:
arr1[0] ∈ {0, 1}, arr1[1] = 2
-
j = 3:
k = min(3, 3 + 2 - 3) = min(3, 2) = 2
f[1][3] = s[2] = 3
- This counts:
arr1[0] ∈ {0, 1, 2}, arr1[1] = 3
So f[1] = [0, 1, 2, 3]
Step 3: Process i = 2 (nums[2] = 2)
Prefix sum: s = [0, 1, 3, 6]
For each possible j
from 0 to 2:
-
j = 0:
k = min(0, 0 + 3 - 2) = min(0, 1) = 0
f[2][0] = s[0] = 0
-
j = 1:
k = min(1, 1 + 3 - 2) = min(1, 2) = 1
f[2][1] = s[1] = 1
- This represents:
arr1 = [0, 1, 1], arr2 = [2, 2, 1]
-
j = 2:
k = min(2, 2 + 3 - 2) = min(2, 3) = 2
f[2][2] = s[2] = 3
- This represents three valid sequences:
arr1 = [0, 1, 2], arr2 = [2, 2, 0]
arr1 = [0, 2, 2], arr2 = [2, 1, 0]
arr1 = [1, 2, 2], arr2 = [1, 1, 0]
So f[2] = [0, 1, 3, 0]
Final Answer:
Sum of f[2][j]
for j from 0 to nums[2] = 2
:
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, 1, 2], arr2 = [2, 2, 0]
arr1 = [0, 2, 2], arr2 = [2, 1, 0]
arr1 = [1, 2, 2], arr2 = [1, 1, 0]
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 pairs where the i-th element
8 # of the first array is j
9 dp = [[0] * (max_value + 1) for _ in range(n)]
10
11 # Base case: for the first element, any value from 0 to nums[0] is valid
12 for value in range(nums[0] + 1):
13 dp[0][value] = 1
14
15 # Fill the dp table for each position
16 for i in range(1, n):
17 # Calculate prefix sums for the previous row for optimization
18 prefix_sum = list(accumulate(dp[i - 1]))
19
20 # For each possible value at position i
21 for j in range(nums[i] + 1):
22 # Calculate the maximum valid value for the previous position
23 # Based on the constraint that arr1 is non-decreasing and
24 # arr2 is non-increasing where arr1[i] + arr2[i] = nums[i]
25 max_prev_value = min(j, j + nums[i - 1] - nums[i])
26
27 # If there are valid previous values, sum them up
28 if max_prev_value >= 0:
29 dp[i][j] = prefix_sum[max_prev_value] % MOD
30
31 # Return the sum of all valid configurations for the last position
32 return sum(dp[-1][:nums[-1] + 1]) % MOD
33
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 non-decreasing array 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 to optimize cumulative sum calculation
18 int[] prefixSum = new int[maxValue + 1];
19
20 // Process each position from index 1 to n-1
21 for (int i = 1; i < n; i++) {
22 // Build prefix sum array for previous row
23 // prefixSum[j] stores the sum of dp[i-1][0] to dp[i-1][j]
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 dp values 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 decreasing array must be valid
33 int maxPrevValue = Math.min(currentValue, currentValue + nums[i - 1] - nums[i]);
34
35 // Only update if we have a valid previous value
36 if (maxPrevValue >= 0) {
37 dp[i][currentValue] = prefixSum[maxPrevValue];
38 }
39 }
40 }
41
42 // Calculate the final answer by summing all valid configurations at the last position
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 pairs where:
9 // - We've processed elements 0 to i
10 // - The non-decreasing sequence has value j at position i
11 vector<vector<int>> dp(n, vector<int>(maxValue + 1, 0));
12
13 // Base case: for the first element, any value from 0 to nums[0] is valid
14 // Since arr1[0] + arr2[0] = nums[0], and both must be non-negative
15 for (int j = 0; j <= nums[0]; ++j) {
16 dp[0][j] = 1;
17 }
18
19 // prefixSum[j] will store the prefix sum of dp[i-1][0] to dp[i-1][j]
20 // This optimization allows us to quickly compute range sums
21 vector<int> prefixSum(maxValue + 1, 0);
22
23 // Process each position from 1 to n-1
24 for (int i = 1; i < n; ++i) {
25 // Build prefix sum array for the previous row
26 prefixSum[0] = dp[i - 1][0];
27 for (int j = 1; j <= maxValue; ++j) {
28 prefixSum[j] = (prefixSum[j - 1] + dp[i - 1][j]) % MOD;
29 }
30
31 // Calculate dp values for current position
32 for (int j = 0; j <= nums[i]; ++j) {
33 // For arr1[i] = j and arr2[i] = nums[i] - j
34 // We need arr1 to be non-decreasing: arr1[i-1] <= arr1[i] = j
35 // We need arr2 to be non-increasing: arr2[i-1] >= arr2[i] = nums[i] - j
36 // From arr2[i-1] >= nums[i] - j, we get:
37 // nums[i-1] - arr1[i-1] >= nums[i] - j
38 // Therefore: arr1[i-1] <= j + nums[i-1] - nums[i]
39
40 // The valid range for arr1[i-1] is [0, min(j, j + nums[i-1] - nums[i])]
41 int maxPrevValue = min(j, j + nums[i - 1] - nums[i]);
42
43 if (maxPrevValue >= 0) {
44 // Sum all dp[i-1][k] where k is in valid range [0, maxPrevValue]
45 dp[i][j] = prefixSum[maxPrevValue];
46 }
47 }
48 }
49
50 // Sum up all valid configurations for the last position
51 int result = 0;
52 for (int j = 0; j <= nums[n - 1]; ++j) {
53 result = (result + dp[n - 1][j]) % MOD;
54 }
55
56 return result;
57 }
58};
59
1/**
2 * Counts valid pairs of non-decreasing arrays that sum to the given array
3 * @param nums - Input array of positive integers
4 * @returns Number 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 arrays up to index i
12 // where the first array has 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 the first position
19 for (let value = 0; value <= nums[0]; value++) {
20 dp[0][value] = 1;
21 }
22
23 // Prefix sum array to optimize computation
24 const prefixSum: number[] = Array(maxValue + 1).fill(0);
25
26 // Process each position in the array 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 states for current position
35 for (let currentValue = 0; currentValue <= nums[i]; currentValue++) {
36 // Maximum value at previous position that maintains validity
37 // Must ensure both arrays remain non-decreasing
38 const maxPrevValue: number = Math.min(
39 currentValue,
40 currentValue + nums[i - 1] - nums[i]
41 );
42
43 // Only update if we have a valid previous state
44 if (maxPrevValue >= 0) {
45 dp[i][currentValue] = prefixSum[maxPrevValue];
46 }
47 }
48 }
49
50 // Sum all valid endings at the last position
51 let result: number = 0;
52 for (let value = 0; value <= nums[arrayLength - 1]; value++) {
53 result = (result + dp[arrayLength - 1][value]) % 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
.
- The outer loop runs
n
times (from index 1 to n-1) - For each iteration
i
, we compute the prefix sum usingaccumulate(f[i-1])
, which takesO(m)
time - The inner loop runs up to
nums[i] + 1
times, which is at mostm + 1
times - Inside the inner loop, operations are
O(1)
- Therefore, the total time complexity is
O(n × m)
The space complexity is O(n × m)
.
- The 2D array
f
has dimensionsn × (m + 1)
, requiringO(n × m)
space - The prefix sum array
s
created in each iteration has sizeO(m)
, but this doesn't increase the overall space complexity - Therefore, the total space complexity is
O(n × m)
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Integer Overflow When Computing Prefix Sums
The most common pitfall in this solution is forgetting to apply the modulo operation during intermediate calculations, particularly when computing prefix sums. The accumulate
function in Python doesn't automatically apply modulo, which can lead to integer overflow issues even though Python handles big integers well - the values can become unnecessarily large and slow down computation.
Problem Code:
# This can cause values to grow very large
prefix_sum = list(accumulate(dp[i - 1]))
Solution: Apply modulo during the accumulation process:
def accumulate_with_mod(arr, mod):
result = []
total = 0
for val in arr:
total = (total + val) % mod
result.append(total)
return result
# Use this instead
prefix_sum = accumulate_with_mod(dp[i - 1], MOD)
2. Index Out of Bounds When Calculating max_prev_value
Another pitfall is not properly handling edge cases where max_prev_value
could exceed the bounds of the prefix sum array. If nums[i-1] - nums[i]
is large, j + nums[i-1] - nums[i]
might exceed the maximum index of the previous dp row.
Problem Scenario:
When nums[i-1] > nums[i]
by a large margin, the calculated max_prev_value
could be larger than max_value
.
Solution: Ensure proper bounds checking:
max_prev_value = min(j, j + nums[i - 1] - nums[i], max_value)
if max_prev_value >= 0 and max_prev_value < len(prefix_sum):
dp[i][j] = prefix_sum[max_prev_value] % MOD
3. Incorrect Base Case Initialization
A subtle pitfall is incorrectly initializing the base case. Some might initialize only dp[0][0] = 1
thinking we must start with arr1[0] = 0
, but actually any value from 0 to nums[0]
is valid for the first position.
Wrong Initialization:
dp[0][0] = 1 # Only one value initialized
Correct Initialization:
for value in range(nums[0] + 1):
dp[0][value] = 1 # All valid values from 0 to nums[0]
4. Final Answer Calculation Error
When calculating the final answer, it's easy to mistakenly sum the entire last row of the dp table instead of only summing valid entries up to nums[-1]
.
Wrong:
return sum(dp[-1]) % MOD # Sums all entries including invalid ones
Correct:
return sum(dp[-1][:nums[-1] + 1]) % MOD # Only sum valid entries
These pitfalls can lead to incorrect results or runtime errors. Always ensure proper modulo operations, bounds checking, and careful attention to the problem constraints when implementing dynamic programming solutions.
What are the most two important steps in writing a depth first search function? (Select 2)
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!