Facebook Pixel

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:

  1. Both arrays have the same length n as the original array
  2. arr1 is non-decreasing: arr1[0] <= arr1[1] <= ... <= arr1[n-1]
  3. arr2 is non-increasing: arr2[0] >= arr2[1] >= ... >= arr2[n-1]
  4. 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, if arr1[i] = j, then arr1[i-1] <= j
  • Since arr2 must be non-increasing and arr2[i] = nums[i] - j, we need nums[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.

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

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, so arr1[i-1] <= j
  • arr2 must be non-increasing, so arr2[i-1] >= arr2[i], which means nums[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 dimensions n × (m + 1), where n = len(nums) and m = max(nums)
  • For the base case when i = 0, set f[0][j] = 1 for all 0 ≤ j ≤ nums[0], since we can choose any value from 0 to nums[0] for arr1[0]

State Transition: For each position i from 1 to n-1:

  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] to f[i-1][k] as s[k]
  2. For each possible value j from 0 to nums[i]:

    • Calculate the maximum valid previous value: k = min(j, j + nums[i-1] - nums[i])
    • If k ≥ 0, then f[i][j] = s[k] % mod
    • This represents the sum of all f[i-1][j'] where j' ≤ k

The constraint k = min(j, j + nums[i-1] - nums[i]) ensures:

  • arr1[i-1] ≤ j (non-decreasing property of arr1)
  • arr2[i-1] ≥ arr2[i] (non-increasing property of arr2)

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 Evaluator

Example 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 size 3 × 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, then arr2[0] = 2 - 0 = 2
  • If arr1[0] = 1, then arr2[0] = 2 - 1 = 1
  • If arr1[0] = 2, then arr2[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:

  1. arr1 = [0, 1, 1], arr2 = [2, 2, 1]
  2. arr1 = [0, 1, 2], arr2 = [2, 2, 0]
  3. arr1 = [0, 2, 2], arr2 = [2, 1, 0]
  4. 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 using accumulate(f[i-1]), which takes O(m) time
  • The inner loop runs up to nums[i] + 1 times, which is at most m + 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 dimensions n × (m + 1), requiring O(n × m) space
  • The prefix sum array s created in each iteration has size O(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.

Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

What are the most two important steps in writing a depth first search function? (Select 2)


Recommended Readings

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

Load More