10

This problem is an addition to the familiar stack question(https://leetcode.com/problems/minimum-add-to-make-parentheses-valid/) where we have to return the minimum number of additions to make the parentheses string valid. But that question consists of only '(' and ')'. What will happen if we extend that question to other types of parentheses like '[', ']', '{', '}'. I just came across this in a discussion among my friends and need help on how to approach.

For example: [[[{{}]]){)}) -> [[[{{}}]]] (){()}() in this case answer is 5 additions to make it valid.

I couldn't come up with a proper approach. 2 approaches I considered are:

  1. Similar to normal question, we push the opening types '(', '{', '[' to the stack as we browse through the string and if we find closing type ')', '}', ']' we check the top of the stack, if they both compliment each other, we pop and continue else we increment the counter and continue without popping out. After traversing the string, we output the answer as sum of counter and stack's size. In this approach the above example will not work as that extra '{' will break the approach.

  2. Another approach is similar to above ie. we push the opening type of parentheses and if we find a closing type and if the stack's top compliment it, we pop and continue with the string, else we will pop out till we get a matching string and for every pop we increment the counter. After traversing the string, the total value is sum of counter and stack's size. But that will not work for cases like {{{{]}}}} where the character ']' will pop out everything and it will increase the answer.

I was also thinking of combining these, more like a Dynamic Programming where we will take the maximum of either seeing the top most value or seeing till we get a match in the stack or if stack becomes empty. But I am not sure on whether these 2 are the only cases to consider.

John Mathews
  • 101
  • 2
  • I think that your dynamic programming idea is the right approach. My suggestion is that you maintain a counter for each opening type that keeps track of how many are currently on the stack. That way, when you find a closing type, you'll know if there's a match for it on the stack. If there is no match, then the only choice is to increment the number of additions, and continue without popping. – user3386109 Oct 06 '20 at 05:44
  • That is a good idea but for the match found case, we will have to pop it out or add a new character here and find which is giving minimum additions? In that case I think it will become O(n^2) I guess. I will come up with a code for that and then I will try to break it using some test case. The only part I am skeptical on this approach is proving that it always works. – John Mathews Oct 06 '20 at 05:54
  • Yup, if there is a match, the code needs to try both options: either pop it out, or add a new character. The time complexity will depend on how many of those decisions need to be made. Keeping the counters for each type reduces the number of decisions. – user3386109 Oct 06 '20 at 06:07

3 Answers3

0

Explanation

We will process our input string character by character and update certain information about brackets encountered so far. For each bracket type, create a stack that keeps positions of uncompensated opening brackets. Basically, it says how many closing brackets of the current type are needed to make the string valid at the time of checking.

For each bracket of the input, do one of the following:

  1. If the bracket is an opening one (any type), just add its position to the corresponding stack.
  2. Otherwise, it's a closing bracket. If there are no opening brackets in the stack, just increment the resulting sum - unbalanced closing bracket can be compensated right away.
  3. Finally, it's a closing bracket and there are opening brackets in the stack of the current type. So, add the number of all unbalanced brackets of the other types that are located between the last opening bracket of the same type and the current bracket! Don't forget to remove the matching elements from the stacks.

At the end, add a remaining size of each stack to the resulting sum because there may still be unbalanced opening brackets of each type.

Code

I created a simple solution in C++, but it can be easily converted to any other language if needed:

#include <iostream>
#include <stack>
#include <unordered_map>

bool isOpeningBracket(char bracket) {
    return bracket == '(' || bracket == '[' || bracket == '{';
}

