LeetCode 1096. 花括号展开 II

1096. 花括号展开 II

如果你熟悉 Shell 编程,那么一定了解过花括号展开,它可以用来生成任意字符串。

花括号展开的表达式可以看作一个由 花括号逗号 和 小写英文字母 组成的字符串,定义下面几条语法规则:

  • 如果只给出单一的元素 x,那么表达式表示的字符串就只有 "x"R(x) = {x}
    • 例如,表达式 "a" 表示字符串 "a"
    • 而表达式 "w" 就表示字符串 "w"
  • 当两个或多个表达式并列,以逗号分隔,我们取这些表达式中元素的并集。R({e_1,e_2,...}) = R(e_1) ∪ R(e_2) ∪ ...
    • 例如,表达式 "{a,b,c}" 表示字符串 "a","b","c"
    • 而表达式 "{{a,b},{b,c}}" 也可以表示字符串 "a","b","c"
  • 要是两个或多个表达式相接,中间没有隔开时,我们从这些表达式中各取一个元素依次连接形成字符串。R(e_1 + e_2) = {a + b for (a, b) in R(e_1) × R(e_2)}
    • 例如,表达式 "{a,b}{c,d}" 表示字符串 "ac","ad","bc","bd"
  • 表达式之间允许嵌套,单一元素与表达式的连接也是允许的。
    • 例如,表达式 "a{b,c,d}" 表示字符串 "ab","ac","ad"​​​​​​
    • 例如,表达式 "a{b,c}{d,e}f{g,h}" 可以表示字符串 "abdfg", "abdfh", "abefg", "abefh", "acdfg", "acdfh", "acefg", "acefh"

给出表示基于给定语法规则的表达式 expression,返回它所表示的所有字符串组成的有序列表。

假如你希望以「集合」的概念了解此题,也可以通过点击 “显示英文描述” 获取详情。

示例 1:

输入:expression = "{a,b}{c,{d,e}}"
输出:["ac","ad","ae","bc","bd","be"]

示例 2:

输入:expression = "{{a,z},a{b,c},{ab,z}}"
输出:["a","ab","ac","z"]
解释:输出中 不应 出现重复的组合结果。

提示:

  • 1 <= expression.length <= 60
  • expression[i] 由 '{''}'',' 或小写英文字母组成
  • 给出的表达式 expression 用以表示一组基于题目描述中语法构造的字符串

提示 1

You can write helper methods to parse the next "chunk" of the expression.

If you see eg. "a", the answer is just the set {a}.

If you see "{", you parse until you complete the "}" (the number of { and } seen are equal) and that becomes a chunk that you find where the appropriate commas are, and parse each individual expression between the commas.

解法1:递归解析

思路与算法

表达式可以拆分为多个子表达式,以逗号分隔或者直接相接。我们应当先按照逗号分割成多个子表达式进行求解,然后再对所有结果求并集。这样做的原因是求积的优先级高于求并集的优先级。

我们用 expr 表示一个任意一种表达式,用 term 表示一个最外层没有逗号分割的表达式,那么 expr 可以按照如下规则分解:

expr→term ∣ term,expr

其中的 ∣ 表示或者,即 expr 可以分解为前者,也可以分解为后者。

再来看 term, term 可以由小写英文字母或者花括号包括的表达式直接相接组成,我们用 item 来表示每一个相接单元,那么 term 可以按照如下规则分解:

term→item ∣ item term

item 可以进一步分解为小写英文字母 letter 或者花括号包括的表达式,它的分解如下:

item→letter ∣ {expr}

在代码中,我们编写三个函数,分别负责以上三种规则的分解:

  1. expr 函数,不断的调用 term,并与其结果进行合并。如果匹配到表达式末尾或者当前字符不是逗号时,则返回。
  2. term 函数,不断的调用 item,并与其结果求积。如果匹配到表达式末尾或者当前字符不是小写字母,并且也不是左括号时,则返回。
  3. item 函数,根据当前字符是不是左括号来求解。如果是左括号,则调用 expr,返回结果;否则构造一个只包含当前字符的字符串集合,返回结果。

