Facebook Pixel

3269. Constructing Two Increasing Arrays

Problem Description

You are given two integer arrays nums1 and nums2 that contain only 0s and 1s. Your task is to replace each element in both arrays with positive integers following these rules:

  • Replace every 0 with an even positive integer
  • Replace every 1 with an odd positive integer
  • After replacement, both arrays must be strictly increasing (each element must be greater than the previous one)
  • Each positive integer can be used at most once across both arrays

The goal is to minimize the largest number that appears in either array after all replacements are made.

For example, if nums1 = [0, 1] and nums2 = [1, 0], you need to:

  1. Replace the 0 in nums1 with an even number and the 1 with an odd number
  2. Replace the 1 in nums2 with an odd number and the 0 with an even number
  3. Ensure both arrays are increasing
  4. Use each integer at most once
  5. Minimize the maximum value across both arrays

The solution uses dynamic programming where f[i][j] represents the minimum possible largest value when considering the first i elements of nums1 and the first j elements of nums2. The nxt(x, y) function finds the smallest integer greater than x that has the same parity as y (even if y = 0, odd if y = 1).

The transition works by either:

  • Adding the next element from nums1: nxt(f[i-1][j], nums1[i-1])
  • Adding the next element from nums2: nxt(f[i][j-1], nums2[j-1])

And taking the minimum of these two options to minimize the largest value needed.

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

Intuition

The key insight is that we're essentially merging two sequences while maintaining specific parity constraints and trying to minimize the maximum value used. Think of it like having two production lines where we need to assign increasing numbers to items, with even numbers for 0s and odd numbers for 1s.

Since we want to minimize the largest number and each integer can only be used once, we need to be strategic about which array gets which number. If we greedily assign numbers to one array first, we might force the other array to use unnecessarily large numbers.

This naturally leads to a dynamic programming approach where we consider processing elements from both arrays simultaneously. At each step, we have a choice: process the next element from nums1 or process the next element from nums2.

The state f[i][j] captures "what's the smallest maximum number we need if we've processed i elements from nums1 and j elements from nums2?" This is optimal because:

  1. We need to track how many elements we've processed from each array to know what remains
  2. We need to know the current maximum number used so far to ensure the next number is both greater and has the correct parity

The nxt(x, y) function elegantly handles finding the next valid number: given the current maximum x, find the smallest number greater than x with the same parity as y. If x = 5 (odd) and we need an even number (y = 0), we get 6. If we need an odd number (y = 1), we get 7.

By considering both options at each step (taking from nums1 or nums2) and choosing the one that results in a smaller maximum, we ensure we're using the smallest possible numbers throughout the process, ultimately minimizing the final maximum value.

Learn more about Dynamic Programming patterns.

Solution Approach

The solution uses a 2D dynamic programming table where f[i][j] represents the minimum possible largest number when we've assigned values to the first i elements of nums1 and the first j elements of nums2.

Helper Function: The nxt(x, y) function finds the smallest integer greater than x that has the same parity as y:

  • If (x & 1 ^ y) == 1, it means x and y have different parities, so we return x + 1
  • Otherwise, they have the same parity, so we need to skip one number and return x + 2

Initialization: We create a 2D DP table f of size (m+1) × (n+1) initialized with zeros, where m and n are the lengths of nums1 and nums2 respectively.

Base Cases:

  1. For f[i][0] (only processing nums1): Each element must be greater than the previous with correct parity. We iterate through nums1 and update f[i][0] = nxt(f[i-1][0], nums1[i-1])

  2. For f[0][j] (only processing nums2): Similarly, we iterate through nums2 and update f[0][j] = nxt(f[0][j-1], nums2[j-1])

State Transition: For general cases where i > 0 and j > 0, we have two choices at each step:

  • Assign a number to nums1[i-1]: This would require nxt(f[i-1][j], nums1[i-1])
  • Assign a number to nums2[j-1]: This would require nxt(f[i][j-1], nums2[j-1])

We take the minimum of these two options: f[i][j] = min(nxt(f[i-1][j], nums1[i-1]), nxt(f[i][j-1], nums2[j-1]))

Final Answer: The value f[m][n] gives us the minimum possible largest number after processing all elements from both arrays.

The time complexity is O(m × n) for filling the DP table, and the space complexity is also O(m × n) for storing the 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 a concrete example with nums1 = [1, 0, 1, 0] and nums2 = [0, 1].

First, let's understand what nxt(x, y) does:

  • nxt(5, 0) = 6 (next even number after 5)
  • nxt(5, 1) = 7 (next odd number after 5)
  • nxt(6, 0) = 8 (next even number after 6)
  • nxt(6, 1) = 7 (next odd number after 6)

