最小添加,使由 '{', '}', '[', ']', '(', ')' 组成的括号字符串有效

Minimum add to make parentheses string consisting of '{', '}', '[', ']', '(', ')' valid

提问人:John Mathews 提问时间:10/6/2020 更新时间:2/10/2022 访问量:837

问:

这个问题是对熟悉的堆栈问题(https://leetcode.com/problems/minimum-add-to-make-parentheses-valid/)的补充,在该问题中,我们必须返回最少的添加次数才能使括号字符串有效。但这个问题只包括“(”和“)”。如果我们将这个问题扩展到其他类型的括号,如'[', ']', '{', '}',会发生什么。我刚刚在朋友之间的讨论中遇到了这个问题,需要有关如何接近的帮助。

例如:[[[{{}]]){)}) -> [[[{{}}]]] (){()} 在这种情况下,答案是 5 个补充以使其有效。

我想不出一个合适的方法。我考虑的 2 种方法是:

  1. 与普通问题类似,当我们浏览字符串时,我们将开始类型 '(', '{', '[' 推送到堆栈中,如果我们找到关闭类型 ')', '}', ']',我们检查堆栈的顶部,如果它们相互补充,我们弹出并继续,否则我们增加计数器并继续而不会弹出。遍历字符串后,我们将答案输出为计数器和堆栈大小的总和。在这种方法中,上面的例子将不起作用,因为额外的“{”会破坏该方法。

  2. 另一种方法与上述类似,即。我们按下括号的开始类型,如果我们找到一个结束类型,并且堆栈的顶部与它相得益彰,我们弹出并继续字符串,否则我们将弹出,直到我们得到一个匹配的字符串,对于每次弹出,我们都会增加计数器。遍历字符串后,总值是计数器和堆栈大小的总和。但这不适用于像 {{{{]}}}} 这样的情况,其中字符 ']' 会弹出所有内容,并且会增加答案。

我也在考虑将这些结合起来,更像是一个动态编程,我们将最大限度地看到最大的价值,或者看到直到我们在堆栈中得到匹配或堆栈变为空。但我不确定这 2 种是否是唯一需要考虑的情况。

字符串 算法 数据结构 堆栈 动态规划

评论

0赞 user3386109 10/6/2020
我认为您的动态编程思想是正确的方法。我的建议是,您为每种打开类型维护一个计数器,以跟踪堆栈上当前有多少个。这样,当您找到关闭类型时,您就会知道堆栈上是否有匹配项。如果没有匹配项,那么唯一的选择是增加添加的数量,然后继续而不弹出。
0赞 John Mathews 10/6/2020
这是个好主意,但对于找到匹配的情况,我们将不得不将其弹出或在此处添加一个新角色,并找到哪个添加最少?在那种情况下,我认为它会变成 O(n^2),我猜。我将为此想出一个代码,然后我将尝试使用一些测试用例来破坏它。我对这种方法唯一持怀疑态度的部分是证明它总是有效的。
0赞 user3386109 10/6/2020
是的,如果有匹配项,代码需要尝试两个选项:要么弹出它,要么添加一个新字符。时间的复杂性将取决于需要做出多少这样的决定。保留每种类型的计数器可减少决策数。

答:

0赞 Anatolii 5/4/2021 #1

解释

我们将逐个字符处理输入的字符串,并更新有关到目前为止遇到的括号的某些信息。对于每种支架类型,创建一个堆栈,用于保留未补偿的开放支架的位置。基本上,它表示在检查时需要多少个当前类型的右括号才能使字符串有效。

对于输入的每个括号,请执行下列操作之一:

  1. 如果支架是开口支架(任何类型),只需将其位置添加到相应的堆栈中即可。
  2. 否则,它是一个结束括号。如果堆栈中没有左括号,只需增加结果总和 - 可以立即补偿不平衡的右括号。
  3. 最后,它是一个右括号,在当前类型的堆栈中有左括号。因此,将位于相同类型的最后一个左括号和当前括号之间的其他类型的所有不平衡括号的数量相加!不要忘记从堆栈中删除匹配的元素。