以下示意图以 {a,b}{c,{d,e}} 为例,展示了表达式递归拆解以及回溯的全过程。

在代码实现过程中有以下细节:

维护一个外部指针来遍历整个表达式,或者将表达式和当前遍历下标以引用的方式传递给被调函数。
因为最终答案需要去重,所以可以先用集合来求解中间结果,最后再转换成已排序的列表作为最终答案。

解题思路

  1. 理解表达式结构:首先,我们需要理解表达式的结构。表达式由基本元素(小写字母)和花括号包含的子表达式组成。子表达式可以包含逗号分隔的多个选项,也可以与其他子表达式或基本元素相连。

  2. 分解表达式:表达式可以分解为更小的单元,这些单元可以是单个字符或花括号内的组合。我们需要递归地分解这些单元,直到无法进一步分解。

  3. 处理并集和积:表达式中的逗号表示并集(即取所有可能的选项),而连续的单元表示积(即所有单元的排列组合)。我们需要先处理并集,再处理积。

  4. 递归解析:通过递归地解析表达式,我们可以生成所有可能的字符串组合。递归的底部是遇到单个字符或花括号内的表达式已经完全解析。

  5. 去重和排序:由于递归过程中可能会生成重复的字符串,我们需要在最后将结果去重并排序,以满足题目要求的有序列表。

算法逻辑

  1. 分解表达式:使用递归函数 expr 来分解表达式。这个函数会处理表达式中的逗号,并将其拆分为更小的表达式 term

  2. 分解单元:在 expr 函数中,使用 term 函数来分解表达式中的单元。term 函数会处理连续的字符或花括号内的表达式。

  3. 处理花括号:在 term 函数中,如果遇到花括号,使用 item 函数来处理花括号内的表达式。item 函数会递归地解析花括号内的表达式,直到找到闭合的花括号。

  4. 生成组合:在 item 函数中,如果当前字符不是花括号,那么它就是一个基本元素,直接添加到结果集合中。如果是花括号,那么递归解析内部的表达式,并生成所有可能的选项。

  5. 回溯:在递归过程中,我们需要回溯到上一个函数调用,以便尝试所有可能的选项。这通过在 item 函数中添加和删除当前处理的字符或选项来实现。

  6. 去重和排序:在所有递归调用完成后,我们将结果集合转换为列表,并对其进行排序,以生成最终的有序列表。

Java版:

class Solution {
    private String expression;
    private int index;
    private int n;

    public List<String> braceExpansionII(String expression) {
        this.expression = expression;
        this.index = 0;
        this.n = expression.length();
        Set<String> ret = expr();
        return new ArrayList<String>(ret);
    }

    private Set<String> expr() {
        Set<String> ret = new TreeSet<>();
        while (true) {
            ret.addAll(term());
            if (index < n && expression.charAt(index) == ',') {
                index++;
                continue;
            } else {
                break;
            }
        }
        return ret;
    }

    private Set<String> term() {
        Set<String> ret = new TreeSet<>();
        ret.add("");
        while (index < n && (expression.charAt(index) == '{' || Character.isLowerCase(expression.charAt(index)))) {
            Set<String> sub = item();
            Set<String> tmp = new TreeSet<>();
            for (String left : ret) {
                for (String right : sub) {
                    tmp.add(left + right);
                }
            }
            ret = tmp;
        }
        return ret;
    }

    private Set<String> item() {
        Set<String> ret = new TreeSet<>();
        if (expression.charAt(index) == '{') {
            index++;
            ret = expr();
        } else {
            ret.add(String.valueOf(expression.charAt(index)));
        }
        index++;
        return ret;
    }
}

Python3版:
使用 nonlocal 关键字允许函数内部的 index 变量与方法外部的 index 绑定