int main() {
    std::string line;
    std::cin >> line;
    std::unordered_map<char, char> closingToOpeningBracket = {
            {')', '('},
            {']', '['},
            {'}', '{'}
    };

    std::unordered_map<char, std::unique_ptr<std::stack<uint64_t>>> bracketsMap;
    bracketsMap['{'] = std::make_unique<std::stack<uint64_t>>();
    bracketsMap['['] = std::make_unique<std::stack<uint64_t>>();
    bracketsMap['('] = std::make_unique<std::stack<uint64_t>>();
    uint64_t addOperations = 0;

    for(auto i = 0; i < line.size(); i++) {
        auto bracket = line[i];
        bool isOpening = isOpeningBracket(bracket);

        auto key = bracket;
        if (!isOpening) {
            key = closingToOpeningBracket[bracket];
        }
        auto &bracketStack = bracketsMap[key];
        if (isOpening) {
            bracketStack->push(i);
        } else if (!bracketStack->empty()) {
            auto openingBracketPosition = bracketStack->top();
            bracketStack->pop();

            for (auto & [key, value] : bracketsMap) {
                while (!value->empty() && value->top() > openingBracketPosition) {
                    addOperations++;
                    value->pop();
                }
            }
        } else {
            addOperations++;
        }
    }

    for (auto & [key, value] : bracketsMap) {
        addOperations += value->size();
    }

    std::cout << addOperations << "\n";

    return 0;
}

Time and Space Complexity

The time and space complexity of this solution is O(n).

Anatolii
  • 14,139
  • 4
  • 35
  • 65
  • This seems to fail on input like `{ ( } ) } [`, returning 4 when only 2 removals are needed. I think the greedy strategy might not work for this problem. – kcsquared Feb 09 '22 at 16:37
  • @kcsquared you're right. It's wrong... – Anatolii Feb 09 '22 at 16:52
  • The other current answer is also wrong, by the way. I'm in the process of posting an `O(n^3)` dynamic programming solution which I used to test this; perhaps you can extend your solution to do better than this? I think `O(n^2)` might be possible. – kcsquared Feb 09 '22 at 17:01
0

Just Think Greedily, for every closing tag there must be an opening tag if it is not so then we have to add an opening bracket.

So we can find the minimum add by iterating over the string and keeping the count of opening brackets. So when we encounter an opening bracket we increase our count variable and when we encounter a closing bracket we decrease our count variable if we have some positive count otherwise if the count is zero it means that we have to add an opening bracket here. Below is the code for this greedy approach. Time Complexity O(n) and Space Complexity O(1).

int minAddToMakeValid(string s) {
    int cnt1  = 0 , cnt2 = 0, cnt3 = 0,  ans = 0;
    for(char ch : s){
        if(ch == '(')cnt1++;
        else if(ch == ')'){
            if(cnt1==0)ans++;
            else cnt1--;
        }
        if(ch == '{')cnt2++;
        else if(ch == '}'){
            if(cnt2==0)ans++;
            else cnt2--;
        }
        if(ch == '[')cnt3++;
        else if(ch == ']'){
            if(cnt3==0)ans++;
            else cnt3--;
        }
    }
    return ans + cnt1 + cnt2 + cnt3;
}
Pulkit Sharma
  • 390
  • 2
  • 14
0

You can do this in O(n^3) time (for any number of bracket types) with dynamic programming. This is not a lower bound on the runtime, but it appears that a greedy approach doesn't work for this problem.

First, it's helpful to realize that the 'minimum additions to balance' is the same as the 'minimum deletions to balance' for any bracket string, since the deletion framework is easier to work with. To see why this is true, consider a minimum set of additions: for every bracket that is now matched but was unmatched before, we could have also deleted that bracket, and vice versa.

The idea is to compute all possible bracket pairs: create a list of all indices [i, j], 0 <= i < j < n, where s[i] and s[j] are an open and closed bracket pair of the same type. Then, we find the maximum number of intervals [i, j] we can have, such that any two intervals are either nested or disjoint. This is exactly the requirements to be balanced, and, if you're curious, means that we're looking for the maximum size trivially perfect subgraph of the intersection graph formed by our intervals.

