Facebook Pixel

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:

  1. Both arr1 and arr2 have the same length n as the input array
  2. arr1 is monotonically non-decreasing, meaning arr1[0] <= arr1[1] <= ... <= arr1[n-1]
  3. arr2 is monotonically non-increasing, meaning arr2[0] >= arr2[1] >= ... >= arr2[n-1]
  4. For every index i, the sum arr1[i] + arr2[i] must equal nums[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.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

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 all 0 <= 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 ensure arr1 is non-decreasing)
  • j' <= j + nums[i-1] - nums[i] (to ensure arr2 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:

  1. Initialize the DP table: Create a 2D array f of size n × (m+1) where m = max(nums) to cover all possible values of arr1[i].

  2. Fill base case: Set f[0][j] = 1 for j from 0 to nums[0].

  3. Build the DP table: For each position i from 1 to n-1:

    • First, compute prefix sums of the previous row: s = accumulate(f[i-1])
    • For each possible value j from 0 to nums[i]:
      • Calculate the upper bound: k = min(j, j + nums[i-1] - nums[i])
      • If k >= 0, set f[i][j] = s[k] % mod
  4. 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 Evaluator

Example 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-decreasing
  • arr2 is non-increasing
  • arr1[i] + arr2[i] = nums[i] for all i

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, then arr2[0] = 2
  • If arr1[0] = 1, then arr2[0] = 1
  • If arr1[0] = 2, then arr2[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: Need arr1[0] ≤ min(0, -1) = -1. No valid previous states. f[1][0] = 0
  • j = 1: Need arr1[0] ≤ min(1, 0) = 0. Only arr1[0] = 0 works. f[1][1] = f[0][0] = 1
  • j = 2: Need arr1[0] ≤ min(2, 1) = 1. Both arr1[0] = 0 and arr1[0] = 1 work. f[1][2] = f[0][0] + f[0][1] = 2
  • j = 3: Need arr1[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: Need arr1[1] ≤ 0. No valid states (f[1][0] = 0). f[2][0] = 0
  • j = 1: Need arr1[1] ≤ 1. Only arr1[1] = 1 works. f[2][1] = f[1][1] = 1
  • j = 2: Need arr1[1] ≤ 2. Both arr1[1] = 1 and arr1[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:

  1. arr1 = [0,1,1], arr2 = [2,2,1]
  2. arr1 = [0,2,2], arr2 = [2,1,0]
  3. arr1 = [1,2,2], arr2 = [1,1,0]
  4. 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: Need arr1[1] ≤ 2. We have f[1][1] = 1 and f[1][2] = 2, so f[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]) takes O(m) time
    • Inner loop runs up to nums[i] + 1 times, which is at most O(m)
    • Total for main loops: O(n × m)
  • 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 dimensions n × (m + 1), requiring O(n × m) space
  • The accumulate list s requires O(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
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

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

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More