最后,将每个堆栈的剩余大小添加到生成的总和中,因为每种类型的左括号可能仍然存在不平衡。

法典

我在 C++ 中创建了一个简单的解决方案,但如果需要,它可以很容易地转换为任何其他语言:

#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;
}

时间和空间复杂性

该解的时间和空间复杂度为 O(n)。

评论

0赞 kcsquared 2/10/2022
这似乎在输入时失败,例如 ,当只需要 4 次删除时返回 2。我认为贪婪的策略可能不适用于这个问题。{ ( } ) } [
0赞 Anatolii 2/10/2022
@kcsquared你是对的。这是错误的......
0赞 kcsquared 2/10/2022
顺便说一句,目前的另一个答案也是错误的。我正在发布一个动态编程解决方案,我用它来测试它;也许您可以扩展您的解决方案以做得更好?我认为可能是可能的。O(n^3)O(n^2)
0赞 Pulkit Sharma 10/21/2021 #2

贪婪地想想,对于每个结束标签,都必须有一个开始标签,如果不是这样,那么我们必须添加一个开始括号。

因此,我们可以通过遍历字符串并保留左括号的计数来找到最小加法。因此,当我们遇到左括号时,我们会增加计数变量,当我们遇到右括号时,如果我们有一些正数,我们会减少计数变量,否则,如果计数为零,则意味着我们必须在此处添加一个左括号。下面是这种贪婪方法的代码。时间复杂度 O(n) 和空间复杂度 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;
}

评论

0赞 Shridhar R Kulkarni 10/22/2021
我们需要照顾和吗?{[
0赞 Pulkit Sharma 10/22/2021
@ShridharRKulkarni很简单,你可以稍微扩展一下上面的逻辑,而不是一个变量,你可以使用 3 个单独的变量。我确实更新了我的代码,你可以看看它。
2赞 Alois Christen 10/22/2021
此解决方案不考虑定位:给出 0,但您至少需要 2 个括号来验证它( [ { [ { ) } ] } ]
0赞 kcsquared 2/10/2022 #3

您可以使用动态编程及时执行此操作(对于任意数量的支架类型)。这不是运行时的下限,但贪婪的方法似乎不适用于此问题。O(n^3)

首先,对于任何括号字符串,了解“平衡的最小添加量”与“平衡的最小删除量”相同是有帮助的,因为删除框架更易于使用。要了解为什么会这样,请考虑一组最小的添加:对于现在匹配但之前不匹配的每个括号,我们也可以删除该括号,反之亦然。

这个想法是计算所有可能的括号对:创建一个所有索引的列表,其中 和 是相同类型的开括号和闭括号对。然后,我们找到我们可以拥有的最大间隔数,使得任何两个间隔要么是嵌套的,要么是不相交的。这正是需要平衡的要求,如果你好奇的话,这意味着我们正在寻找由我们的区间形成的交集图的最大大小的简单完美子图。[i, j], 0 <= i < j < ns[i]s[j][i, j]

有间隔,因此对这种方法的任何修改都有一个下限。我们对这些间隔进行排序(按开始排序,如果并列,则按结束排序),并使用动态规划 (DP) 来查找我们可以拥有的嵌套或不相交间隔的最大数量。O(n^2)O(n^2)

我们的 DP 方程有 3 个参数:左、右和min_index。 是我们被允许使用的索引的包容性范围,并且是我们允许使用的最小索引(在我们的区间列表中)区间。如果我们知道我们可以实际使用的最左边的区间,比如说,答案将来自使用或不使用这个区间。如果我们不使用它,我们会得到.如果我们确实使用间隔,我们将可以嵌套在其中的最大间隔数相加,加上严格从 .这是。[left, right]smin_index[start, end]dp(left, right, min_index+1)(start, end)end1 + dp(start+1, end-1, min_index+1) + dp(end+1, right, min_index+1)

如需更完整的定义:

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.

下面是该算法的 Python 实现:

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)

例子:

( [ ( [ ) } ]     -->   3

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

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

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

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

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