LeetCode 1896. 反转表达式值的最少操作次数

1896. 反转表达式值的最少操作次数

给你一个 有效的 布尔表达式,用字符串 expression 表示。这个字符串包含字符 '1''0''&'(按位  运算),'|'(按位  运算),'(' 和 ')' 。

  • 比方说,"()1|1" 和 "(1)&()" 不是有效 布尔表达式。而 "1", "(((1))|(0))" 和 "1|(0&(1))" 是 有效 布尔表达式。

你的目标是将布尔表达式的  反转 (也就是将 0 变为 1 ,或者将 1 变为 0),请你返回达成目标需要的 最少操作 次数。

  • 比方说,如果表达式 expression = "1|1|(0&0)&1" ,它的  为 1|1|(0&0)&1 = 1|1|0&1 = 1|0&1 = 1&1 = 1 。我们想要执行操作将 新的 表达式的值变成 0 。

可执行的 操作 如下:

  • 将一个 '1' 变成一个 '0' 。
  • 将一个 '0' 变成一个 '1' 。
  • 将一个 '&' 变成一个 '|' 。
  • 将一个 '|' 变成一个 '&' 。

注意:'&' 的 运算优先级 与 '|' 相同 。计算表达式时,括号优先级 最高 ,然后按照 从左到右 的顺序运算。

示例 1:

输入:expression = "1&(0|1)"
输出:1
解释:我们可以将 "1&(0|1)" 变成 "1&(0&1)" ,执行的操作为将一个 '|' 变成一个 '&' ,执行了 1 次操作。
新表达式的值为 0 。

示例 2:

输入:expression = "(0&0)&(0&0&0)"
输出:3
解释:我们可以将 "(0&0)&(0&0&0)" 变成 "(0|1)|(0&0&0)" ,执行了 3 次操作。
新表达式的值为 1 。

示例 3:

输入:expression = "(0|(1|0&1))"
输出:1
解释:我们可以将 "(0|(1|0&1))" 变成 "(0|(0|0&1))" ,执行了 1 次操作。
新表达式的值为 0 。

提示:

  • 1 <= expression.length <= 10^5
  • expression 只包含 '1''0''&''|''(' 和 ')'
  • 所有括号都有与之匹配的对应括号。
  • 不会有空的括号(也就是说 "()" 不是 expression 的子字符串)。

提示 1

How many possible states are there for a given expression?


提示 2

Is there a data structure that we can use to solve the problem optimally?

解法1:表达式解析 + 动态规划

如果我们只需要对表达式进行解析,那么在数字栈中,我们存放表达式的值即可。

然而本题需要我们通过最少的操作次数,将表达式的值反转,即 0 变成 1,1 变成 0,因此我们可以考虑在数字栈中多存放一些值。一种解决方法是:

我们在数字栈中存放一个二元组 (x,y),其中 x 表示将对应表达式的值变为 0,需要的最少操作次数,y 表示将对应表达式的值变为 1,需要的最少操作次数。
那么我们只需要修改表达式解析中的四个部分:

显然,单个的0对应于状态(0,1),而单个的1对应于状态(1,0)。

  • 如果我们遇到一个 0,原先我们会将 0 入数字栈,而此时我们需要将二元组 (0,1) 入数字栈。因为 0 就是 0,而将 0 变成 1 需要一次操作;
  • 如果我们遇到一个 1,原先我们会将 1 入数字栈,而此时我们需要将二元组 (1,0) 入数字栈。因为 1 就是 1,而将 1 变成 0 需要一次操作;