Now, let's build our DP table f[i][j] step by step:

Initialization: Create a 5×3 table (since m=4, n=2)

Base cases:

  • f[0][0] = 0 (no elements processed)
  • f[1][0] = nxt(0, nums1[0]) = nxt(0, 1) = 1 (first element of nums1 is 1, need odd)
  • f[2][0] = nxt(1, nums1[1]) = nxt(1, 0) = 2 (second element is 0, need even > 1)
  • f[3][0] = nxt(2, nums1[2]) = nxt(2, 1) = 3 (third element is 1, need odd > 2)
  • f[4][0] = nxt(3, nums1[3]) = nxt(3, 0) = 4 (fourth element is 0, need even > 3)

Similarly for nums2:

  • f[0][1] = nxt(0, nums2[0]) = nxt(0, 0) = 2 (first element is 0, need even)
  • f[0][2] = nxt(2, nums2[1]) = nxt(2, 1) = 3 (second element is 1, need odd > 2)

General transitions: For f[1][1]:

  • Option 1: Process nums1[0] after nums2[0]: nxt(f[0][1], nums1[0]) = nxt(2, 1) = 3
  • Option 2: Process nums2[0] after nums1[0]: nxt(f[1][0], nums2[0]) = nxt(1, 0) = 2
  • f[1][1] = min(3, 2) = 2

For f[2][1]:

  • Option 1: Process nums1[1] after processing nums1[0] and nums2[0]: nxt(f[1][1], nums1[1]) = nxt(2, 0) = 4
  • Option 2: Process nums2[0] after processing nums1[0] and nums1[1]: nxt(f[2][0], nums2[0]) = nxt(2, 0) = 4
  • f[2][1] = min(4, 4) = 4

Continuing this process through the entire table:

     j=0  j=1  j=2
i=0   0    2    3
i=1   1    2    3
i=2   2    4    5
i=3   3    5    6
i=4   4    6    7

The final answer is f[4][2] = 7, meaning the minimum possible largest number needed is 7.

This would correspond to assignments like:

  • nums1: [1, 2, 3, 4] (odd, even, odd, even)
  • nums2: [6, 7] (even, odd)

Each array is strictly increasing, parities match the original 0s and 1s, all numbers are unique, and the maximum value (7) is minimized.

Solution Implementation