There are O(n^2) intervals, so any modification of this approach has an O(n^2) lower bound. We sort these intervals (by start, then by end if tied), and use dynamic programming (DP) to find the maximum number of nested or disjoint intervals we can have.

Our DP equation has 3 parameters: left, right, and min_index. [left, right] is an inclusive range of indices of s we are allowed to use, and min_index is the smallest index (in our interval list) interval we are allowed to use. If we know the leftmost interval that we can feasibly use, say, [start, end], the answer will come from either using or not using this interval. If we don't use it, we get dp(left, right, min_index+1). If we do use the interval, we add the maximum number of intervals we can nest inside (start, end), plus the maximum number of intervals starting strictly after end. This is 1 + dp(start+1, end-1, min_index+1) + dp(end+1, right, min_index+1).

For a fuller definition:

dp(left, right, min_index) := 
maximum number of intervals from interval_list[min_index:]
that are contained in [left, right] and all pairwise nested or disjoint.

Also, let 
first_index := max(smallest index of an interval starting at or after left,
                  min_index)
so that interval_list[first_index] = (first_start, first_end).


dp(left, right, min_index) = 0 if (left > right or first_index >= length(interval_list)),

                             max(dp(left, right, first_index+1),
                                 1
                                 + dp(first_start+1, first_end-1, first_index+1)
                                 + dp(first_end+1, right, first_index+1))

                                 otherwise.

Here's a Python implementation of the algorithm:

def balance_multi_string(s: str) -> int:
    """Given a multi-paren string s, return minimum deletions to balance
       it. 'Balanced' means all parentheses are matched, and
       all pairs from different types are either nested or disjoint
       Runs in O(n^3) time.
    """

    open_brackets = {'{', '[', '('}
    closed_brackets = {'}', ']', ')'}
    bracket_partners = {'{': '}', '[': ']', '(': ')',
                        '}': '{', ']': '[', ')': '('}
    
    n = len(s)

    bracket_type_to_open_locations = collections.defaultdict(list)

    intervals = []

    for i, x in enumerate(s):
        if x in closed_brackets:
            for j in bracket_type_to_open_locations[bracket_partners[x]]:
                intervals.append((j, i))
        else:
            bracket_type_to_open_locations[x].append(i)

    if len(intervals) == 0:
        return n

    intervals.sort()
    num_intervals = len(intervals)

    @functools.lru_cache(None)
    def point_to_first_interval_strictly_after(point: int) -> int:
        """Given a point, return index of first interval starting
        strictly after, or num_intervals if there is none."""
        if point > intervals[-1][0]:
            return num_intervals
        if point < intervals[0][0]:
            return 0
        return bisect.bisect_right(intervals, (point, n + 2))

    @functools.lru_cache(None)
    def dp(left: int, right: int, min_index: int) -> int:
        """Given inclusive range [left,right], and minimum interval index,
        return the maximum number of intervals we can add
        within this range so that all added intervals
        are either nested or disjoint."""
        if left >= right or min_index >= num_intervals:
            return 0
        starting_idx = max(point_to_first_interval_strictly_after(left - 1), min_index)
        if starting_idx == num_intervals or intervals[starting_idx][0] >= right:
            return 0
        first_start, first_end = intervals[starting_idx]
        best_answer = dp(first_start, right, starting_idx + 1)  # Without first interval

        if first_end <= right:  # If we include the first interval
            best_answer = max(best_answer,
                              1
                              + dp(first_start + 1, first_end - 1, starting_idx + 1)
                              + dp(first_end + 1, right, starting_idx + 1))
        return best_answer

    return n - 2 * dp(0, n - 1, 0)

Examples:

( [ ( [ ) } ]     -->   3

} ( } [ ) [ { }   -->   4

} ( } } ) ] ) {   -->   6

{ ) { ) { [ } }   -->   4

) ] } { } [ ( {   -->   6

] ) } } ( [ } {   -->   8
kcsquared
  • 5,244
  • 1
  • 11
  • 36