class Solution:
    def braceExpansionII(self, expression: str) -> List[str]:
        index = 0
        n = len(expression)

        def expr() -> set:
            nonlocal index
            ret = set()
            while True:
                ret = ret.union(term())
                if index < n and expression[index] == ',':
                    index += 1
                    continue
                else:
                    break 
            return ret 
        

        def term() -> set:
            nonlocal index
            ret = {""}
            while index < n and (expression[index].islower() or expression[index] == '{'):
                sub = item()
                temp = set()
                for left in ret:
                    for right in sub:
                        temp.add(left + right)
                ret = temp 
            return ret

        def item() -> set:
            nonlocal index 
            ret = set()
            if expression[index] == '{':
                index += 1
                ret = expr()
            else:
                ret = {expression[index]}
            index += 1
            return ret 

        return sorted(list(expr()))

复杂度分析

  • 时间复杂度:O(nlogn),其中 n 是 expression 的长度。整个 expression 只会遍历一次,时间复杂度为 O(n),集合合并以及求积运算的时间复杂度为 O(nlogn),因此总的时间复杂度为 O(nlogn)。
  • 空间复杂度:O(n)。递归过程所需的栈空间为 O(n),以及存放中间答案的空间复杂度为 O(n),因此总的空间复杂度为 O(n)。

解法2:栈

思路与算法

如果把题目中的表达式并列关系看做是求和,把相接看做是求积,那么求解整个表达式的过程可以类比于求解中缀表达式的过程,例如:{a,b}{c,{d,e}} 可以看做是 {a,b}×{c+{d+e}}。

与求解中缀表达式一样,在遍历表达式的过程中我们需要用到两个栈,一个用来存放运算符(即加号和乘号,以及左大括号),另一个用来存运算对象(即集合)。

在本题中有一个特殊情况需要处理,就是乘号需要我们自己来添加,我们按照当前字符的种类来判断前面是否需要添加乘号:

  • 如果当前字符是 ‘‘{",并且前面是 ‘‘}" 或者小写英文字母时,需要添加乘号运算。
  • 如果当前字符是小写字母,并且前面是 ‘‘}" 或者是小写英文字母时,需要添加乘号运算。
  • 如果当前字符是 ‘‘," ,则前面一定不需要添加乘号运算。
  • 如果当前字符是 ‘‘}",则前面一定不需要添加乘号运算。