1class Solution:
2    def minLargest(self, nums1: List[int], nums2: List[int]) -> int:
3        def get_next_value(previous_value: int, current_num: int) -> int:
4            """
5            Calculate the next valid value based on parity constraints.
6
7            Args:
8                previous_value: The previous value in the sequence
9                current_num: The current number to consider for parity
10
11            Returns:
12                The next valid value (previous + 1 if different parity, else previous + 2)
13            """
14            # Check if previous value and current number have different parity
15            if (previous_value & 1) ^ (current_num & 1) == 1:
16                return previous_value + 1
17            else:
18                return previous_value + 2
19
20        # Get dimensions of both arrays
21        length_nums1 = len(nums1)
22        length_nums2 = len(nums2)
23
24        # Initialize DP table with dimensions (m+1) x (n+1)
25        # dp[i][j] represents the minimum largest value using first i elements from nums1
26        # and first j elements from nums2
27        dp = [[0] * (length_nums2 + 1) for _ in range(length_nums1 + 1)]
28
29        # Fill first column: only using elements from nums1
30        for i in range(1, length_nums1 + 1):
31            current_element = nums1[i - 1]
32            dp[i][0] = get_next_value(dp[i - 1][0], current_element)
33
34        # Fill first row: only using elements from nums2
35        for j in range(1, length_nums2 + 1):
36            current_element = nums2[j - 1]
37            dp[0][j] = get_next_value(dp[0][j - 1], current_element)
38
39        # Fill the rest of the DP table
40        for i in range(1, length_nums1 + 1):
41            current_nums1_element = nums1[i - 1]
42            for j in range(1, length_nums2 + 1):
43                current_nums2_element = nums2[j - 1]
44
45                # Choose minimum between taking from nums1 or nums2
46                option_from_nums1 = get_next_value(dp[i - 1][j], current_nums1_element)
47                option_from_nums2 = get_next_value(dp[i][j - 1], current_nums2_element)
48
49                dp[i][j] = min(option_from_nums1, option_from_nums2)
50
51        # Return the minimum largest value using all elements from both arrays
52        return dp[length_nums1][length_nums2]
53
1class Solution {
2    public int minLargest(int[] nums1, int[] nums2) {
3        int m = nums1.length;
4        int n = nums2.length;
5
6        // dp[i][j] represents the minimum largest value when considering
7        // first i elements from nums1 and first j elements from nums2
8        int[][] dp = new int[m + 1][n + 1];
9
10        // Initialize first column: only taking elements from nums1
11        for (int i = 1; i <= m; i++) {
12            dp[i][0] = getNextValue(dp[i - 1][0], nums1[i - 1]);
13        }
14
15        // Initialize first row: only taking elements from nums2
16        for (int j = 1; j <= n; j++) {
17            dp[0][j] = getNextValue(dp[0][j - 1], nums2[j - 1]);
18        }
19
20        // Fill the dp table
21        for (int i = 1; i <= m; i++) {
22            for (int j = 1; j <= n; j++) {
23                // Option 1: Take element from nums1
24                int takeFromNums1 = getNextValue(dp[i - 1][j], nums1[i - 1]);
25
26                // Option 2: Take element from nums2
27                int takeFromNums2 = getNextValue(dp[i][j - 1], nums2[j - 1]);
28
29                // Choose the minimum of the two options
30                dp[i][j] = Math.min(takeFromNums1, takeFromNums2);
31            }
32        }
33
34        return dp[m][n];
35    }
36
37    /**
38     * Calculates the next value based on the current value and the element value.
39     * If the XOR of their least significant bits equals 1, increment by 1.
40     * Otherwise, increment by 2.
41     *
42     * @param currentValue the current accumulated value
43     * @param elementValue the value of the current element
44     * @return the next value after applying the rule
45     */
46    private int getNextValue(int currentValue, int elementValue) {
47        // Check if LSB of currentValue XOR LSB of elementValue equals 1
48        if ((currentValue & 1) ^ (elementValue & 1)) == 1) {
49            return currentValue + 1;
50        } else {
51            return currentValue + 2;
52        }
53    }
54}
55
1class Solution {
2public:
3    int minLargest(vector<int>& nums1, vector<int>& nums2) {
4        int m = nums1.size();
5        int n = nums2.size();
6
7        // dp[i][j] represents the minimum possible largest value when using
8        // first i elements from nums1 and first j elements from nums2
9        vector<vector<int>> dp(m + 1, vector<int>(n + 1, 0));
10
11        // Helper function to get the next valid value based on parity constraint
12        // If current value and element have different parity, increment by 1
13        // If they have the same parity, increment by 2 to maintain alternating parity
14        auto getNextValue = [](int currentValue, int element) -> int {
15            // Check if current value and element have different parity
16            bool differentParity = ((currentValue & 1) ^ (element & 1)) == 1;
17            return differentParity ? currentValue + 1 : currentValue + 2;
18        };
19
20        // Initialize first column: only using elements from nums1
21        for (int i = 1; i <= m; ++i) {
22            dp[i][0] = getNextValue(dp[i - 1][0], nums1[i - 1]);
23        }
24
25        // Initialize first row: only using elements from nums2
26        for (int j = 1; j <= n; ++j) {
27            dp[0][j] = getNextValue(dp[0][j - 1], nums2[j - 1]);
28        }
29
30        // Fill the dp table
31        for (int i = 1; i <= m; ++i) {
32            for (int j = 1; j <= n; ++j) {
33                // Option 1: Take element from nums1
34                int takeFromNums1 = getNextValue(dp[i - 1][j], nums1[i - 1]);
35
36                // Option 2: Take element from nums2
37                int takeFromNums2 = getNextValue(dp[i][j - 1], nums2[j - 1]);
38
39                // Choose the minimum of the two options
40                dp[i][j] = min(takeFromNums1, takeFromNums2);
41            }
42        }
43
44        // Return the minimum largest value when using all elements from both arrays
45        return dp[m][n];
46    }
47};
48
1/**
2 * Finds the minimum largest value after applying operations on two arrays
3 * @param nums1 - First input array
4 * @param nums2 - Second input array
5 * @returns The minimum possible largest value
6 */
7function minLargest(nums1: number[], nums2: number[]): number {
8    const firstArrayLength: number = nums1.length;
9    const secondArrayLength: number = nums2.length;
10
11    // Create a 2D DP table to store intermediate results
12    // dp[i][j] represents the minimum largest value using first i elements from nums1 and first j elements from nums2
13    const dp: number[][] = Array.from(
14        { length: firstArrayLength + 1 },
15        () => Array(secondArrayLength + 1).fill(0)
16    );
17
18    /**
19     * Calculates the next valid value based on current value and element
20     * If current value and element have different parity, increment by 1
21     * Otherwise, increment by 2 to maintain the alternating pattern
22     * @param currentValue - The current maximum value
23     * @param element - The element to compare parity with
24     * @returns The next valid value
25     */
26    const getNextValue = (currentValue: number, element: number): number => {
27        // Check if currentValue and element have different parity using XOR
28        const haveDifferentParity: boolean = ((currentValue & 1) ^ element) !== 0;
29        return haveDifferentParity ? currentValue + 1 : currentValue + 2;
30    };
31
32    // Initialize first column: using only elements from nums1
33    for (let i = 1; i <= firstArrayLength; i++) {
34        dp[i][0] = getNextValue(dp[i - 1][0], nums1[i - 1]);
35    }
36
37    // Initialize first row: using only elements from nums2
38    for (let j = 1; j <= secondArrayLength; j++) {
39        dp[0][j] = getNextValue(dp[0][j - 1], nums2[j - 1]);
40    }
41
42    // Fill the DP table using dynamic programming
43    for (let i = 1; i <= firstArrayLength; i++) {
44        for (let j = 1; j <= secondArrayLength; j++) {
45            // Choose minimum between taking element from nums1 or nums2
46            const takeFromNums1: number = getNextValue(dp[i - 1][j], nums1[i - 1]);
47            const takeFromNums2: number = getNextValue(dp[i][j - 1], nums2[j - 1]);
48            dp[i][j] = Math.min(takeFromNums1, takeFromNums2);
49        }
50    }
51
52    // Return the minimum largest value after processing all elements
53    return dp[firstArrayLength][secondArrayLength];
54}
55

