题目描述

✅ 面试题 08.14. 布尔运算

image-20260929011241041

题意分析

不改变数字和运算符的先后顺序,只通过括号改变布尔表达式的结合方式,统计结果等于 result 的方案数。不同分组即使算出相同布尔值,也要分别计数;运算符只有 &、|、^,操作数只有 0、1。

解法:枚举最后运算符并记忆子表达式计数

核心思路

[!blue]

任意完整的括号分组,都有一个最后执行的运算符。它把原表达式分成连续的左、右两部分;左右分别完成运算后,再由这个运算符合并。因此可以枚举每个运算符作为最后一步,把问题递归拆成两个更短的表达式。

dfs(s) 同时返回两个计数:res[0] 是子表达式结果为 0 的方案数,res[1] 是结果为 1 的方案数。父运算可能用到左右任一种结果,不能只计算最终希望得到的那一种。子表达式只有一个数字时,得到该数字的方案恰好为 1,另一结果为 0。

固定最后运算符后,枚举左右结果 i、j 的四种组合。左边有 left[i] 种分组,右边有 right[j] 种,它们可以独立搭配,所以共有 left[i] * right[j] 种组合。按当前运算符算出 v,把这个乘积加入 res[v];代码直接对 0、1 使用按位与、或、异或,结果就是对应的布尔值。

不同最后运算符的位置对应不同的最外层划分,因此各位置的贡献相加;每个完整方案又只有唯一的最后运算符和左右分组,所以不会重复计算同一方案。递归一直缩短表达式,最终到达单个数字,覆盖了全部分组方式。

不同划分会反复求解相同子表达式,用字符串内容作为键缓存这两个计数即可。相同内容在不同位置的计算规则相同,可以复用结果;这是复用计算,不是把不同位置的方案合并掉,它们仍在各自的父划分中参与乘法计数。

解题步骤

  1. 创建本次调用的记忆表,从完整表达式开始递归。
  2. 若子表达式已经缓存,直接返回两个计数;若只剩一个数字,返回该数字对应计数为 1 的数组。
  3. 枚举合法运算符位置,将左右子串分别递归求值。
  4. 枚举左右结果 0、1,计算当前运算结果,并累加两边方案数的乘积。
  5. 将当前子表达式的两个总计数缓存,最后取出 result 对应的次数。

代码实现

// 每个子表达式同时返回求值为 0 与 1 的方案数。
class Solution {
    private Map<String, int[]> memo;

    public int countEval(String s, int result) {
        memo = new HashMap<>();
        int[] answer = dfs(s);

        return result == 0 || result == 1 ? answer[result] : 0;
    }

    private int[] dfs(String s) {
        if (memo.containsKey(s)) {
            return memo.get(s);
        }

        int[] res = new int[2];

        if (s.length() == 1) {
            res[Integer.parseInt(s)] = 1;

            return res;
        }

        for (int k = 0; k < s.length(); ++k) {
            char op = s.charAt(k);

            if (op == '&' || op == '|' || op == '^') {
                int[] left = dfs(s.substring(0, k));
                int[] right = dfs(s.substring(k + 1));

                for (int i = 0; i < 2; ++i) {
                    for (int j = 0; j < 2; ++j) {
                        int v = 0;

                        if (op == '&') {
                            v = i & j;
                        } else if (op == '|') {
                            v = i | j;
                        } else if (op == '^') {
                            v = i ^ j;
                        }

                        res[v] += left[i] * right[j];
                    }
                }
            }
        }

        memo.put(s, res);

        return res;
    }
}
// 每个子表达式同时返回求值为 0 与 1 的方案数。
func countEval(s string, result int) int {
    memo := map[string][]int{}
    var dfs func(string) []int
    dfs = func(s string) []int {
        if v, ok := memo[s]; ok {
            return v
        }
        res := make([]int, 2)
        if len(s) == 1 {
            res[s[0]-'0'] = 1
            return res
        }
        for k, c := range s {
            if c == '0' || c == '1' {
                continue
            }
            left, right := dfs(s[:k]), dfs(s[k+1:])
            for i, v1 := range left {
                for j, v2 := range right {
                    v := 0
                    if c == '&' {
                        v = i & j
                    } else if c == '|' {
                        v = i | j
                    } else if c == '^' {
                        v = i ^ j
                    }
                    res[v] += v1 * v2
                }
            }
        }
        memo[s] = res
        return res
    }
    answer := dfs(s)
    if result == 0 || result == 1 {
        return answer[result]
    }
    return 0
}

复杂度分析

  • 时间复杂度:设操作数数量为 $n$,表达式长度也为 $O(n)$。子表达式最多有 $O(n^2)$ 个,每个枚举 $O(n)$ 个分割,四种真假组合是常数开销。当前实现还会构造子串并计算字符串哈希,单次可能花费 $O(n)$,因此包含字符串操作的时间上界为 $O(n^4)$。
  • 空间复杂度:Java 缓存的子串会保存各自字符内容,至多 $O(n^2)$ 个键、每个长 $O(n)$,空间上界为 $O(n^3)$;Go 子串共享原字符串底层内容,缓存键和计数占 $O(n^2)$。两者递归深度均为 $O(n)$。

关键点总结

[!green]

  • 按最后执行的运算符划分,左右分组独立,贡献相乘;不同划分的贡献相加。
  • 一个子表达式同时返回假、真两种计数,父层才能枚举完整真值组合。
  • 题面最多 19 个运算符,全部括号分组数不超过第 19 个卡特兰数 1767263190,当前整数计数可以容纳。

易错点总结

[!yellow]

  • 只能在运算符位置分割,不能把数字位置当成最后一步。
  • 不能固定按原运算优先级求值,题目允许括号改变结合顺序,需要枚举所有根划分。
  • 左右方案数应相乘,单独相加不能表示两边的全部搭配。
  • 布尔结果相同不代表括号方案相同,不能用结果集合去重。
  • 字符串键会产生构造或哈希开销,不能把下标区间缓存的复杂度直接套到当前代码上。

相似题目

题目 难度 关联与区别
241. 为运算表达式设计优先级 中等 同样枚举最后运算符,原题输出所有算术结果,本题把结果压缩为0和1的方案计数。
312. 戳气球 困难 同样通过选择最后一步拆成独立左右区间,原题取最优值,本题累计所有方案。
转载与许可
作者
链接 https://hgnulb.github.io/blog/2026/72532464
许可 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!