因此,只有当前字符是 ‘‘{" 或者小写字母时,才需要考虑是否在前面添加乘号。

接下来我们分析运算优先级的问题,在本题中只涉及加法和乘法两种运算。如果一个表达式同时有并列和相接,那我们应该先计算相接的结果,再计算并列的结果。因此,乘法的优先级要大于加法。

至此,我们可以按照如下流程来计算表达式的值:

  • 如果遇到 ‘‘,",则先判断运算符栈顶是否是乘号,如果是乘号则需要先计算乘法,直到栈顶不是乘号为止,再将加号放入运算符栈中。
  • 如果遇到 ‘‘{",则先判断是否需要添加乘号,再将 { 放入运算符栈。
  • 如果遇到 ‘‘}",则不断地弹出运算符栈顶,并进行相应的计算,直到栈顶为左括号为止。
  • 如果遇到小写字母,则先判断是否需要添加乘号,再构造一个只包含当前小写字母的字符串集合,放入集合栈中。

按照上述流程遍历完一次之后,由于题目给定的表达式中最外层可能没有大括号,例如 {a,b}{c,{d,e}},因此运算符栈中可能依然有元素,我们需要依次将他们弹出并进行计算。最终,集合栈栈顶元素即为答案。

算法逻辑:

  1. 初始化两个栈:一个用于存放运算符,另一个用于存放集合。

  2. 遍历表达式:对表达式中的每个字符进行遍历。

  3. 字符分类处理

    • 逗号(,:检查运算符栈顶元素,如果是乘号,则执行乘法运算直到栈顶不是乘号,然后压入加号。
    • 左大括号({:检查前一个字符,如果需要,则先压入乘号,然后压入左大括号。
    • 右大括号(}:连续弹出运算符栈顶元素,执行相应运算,直到遇到左大括号。
    • 小写字母:检查前一个字符,如果需要,则先压入乘号,然后将字母作为一个新集合压入集合栈。
  4. 运算符执行:根据运算符栈顶元素,执行加法或乘法运算。加法是集合的合并,乘法是集合的笛卡尔积。

  5. 循环结束处理:如果遍历结束后运算符栈不为空,继续执行运算直到栈为空。

  6. 结果输出:集合栈的栈顶元素即为最终结果,将其转换为排序后的列表返回。

Java版:

class Solution {
    public List<String> braceExpansionII(String expression) {
        Deque<Character> op = new ArrayDeque<>();
        List<Set<String>> stack = new ArrayList<>();

        for (int i = 0; i < expression.length(); i++) {
            char ch = expression.charAt(i);
            if (ch == ',') {
                // 不断地弹出栈顶运算符,直到栈为空或者栈顶不为乘号
                while (!op.isEmpty() && op.peek() == '*') {
                    ope(op, stack);
                }
                op.push('+');
            } else if (ch == '{') {
                // 首先判断是否需要添加乘号,再将 { 添加到运算符栈中
                if (i > 0 && (expression.charAt(i - 1) == '}' || Character.isLowerCase(expression.charAt(i - 1)))) {
                    op.push('*');
                }
                op.push('{');
            } else if (ch == '}') {
                // 不断地弹出栈顶运算符,直到栈顶为 {
                while (!op.isEmpty() && op.peek() != '{') {
                    ope(op, stack);
                }
                op.pop();
            } else {
                // 首先判断是否需要添加乘号,再将新构造的集合添加到集合栈中
                if (i > 0 && (expression.charAt(i - 1) == '}' || Character.isLowerCase(expression.charAt(i - 1)))) {
                    op.push('*');
                }
                String x = String.valueOf(ch);
                stack.add(new TreeSet<String>() {{
                    add(x);
                }});
            }
        }

        while (!op.isEmpty()) {
            ope(op, stack);
        }
        return new ArrayList<String>(stack.get(stack.size() - 1));
    }

    // 弹出栈顶运算符,并进行计算
    private void ope(Deque<Character> op, List<Set<String>> stack) {
        int l = stack.size() - 2;
        int r = stack.size() - 1;
        if (op.peek() == '+') {
            stack.get(l).addAll(stack.get(r));
        } else {
            Set<String> tmp = new TreeSet<>();
            for (String left : stack.get(l)) {
                for (String right : stack.get(r)) {
                    tmp.add(left + right);
                }
            }
            stack.set(l, tmp);
        }
        stack.remove(stack.size() - 1);
        op.pop();
    }
}

Python3版:

class Solution:
    def braceExpansionII(self, expression: str) -> List[str]:
        op = []
        stack = []

        def ope():
            if op[-1] == '+':
                stack[-2].update(stack[-1])
            else:
                tmp = set()
                for left in stack[-2]:
                    for right in stack[-1]:
                        tmp.add(left + right)
                stack[-2] = tmp 
            stack.pop()
            op.pop()


        for i, ch in enumerate(expression):
            if ch == ',':
                while op and op[-1] == '*':
                    ope()
                op.append('+')
            elif ch == '{':
                if i > 0 and (expression[i - 1] == '}' or expression[i - 1].islower()): 
                    op.append('*')
                op.append('{')
            elif ch == '}':
                while op and op[-1] != '{':
                    ope()
                op.pop()
            else:
                if i > 0 and (expression[i - 1] == '}' or expression[i - 1].islower()):
                    op.append('*')
                stack.append({expression[i]})
        
        while op:
            ope()
        return sorted(list(stack[-1]))

复杂度分析

  • 时间复杂度:O(nlogn),其中 n 是 expression 的长度。整个 expression 只会遍历一次,时间复杂度为 O(n),集合合并以及求积运算的时间复杂度为 O(nlogn),因此总的时间复杂度为 O(nlogn)。
  • 空间复杂度:O(n)。过程中用到了两个栈,他们都满足在任意时刻元素个数不超过 O(n),包含 n 个元素的集合的时间复杂度为 O(n),因此总的空间复杂度为 O(n)。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值