Time and Space Complexity

The time complexity is O(m × n), where m is the length of nums1 and n is the length of nums2. This is because the algorithm uses a nested loop structure - after the initialization phase, there are two nested loops that iterate through all elements of nums1 (from index 1 to m) and all elements of nums2 (from index 1 to n), resulting in m × n iterations. Each iteration performs constant-time operations (the nxt function and comparison operations).

The space complexity is O(m × n). The algorithm creates a 2D array f with dimensions (m + 1) × (n + 1) to store the dynamic programming states. This requires O((m + 1) × (n + 1)) = O(m × n) space. The additional variables like i, j, x, and y only require constant space, so the overall space complexity is dominated by the 2D array.

Learn more about how to find time and space complexity quickly.

Common Pitfalls

1. Misunderstanding the Parity Check Logic

Pitfall: The XOR operation (previous_value & 1) ^ (current_num & 1) can be confusing. Developers might incorrectly implement this as checking if the previous value has the same parity as what we need, rather than checking if it has different parity from what we need.

Why it happens: The logic seems counterintuitive - when XOR equals 1 (different parities), we only add 1, but when XOR equals 0 (same parities), we add 2.

Correct understanding:

  • current_num = 0 means we need an even number
  • current_num = 1 means we need an odd number
  • If previous_value is odd and we need even (XOR = 1), then previous_value + 1 gives us the next even number
  • If previous_value is even and we need even (XOR = 0), then previous_value + 2 gives us the next even number (skipping the odd one)

2. Incorrect DP Table Initialization

Pitfall: Initializing the entire DP table with values other than 0, or forgetting that dp[0][0] = 0 represents the starting point where no elements have been processed yet.

Solution: Always start with dp[0][0] = 0 as the base case, representing that before processing any elements, the largest number used is 0.

3. Off-by-One Errors in Array Indexing

Pitfall: Confusion between DP indices and array indices. The DP table uses 1-based indexing conceptually (where dp[i][j] represents processing i elements from nums1 and j from nums2), while the arrays use 0-based indexing.

Example of incorrect code:

# Wrong - using i directly instead of i-1
dp[i][0] = get_next_value(dp[i-1][0], nums1[i])

Correct approach:

# Correct - properly adjusting for 0-based array indexing
dp[i][0] = get_next_value(dp[i-1][0], nums1[i-1])

4. Forgetting the Global Constraint

Pitfall: Thinking locally about each array independently and forgetting that we're trying to minimize the maximum value across BOTH arrays simultaneously. The DP approach handles this implicitly by always choosing the minimum necessary value at each step.

Why it matters: If you tried to solve each array independently and then merge, you might violate the constraint that each integer can only be used once across both arrays.

5. Incorrect Understanding of State Transition

Pitfall: Not understanding why we take the minimum of two options in the state transition. Some might think we should take the maximum or have a different logic.

Correct reasoning: At each step dp[i][j], we're deciding which element to assign a value to next. Since we want to minimize the largest value used:

  • If we assign to nums1[i-1] next, we need at least get_next_value(dp[i-1][j], nums1[i-1])
  • If we assign to nums2[j-1] next, we need at least get_next_value(dp[i][j-1], nums2[j-1])
  • We choose the option that results in the smaller maximum value

Solution: Always remember that the DP value represents the minimum possible largest number used so far, and we're greedily choosing the option that keeps this value as small as possible.

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