Facebook Pixel

3480. Maximize Subarrays After Removing One Conflicting Pair

Problem Description

You have an integer n that represents an array nums = [1, 2, 3, ..., n] containing numbers from 1 to n in order.

You're also given a 2D array conflictingPairs where each conflictingPairs[i] = [a, b] indicates that elements a and b form a conflicting pair.

Your task is to:

  1. Remove exactly one element from conflictingPairs (delete one conflicting pair)
  2. After removal, count all possible non-empty subarrays of nums that are valid

A subarray is considered valid if it doesn't contain both elements from any remaining conflicting pair. In other words, for each remaining pair [a, b], a valid subarray cannot have both a and b present at the same time.

The goal is to find which single conflicting pair to remove such that the number of valid subarrays is maximized, and return this maximum count.

Example to clarify:

  • If n = 3, then nums = [1, 2, 3]
  • If conflictingPairs = [[1, 3], [2, 3]]
  • If we remove [1, 3], only [2, 3] remains as a conflict
  • Valid subarrays would be: [1], [2], [3], [1, 2] (since 1 and 2 don't conflict after removing [1,3])
  • Invalid subarrays would be: [2, 3], [1, 2, 3] (both contain 2 and 3 which still conflict)

The function should return the maximum possible number of valid subarrays after optimally choosing which conflicting pair to remove.

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

Intuition

Let's think about how conflicting pairs restrict our subarrays. For any subarray starting at position a, it can extend rightward until it hits a position b where (a, b) forms a conflicting pair. The key insight is that for each starting position, we need to find the earliest (minimum) position where a conflict occurs.

Consider iterating through starting positions from right to left (from n down to 1). For each starting position a, we need to track all positions b > a that conflict with a. The rightmost valid ending position for subarrays starting at a is one less than the minimum such b.

Now, what happens when we remove one conflicting pair? If we remove a pair that contains this minimum b value (let's call it b1), then the new limit becomes the second minimum value (call it b2). This gives us additional valid subarrays.

The clever observation is that we don't need to try removing every possible pair individually. Instead, we can:

  1. Calculate the base count of valid subarrays assuming no pairs are removed (using the minimum b values)
  2. Track how much benefit we'd get by removing pairs that involve each minimum b1

For each starting position a, we maintain:

  • b1: the minimum conflicting position (this determines our base count)
  • b2: the second minimum conflicting position (this determines the benefit if we remove a pair involving b1)

The benefit of removing a pair involving b1 is b2 - b1 additional subarrays. We accumulate these potential benefits in a counter array cnt[b1].

Finally, the answer is the base count plus the maximum benefit we can get from removing one pair (which is the maximum value in our cnt array).

This approach is efficient because instead of trying all possible removals, we identify which removal would give us the maximum benefit by tracking the gap between the first and second minimum conflicting positions.

Learn more about Segment Tree and Prefix Sum patterns.

Solution Approach

The implementation follows the intuition by processing conflicting pairs and tracking minimum values efficiently:

1. Data Structure Setup:

  • Create an adjacency list g where g[a] stores all values b that conflict with a (where a < b)
  • Normalize pairs so that the smaller value is always first: if a > b, swap them
  • Initialize a counter array cnt of size n + 2 to track potential benefits

2. Main Algorithm:

Process starting positions from right to left (n down to 1):

  • Maintain two variables:
    • b1: minimum conflicting position (initially n + 1)
    • b2: second minimum conflicting position (initially n + 1)

For each starting position a:

  • Update minimums: Iterate through all b values in g[a] (positions that conflict with a)

    • If b < b1: Update both values - b2 = b1, b1 = b
    • Else if b < b2: Update only b2 = b
  • Calculate contributions:

    • Base contribution: ans += b1 - a (number of valid subarrays starting at a without removing any pair)
    • Potential benefit: cnt[b1] += b2 - b1 (additional subarrays if we remove a pair involving b1)

3. Find Maximum Benefit:

  • Track the maximum value in cnt array as add
  • This represents the maximum additional subarrays we can get by removing one conflicting pair

4. Return Result:

  • Final answer = ans + add
  • Where ans is the base count and add is the maximum benefit from removal

Key Pattern: The solution uses a greedy approach combined with dynamic tracking of minimum values. By processing positions in reverse order, we can efficiently maintain the two smallest conflicting positions for each starting point without recomputing them repeatedly.

Time Complexity: O(n + m) where m is the number of conflicting pairs Space Complexity: O(n + m) for storing the adjacency list and counter array

Ready to land your dream job?

Unlock your dream job with a 3-minute evaluator for a personalized learning plan!

Start Evaluator

Example Walkthrough

Let's walk through a concrete example with n = 4 and conflictingPairs = [[1, 3], [2, 4], [1, 4]].

This means nums = [1, 2, 3, 4] and we have three conflicting pairs.

Step 1: Build adjacency list After normalizing pairs (smaller value first), we get:

  • g[1] = [3, 4] (1 conflicts with 3 and 4)
  • g[2] = [4] (2 conflicts with 4)
  • g[3] = [] (3 has no conflicts with higher numbers)
  • g[4] = [] (4 is the last element)

Step 2: Process each starting position from right to left

Initialize: ans = 0, cnt = [0, 0, 0, 0, 0, 0] (size n+2)

Starting at position 4:

  • b1 = 5, b2 = 5 (initialized to n+1)
  • No conflicts in g[4]
  • Base contribution: ans += 5 - 4 = 1 (subarray [4])
  • Benefit if we remove pair with b1=5: cnt[5] += 5 - 5 = 0
  • Running ans = 1

Starting at position 3:

  • b1 = 5, b2 = 5
  • No conflicts in g[3]
  • Base contribution: ans += 5 - 3 = 2 (subarrays [3] and [3,4])
  • Benefit: cnt[5] += 5 - 5 = 0
  • Running ans = 3

Starting at position 2:

  • b1 = 5, b2 = 5
  • Process conflict with 4: b1 = 4, b2 = 5
  • Base contribution: ans += 4 - 2 = 2 (subarrays [2] and [2,3])
  • Benefit if we remove pair with b1=4: cnt[4] += 5 - 4 = 1
  • Running ans = 5

Starting at position 1:

  • b1 = 5, b2 = 5
  • Process conflict with 3: b1 = 3, b2 = 5
  • Process conflict with 4: Since 4 > 3 but 4 < 5, update b2 = 4
  • Final: b1 = 3, b2 = 4
  • Base contribution: ans += 3 - 1 = 2 (subarrays [1] and [1,2])
  • Benefit if we remove pair with b1=3: cnt[3] += 4 - 3 = 1
  • Running ans = 7

Step 3: Find maximum benefit

  • cnt = [0, 0, 0, 1, 1, 0]
  • Maximum value in cnt is 1

Step 4: Return result

  • Final answer = 7 + 1 = 8

Verification: The 8 valid subarrays after optimal removal are:

  • If we remove [1,3]: [1], [2], [3], [4], [1,2], [2,3], [3,4], [1,2,3]
  • If we remove [2,4]: [1], [2], [3], [4], [1,2], [2,3], [3,4], [2,3,4]

Both removals give us 8 valid subarrays, which matches our calculated answer.

Solution Implementation

1class Solution:
2    def maxSubarrays(self, n: int, conflictingPairs: List[List[int]]) -> int:
3        # Build adjacency list where graph[i] contains all nodes j where i < j and (i,j) is a conflicting pair
4        graph = [[] for _ in range(n + 1)]
5        for node_a, node_b in conflictingPairs:
6            # Ensure smaller index comes first
7            if node_a > node_b:
8                node_a, node_b = node_b, node_a
9            graph[node_a].append(node_b)
10      
11        # Count array to track potential improvements at each position
12        count = [0] * (n + 2)
13      
14        # Initialize result and maximum additional value
15        result = 0
16        max_additional = 0
17      
18        # Track the two smallest conflicting nodes for current position
19        smallest_conflict = n + 1  # First smallest conflicting node
20        second_smallest_conflict = n + 1  # Second smallest conflicting node
21      
22        # Process nodes in reverse order (from n to 1)
23        for current_node in range(n, 0, -1):
24            # Update smallest and second smallest conflicting nodes
25            for conflict_node in graph[current_node]:
26                if conflict_node < smallest_conflict:
27                    # New smallest found, shift previous smallest to second
28                    second_smallest_conflict = smallest_conflict
29                    smallest_conflict = conflict_node
30                elif conflict_node < second_smallest_conflict:
31                    # Update second smallest
32                    second_smallest_conflict = conflict_node
33          
34            # Add contribution of current node to result
35            result += smallest_conflict - current_node
36          
37            # Update count for potential improvement at smallest_conflict position
38            count[smallest_conflict] += second_smallest_conflict - smallest_conflict
39          
40            # Track maximum potential improvement
41            max_additional = max(max_additional, count[smallest_conflict])
42      
43        # Add the maximum improvement to final result
44        result += max_additional
45      
46        return result
47
1class Solution {
2    public long maxSubarrays(int n, int[][] conflictingPairs) {
3        // Create adjacency list to store conflicts
4        // For each element i, store all elements j where j > i and they conflict
5        List<Integer>[] adjacencyList = new List[n + 1];
6        Arrays.setAll(adjacencyList, index -> new ArrayList<>());
7      
8        // Build the conflict graph
9        // Ensure smaller index points to larger index
10        for (int[] pair : conflictingPairs) {
11            int smaller = pair[0];
12            int larger = pair[1];
13          
14            // Swap if needed to maintain smaller -> larger relationship
15            if (smaller > larger) {
16                int temp = smaller;
17                smaller = larger;
18                larger = temp;
19            }
20            adjacencyList[smaller].add(larger);
21        }
22      
23        // Array to count potential additional subarrays ending at each position
24        long[] additionalCount = new long[n + 2];
25        long totalSubarrays = 0;
26        long maxAdditional = 0;
27      
28        // Track the two smallest conflicting positions for current element
29        int firstBoundary = n + 1;  // Smallest conflicting position
30        int secondBoundary = n + 1; // Second smallest conflicting position
31      
32        // Process elements from right to left
33        for (int currentElement = n; currentElement > 0; --currentElement) {
34            // Update boundaries based on conflicts with current element
35            for (int conflictingElement : adjacencyList[currentElement]) {
36                if (conflictingElement < firstBoundary) {
37                    // New smallest boundary found
38                    secondBoundary = firstBoundary;
39                    firstBoundary = conflictingElement;
40                } else if (conflictingElement < secondBoundary) {
41                    // New second smallest boundary found
42                    secondBoundary = conflictingElement;
43                }
44            }
45          
46            // Count subarrays starting at currentElement and ending before firstBoundary
47            totalSubarrays += firstBoundary - currentElement;
48          
49            // Track potential additional subarrays that could be formed
50            // if we extend beyond firstBoundary up to secondBoundary
51            additionalCount[firstBoundary] += secondBoundary - firstBoundary;
52          
53            // Keep track of maximum additional subarrays possible
54            maxAdditional = Math.max(maxAdditional, additionalCount[firstBoundary]);
55        }
56      
57        // Add the maximum possible additional subarrays to the total
58        totalSubarrays += maxAdditional;
59      
60        return totalSubarrays;
61    }
62}
63
1class Solution {
2public:
3    long long maxSubarrays(int n, vector<vector<int>>& conflictingPairs) {
4        // Build adjacency list where graph[i] contains all elements that conflict with i
5        // Only store conflicts where i < j to avoid duplication
6        vector<vector<int>> adjacencyList(n + 1);
7      
8        for (auto& pair : conflictingPairs) {
9            int firstElement = pair[0];
10            int secondElement = pair[1];
11          
12            // Ensure firstElement < secondElement for consistent storage
13            if (firstElement > secondElement) {
14                swap(firstElement, secondElement);
15            }
16          
17            // Store the conflict: firstElement conflicts with secondElement
18            adjacencyList[firstElement].push_back(secondElement);
19        }
20
21        // Array to count potential additional subarrays at each position
22        vector<long long> additionalCount(n + 2, 0);
23      
24        // Total number of valid subarrays
25        long long totalSubarrays = 0;
26      
27        // Maximum additional subarrays we can get
28        long long maxAdditional = 0;
29      
30        // Track the two smallest conflicting elements greater than current element
31        // Initialize to n+1 (beyond valid range) indicating no conflicts
32        int smallestConflict = n + 1;
33        int secondSmallestConflict = n + 1;
34
35        // Process elements from largest to smallest
36        for (int currentElement = n; currentElement > 0; --currentElement) {
37            // Update the two smallest conflicting elements based on current element's conflicts
38            for (int conflictingElement : adjacencyList[currentElement]) {
39                if (conflictingElement < smallestConflict) {
40                    // New smallest found, shift previous smallest to second
41                    secondSmallestConflict = smallestConflict;
42                    smallestConflict = conflictingElement;
43                } else if (conflictingElement < secondSmallestConflict) {
44                    // Update second smallest
45                    secondSmallestConflict = conflictingElement;
46                }
47            }
48          
49            // Count subarrays starting at currentElement and ending before smallestConflict
50            totalSubarrays += smallestConflict - currentElement;
51          
52            // Track potential additional subarrays between smallestConflict and secondSmallestConflict
53            additionalCount[smallestConflict] += secondSmallestConflict - smallestConflict;
54          
55            // Keep track of the maximum additional subarrays we can obtain
56            maxAdditional = max(maxAdditional, additionalCount[smallestConflict]);
57        }
58
59        // Add the maximum possible additional subarrays to the total
60        totalSubarrays += maxAdditional;
61      
62        return totalSubarrays;
63    }
64};
65
1/**
2 * Calculates the maximum number of subarrays that can be formed from n elements
3 * given conflicting pairs that cannot be in the same subarray
4 * 
5 * @param n - The number of elements (1 to n)
6 * @param conflictingPairs - Array of pairs [a, b] where a and b cannot be in the same subarray
7 * @returns The maximum number of subarrays
8 */
9function maxSubarrays(n: number, conflictingPairs: number[][]): number {
10    // Build adjacency list for conflicts (store only b > a to avoid duplicates)
11    const conflictGraph: number[][] = Array.from({ length: n + 1 }, () => []);
12  
13    for (let [elementA, elementB] of conflictingPairs) {
14        // Ensure elementA is smaller than elementB for consistent processing
15        if (elementA > elementB) {
16            [elementA, elementB] = [elementB, elementA];
17        }
18        conflictGraph[elementA].push(elementB);
19    }
20
21    // Track potential additional subarrays at each boundary position
22    const boundaryCount: number[] = Array(n + 2).fill(0);
23  
24    // Initialize result and maximum additional subarrays
25    let totalSubarrays: number = 0;
26    let maxAdditionalSubarrays: number = 0;
27  
28    // Track the two smallest conflicting elements greater than current element
29    let firstBoundary: number = n + 1;
30    let secondBoundary: number = n + 1;
31
32    // Process elements from largest to smallest
33    for (let currentElement: number = n; currentElement > 0; currentElement--) {
34        // Update boundaries based on conflicts with current element
35        for (const conflictingElement of conflictGraph[currentElement]) {
36            if (conflictingElement < firstBoundary) {
37                // New smallest boundary found, shift existing boundaries
38                secondBoundary = firstBoundary;
39                firstBoundary = conflictingElement;
40            } else if (conflictingElement < secondBoundary) {
41                // New second smallest boundary found
42                secondBoundary = conflictingElement;
43            }
44        }
45      
46        // Add subarrays that can be formed from current element to first boundary
47        totalSubarrays += firstBoundary - currentElement;
48      
49        // Track potential additional subarrays between boundaries
50        boundaryCount[firstBoundary] += secondBoundary - firstBoundary;
51      
52        // Update maximum additional subarrays possible
53        maxAdditionalSubarrays = Math.max(maxAdditionalSubarrays, boundaryCount[firstBoundary]);
54    }
55
56    // Add the maximum additional subarrays to the total
57    totalSubarrays += maxAdditionalSubarrays;
58  
59    return totalSubarrays;
60}
61

Time and Space Complexity

The time complexity is O(n + m), where n is the input parameter n and m is the number of conflicting pairs. The algorithm iterates through all values from n down to 1 (taking O(n) time), and for each value a, it processes all edges in g[a]. Since each conflicting pair is stored exactly once in the adjacency list, the total time to process all edges across all iterations is O(m). Therefore, the overall time complexity is O(n + m).

The space complexity is O(n + m). The adjacency list g requires O(n + m) space (array of size n+1 plus storage for all m edges), and the cnt array requires O(n) space. Since O(n + m) dominates, the overall space complexity is O(n + m).

Note: The reference answer states O(n) for both complexities where n is the number of conflicting pairs, which appears to use different notation. In the reference, n seems to refer to the number of pairs (m in our analysis), making the reference answer O(m) in our notation. However, this would be incorrect for the actual implementation since the algorithm must iterate through all values from n to 1, making the time complexity dependent on both the parameter n and the number of pairs m.

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

Common Pitfalls

1. Incorrect Tracking of Minimum Conflicting Positions

The Pitfall: The current implementation has a critical bug in how it tracks the smallest and second smallest conflicting positions. The variables smallest_conflict and second_smallest_conflict are initialized once outside the loop and never reset for each position. This causes them to accumulate values from previous iterations, leading to incorrect calculations.

Why This Happens: When processing position current_node, we need to find the two smallest positions that conflict with it. However, the code maintains these values across all iterations, meaning conflicts from position n are still considered when processing position n-1, n-2, etc.

The Fix: Reset the tracking variables for each position:

for current_node in range(n, 0, -1):
    # Reset for each position - THIS IS THE FIX
    smallest_conflict = n + 1
    second_smallest_conflict = n + 1
  
    # Update smallest and second smallest conflicting nodes
    for conflict_node in graph[current_node]:
        if conflict_node < smallest_conflict:
            second_smallest_conflict = smallest_conflict
            smallest_conflict = conflict_node
        elif conflict_node < second_smallest_conflict:
            second_smallest_conflict = conflict_node
  
    # Rest of the logic remains the same

2. Misunderstanding the Problem's Counting Logic

The Pitfall: Developers might incorrectly assume that removing a conflicting pair [a, b] only affects subarrays containing both a and b. The actual impact is more nuanced - it affects all subarrays starting from any position up to a that would extend to or past b.

Correct Understanding:

  • When we remove a pair involving position b1, we extend the valid range from [current_node, b1-1] to [current_node, b2-1]
  • This gives us b2 - b1 additional valid subarrays starting at current_node
  • The count[b1] accumulates all such benefits across different starting positions

3. Edge Case: No Conflicts for a Position

The Pitfall: If a position has no conflicts (empty graph[current_node]), the smallest_conflict remains as n + 1, which is correct. However, developers might try to "optimize" by skipping such positions, which would miss counting valid subarrays.

Why It Matters: Even positions with no conflicts contribute to the answer. A position with no conflicts can form subarrays all the way to position n, contributing (n + 1) - current_node valid subarrays.

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

Which algorithm is best for finding the shortest distance between two points in an unweighted graph?


Recommended Readings

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

Load More