给你一个 有效的 布尔表达式,用字符串 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)。
-
表达式解析:首先,使用两个栈来处理表达式,一个用于存放操作数(0或1),另一个用于存放操作符('&', '|', '(', ')')1。
-
状态定义:在数字栈中,每个元素是一个二元组
(x, y)
,其中x
表示将当前子表达式的值变为 0 所需的最少操作次数,y
表示变为 1 所需的最少操作次数1。 -
状态转移:当遇到操作符时,根据操作符的类型('&' 或 '|')和操作数的状态,使用动态规划更新状态。例如,对于操作符 '&',如果两个操作数的状态分别为
(x1, y1)
和(x2, y2)
,则结果状态(x, y)
可以通过以下方式计算:x
可以是x1 + x2
(两个操作数都保持 0),x1 + y2
(第一个操作数变为 0,第二个操作数保持 1),或y1 + x2
(第一个操作数保持 1,第二个操作数变为 0)的最小值。y
可以是y1 + y2
(两个操作数都变为 1)1。
-
括号处理:遇到右括号时,表示一个子表达式的结束。此时,需要根据操作符和子表达式的状态来更新外部表达式的状态1。
-
最终结果:遍历完整个表达式后,数字栈顶的二元组
(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) 的空间。