本题中,除括号外所有运算符优先级相同,需要从左到右进行运算,因此我们每得到一个新的“操作数”(这里既包括由单个的0或1带来的“操作数”,也包括)导致的出栈情形——对于上一层来说,这一层带来了一个新的“操作数”),就应当在上一个操作符不为 ( 时将当前的“操作数”与上一个“操作数”进行一次“运算”,合并为一个新的“操作数”。

如果我们需要取出数字栈顶的两个二元组(原先是元素)以及符号栈顶的 & 与运算符进行运算,原先我们只需要将两个元素进行与运算,再将结果放回数字栈即可,而此时我们需要对两个二元组进行与运算:

设两个二元组分别为 (x 1 ​ ,y 1 ​ ) 以及 (x 2 ​ ,y 2 ​ )。

根据与运算的性质,只有 1 & 1 = 1,其余情况均为 0,因此我们得到的二元组 (x and ​ ,y and ​ ) 有状态转移方程:

如果我们需要取出数字栈顶的两个二元组(原先是元素)以及符号栈顶的 | 或运算符进行运算,原先我们只需要将两个元素进行或运算,再将结果放回数字栈即可,而此时我们需要对两个二元组进行或运算:

设两个二元组分别为 (x 1 ​ ,y 1 ​ ) 以及 (x 2 ​ ,y 2 ​ )。

根据或运算的性质,只有 0 | 0 = 0,其余情况均为 1,因此我们得到的二元组 (x or ​ ,y or ​ ) 有状态转移方程:

根据题目描述,我们可以使用一次操作将 & 变成 |,或者将 | 变成 &,因此 x and ​ 还可以从 x or ​ +1 转移而来,其它的情况类似。

因此,根据符号栈顶的符号,我们会选择:

或者:

进行状态转移。

当我们完成修改后的表达式解析时,符号栈为空,数字栈中恰好有一个二元组 (x,y),其中 x 表示将整个表达式的值变为 0 最少需要的操作次数,y 表示将整个表达式的值变为 1 最少需要的操作次数。然而我们并不知道表达式的值究竟是 0 还是 1,因此不能确定是返回 x 和 y 作为答案。

然而我们发现,由于动态规划中的状态转移一定是最优的,因此如果表达式原本的值为 0,那么 x 的值一定为 0,答案为 y;如果表达式原本的值为 1,y 的值一定为 0,答案为 x。因此,最终的答案即为 max(x,y)。

  1. 表达式解析:首先,使用两个栈来处理表达式,一个用于存放操作数(0或1),另一个用于存放操作符('&', '|', '(', ')')1。

  2. 状态定义:在数字栈中,每个元素是一个二元组 (x, y),其中 x 表示将当前子表达式的值变为 0 所需的最少操作次数,y 表示变为 1 所需的最少操作次数1。

  3. 状态转移:当遇到操作符时,根据操作符的类型('&' 或 '|')和操作数的状态,使用动态规划更新状态。例如,对于操作符 '&',如果两个操作数的状态分别为 (x1, y1)(x2, y2),则结果状态 (x, y) 可以通过以下方式计算:

    • x 可以是 x1 + x2(两个操作数都保持 0),x1 + y2(第一个操作数变为 0,第二个操作数保持 1),或 y1 + x2(第一个操作数保持 1,第二个操作数变为 0)的最小值。
    • y 可以是 y1 + y2(两个操作数都变为 1)1。
  4. 括号处理:遇到右括号时,表示一个子表达式的结束。此时,需要根据操作符和子表达式的状态来更新外部表达式的状态1。

  5. 最终结果:遍历完整个表达式后,数字栈顶的二元组 (x, y) 表示将整个表达式的值变为 0 和 1 的最少操作次数。返回 max(x, y) 作为最终答案。

Java版:

class Solution {
    public int minOperationsToFlip(String expression) {
        Deque<Character> opStack = new ArrayDeque<>();
        Deque<int[]> numStack = new ArrayDeque<>();
        for (int i = 0; i < expression.length(); i++) {
            char ch = expression.charAt(i);
            if ("(|&".indexOf(ch) != -1) {
                opStack.push(ch);
            } else if (ch == '0') {
                numStack.push(new int[]{0, 1});
                calc(opStack, numStack);
            } else if (ch == '1') {
                numStack.push(new int[]{1, 0});
                calc(opStack, numStack);
            } else if (ch == ')') {
                opStack.pop();
                calc(opStack, numStack);
            }
        }
        int[] arr = numStack.pop();
        return arr[0] != 0 ? arr[0] : arr[1];
    }

    private void calc(Deque<Character> opStack, Deque<int[]> numStack) {
        if (numStack.size() < 2 || opStack.peek() == '(') {
            return;
        }
        int[] p = numStack.pop();
        int[] q = numStack.pop();
        int[] r_and = op_and(p, q);
        int[] r_or = op_or(p, q);
        char op = opStack.pop();
        if (op == '&') {
            numStack.push(new int[]{Math.min(r_and[0], 1 + r_or[0]), Math.min(r_and[1], 1 + r_or[1])});
        } else {
            numStack.push(new int[]{Math.min(r_or[0], 1 + r_and[0]), Math.min(r_or[1], 1 + r_and[1])});
        }
    }

    private int[] op_and(int[] p, int[] q) {
        return new int[]{Math.min(p[0] + q[0], Math.min(p[0] + q[1], p[1] + q[0])), p[1] + q[1]};
    }

    private int[] op_or(int[] p, int[] q) {
        return new int[]{p[0] + q[0], Math.min(p[0] + q[1], Math.min(p[1] + q[0], p[1] + q[1]))};
    }
}

 Python3版:

class Solution:
    def minOperationsToFlip(self, expression: str) -> int:
        numStack = list()
        opStack = list()

        def op_and(x1, y1, x2, y2) -> (int, int):
            return (min(x1 + x2, x1 + y2, y1 + x2), y1 + y2)
        
        def op_or(x1, y1, x2, y2) -> (int, int):
            return (x1 + x2, min(x1 + y2, y1 + x2, y1 + y2))
        
        def calc():
            if len(numStack) >= 2 and opStack[-1] in ['|', '&']:
                x2, y2 = numStack.pop()
                x1, y1 = numStack.pop()
                x_and, y_and = op_and(x1, y1, x2, y2)
                x_or, y_or = op_or(x1, y1, x2, y2)

                if opStack[-1] == '|':
                    numStack.append((min(x_or, 1 + x_and), min(y_or, 1 + y_and)))
                else:
                    numStack.append((min(x_and, 1 + x_or), min(y_and, 1 + y_or)))
                opStack.pop()
        
        for ch in expression:
            if ch in ['(', '|', '&']:
                opStack.append(ch)
            elif ch == '0':
                numStack.append((0, 1))
                calc()
            elif ch == '1':
                numStack.append((1, 0))
                calc()
            elif ch == ')':
                # 此时符号栈栈顶一定是左括号
                opStack.pop()
                calc()
        return max(numStack.pop())

复杂度分析

  • 时间复杂度:O(n),其中 n 是字符串 expression 的长度。
  • 空间复杂度:O(n)。在最坏情况下,数字栈和符号栈需要使用 O(n) 的空间。

解法2:表达式解析 + 动态规划

如果当前操作符为&,则:

  • 我们如果要得到0,只需要有一边为0,代价为min(x1​,x2​)。
  • 我们如果要得到1,需要左右两边同时为1,代价为y1​+y2​;或者将操作符变为|,同时只需要左右有一边为1,代价为min(y1​,y2​)+1。

如果当前操作符为|,则:

  • 我们如果要得到0,需要左右两边同时为0,代价为x1​+x2​;或者将操作符变为&,同时只需要左右有一边为0,代价为min(x1​,x2​)+1。
  • 我们如果要得到1,只需要有一边为1,代价为min(y1​,y2​)。

这样我们就实现了操作数之间的运算。

所有操作执行完毕后,我们的操作数栈将只包含一个元素。这个元素必定包含一个零值(对应于表达式原本的值)和一个非零值。而这个非零值就是我们要寻找的答案。

Java版:

class Solution {
    public int minOperationsToFlip(String expression) {
        Deque<Character> opStack = new ArrayDeque<>();
        Deque<int[]> numStack = new ArrayDeque<>();
        for (int i = 0; i < expression.length(); i++) {
            char ch = expression.charAt(i);
            if ("(|&".indexOf(ch) != -1) {
                opStack.push(ch);
            } else if (ch == '0') {
                numStack.push(new int[]{0, 1});
                calc(opStack, numStack);
            } else if (ch == '1') {
                numStack.push(new int[]{1, 0});
                calc(opStack, numStack);
            } else if (ch == ')') {
                opStack.pop();
                calc(opStack, numStack);
            }
        }
        int[] arr = numStack.pop();
        return arr[0] != 0 ? arr[0] : arr[1];
    }

    private void calc(Deque<Character> opStack, Deque<int[]> numStack) {
        if (numStack.size() < 2 || opStack.peek() == '(') {
            return;
        }
        int[] p = numStack.pop();
        int[] q = numStack.pop();
        if (opStack.pop() == '&') {
            numStack.push(new int[]{Math.min(p[0], q[0]), Math.min(p[1] + q[1], 1 + Math.min(p[1], q[1]))});
        } else {
            numStack.push(new int[]{Math.min(p[0] + q[0], 1 + Math.min(p[0], q[0])), Math.min(p[1], q[1])});
        }
    }
}

Python3版:

class Solution:
    def minOperationsToFlip(self, expression: str) -> int:
        opStack = list()
        numStack = list()

        def calc():
            if len(numStack) >= 2 and opStack[-1] != '(':
                x2, y2 = numStack.pop()
                x1, y1 = numStack.pop()
                if opStack[-1] == '&':
                    numStack.append((min(x1, x2), min(y1 + y2, 1 + min(y1, y2))))
                else:
                    numStack.append((min(x1 + x2, 1 + min(x1, x2)), min(y1, y2)))
                opStack.pop()

        for ch in expression:
            if ch in '(|&':
                opStack.append(ch)
            elif ch == '0':
                numStack.append((0, 1))
                calc()
            elif ch == '1':
                numStack.append((1, 0))
                calc()
            elif ch == ')':
                opStack.pop()
                calc()
        return max(numStack.pop())

复杂度分析

  • 时间复杂度:O(n),其中 n 是字符串 expression 的长度。
  • 空间复杂度:O(n)。在最坏情况下,数字栈和符号栈需要使用 O(n) 的空间。
  • 34
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值