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:
- Replace the 0 in
nums1
with an even number and the 1 with an odd number - Replace the 1 in
nums2
with an odd number and the 0 with an even number - Ensure both arrays are increasing
- Use each integer at most once
- 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.
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:
- We need to track how many elements we've processed from each array to know what remains
- 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 meansx
andy
have different parities, so we returnx + 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:
-
For
f[i][0]
(only processingnums1
): Each element must be greater than the previous with correct parity. We iterate throughnums1
and updatef[i][0] = nxt(f[i-1][0], nums1[i-1])
-
For
f[0][j]
(only processingnums2
): Similarly, we iterate throughnums2
and updatef[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 requirenxt(f[i-1][j], nums1[i-1])
- Assign a number to
nums2[j-1]
: This would requirenxt(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 EvaluatorExample 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 numbercurrent_num = 1
means we need an odd number- If
previous_value
is odd and we need even (XOR = 1), thenprevious_value + 1
gives us the next even number - If
previous_value
is even and we need even (XOR = 0), thenprevious_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 leastget_next_value(dp[i-1][j], nums1[i-1])
- If we assign to
nums2[j-1]
next, we need at leastget_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.
What are the most two important steps in writing a depth first search function? (Select 2)
Recommended Readings
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
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask
Want a Structured Path to Master System Design Too? Don’t Miss This!