布尔括号化问题(统计真值表达式为真的方法数)
题目描述
给定一个布尔表达式,由字符 'T'(真)、'F'(假)以及运算符 '&'(与)、'|'(或)、'^'(异或)组成。表达式中的符号是交替出现的,即格式为:符号 运算符 符号 运算符 ... 符号。例如,表达式 "T|F&T^F" 是有效的。
我们的目标是,通过给表达式添加括号来改变运算顺序,计算出表达式最终结果为 True 的方案数。由于添加括号的方式很多,我们需要使用区间动态规划来高效地统计所有可能的方法数。
解题过程
-
问题分析与状态定义
这个问题的核心在于,一个较长的表达式可以通过不同的方式被分割成左右两个子表达式,而整个表达式的结果取决于左右子表达式的结果以及连接它们的运算符。
因此,我们定义一个动态规划数组dp[i][j][result],其中:i和j是表达式字符串的索引(从0开始),表示我们考虑表达式的子串s[i..j](包含两端)。result是一个布尔值,我们用 0 代表 False,1 代表 True。dp[i][j][result]的值表示:在子表达式s[i..j]中,通过添加括号,能够计算出结果为result(True 或 False)的不同方法数。
-
确定基础情况(最小子问题)
最小的子表达式就是一个单一的字符('T' 或 'F'),它不包含任何运算符。对于这样的子表达式,结果是确定的:- 如果
s[i]是 'T',那么dp[i][i][1] = 1(有一种方法得到 True),dp[i][i][0] = 0(没有方法得到 False)。 - 如果
s[i]是 'F',那么dp[i][i][1] = 0,dp[i][i][0] = 1。
- 如果
-
状态转移方程(核心思想)
对于任意一个长度大于1的子表达式s[i..j],我们如何计算dp[i][j][result]呢?
关键在于,这个表达式最终一定会被某个运算符分割成左右两部分。这个运算符就是位于i和j之间的某个运算符。注意,在s[i..j]中,运算符出现在奇数索引位置上(如果假设索引从0开始)。因此,我们需要枚举所有可能的分割点
k,其中k是i和j之间的一个运算符的位置(即i < k < j,并且s[k]是 '&', '|', '^' 中的一个)。对于每一个分割点
k:- 左边子表达式是
s[i..k-1],其可能的结果我们记为left_result(可以是0或1)。 - 右边子表达式是
s[k+1..j],其可能的结果我们记为right_result(可以是0或1)。 - 中间的运算符是
op = s[k]。
整个表达式
s[i..j]的结果,是由left_result和right_result通过运算符op计算出来的。
我们需要遍历左边子表达式的所有可能结果(True/False)和右边子表达式的所有可能结果(True/False),然后根据运算符的真值表,来判断这个组合是否会产生我们想要的最终结果(即dp[i][j][result]中的result)。状态转移方程的一般形式为:
dp[i][j][result] +=对所有满足(left_result op right_result) == result的组合,dp[i][k-1][left_result] * dp[k+1][j][right_result]的求和。让我们具体化每个运算符的转移:
-
运算符 '&' (AND):只有左右都为真,结果才为真。
- 要得到 True (1):
(1 & 1) = 1
dp[i][j][1] += dp[i][k-1][1] * dp[k+1][j][1] - 要得到 False (0): 有三种情况:
(0 & 0)=0,(0 & 1)=0,(1 & 0)=0
dp[i][j][0] += dp[i][k-1][0] * dp[k+1][j][0] + dp[i][k-1][0] * dp[k+1][j][1] + dp[i][k-1][1] * dp[k+1][j][0]
- 要得到 True (1):
-
运算符 '|' (OR):只要有一个为真,结果就为真。
- 要得到 True (1): 有三种情况:
(1 | 1)=1,(1 | 0)=1,(0 | 1)=1
dp[i][j][1] += dp[i][k-1][1] * dp[k+1][j][1] + dp[i][k-1][1] * dp[k+1][j][0] + dp[i][k-1][0] * dp[k+1][j][1] - 要得到 False (0):
(0 | 0) = 0
dp[i][j][0] += dp[i][k-1][0] * dp[k+1][j][0]
- 要得到 True (1): 有三种情况:
-
运算符 '^' (XOR):左右不同为真,相同为假。
- 要得到 True (1):
(1 ^ 0) = 1,(0 ^ 1) = 1
dp[i][j][1] += dp[i][k-1][1] * dp[k+1][j][0] + dp[i][k-1][0] * dp[k+1][j][1] - 要得到 False (0):
(1 ^ 1) = 0,(0 ^ 0) = 0
dp[i][j][0] += dp[i][k-1][1] * dp[k+1][j][1] + dp[i][k-1][0] * dp[k+1][j][0]
- 要得到 True (1):
- 左边子表达式是
-
计算顺序
由于计算dp[i][j]时需要知道所有更短的区间[i, k-1]和[k+1, j]的结果,我们必须按照区间长度由小到大的顺序进行递推。- 设
n为表达式字符串的长度。 - 外层循环:遍历区间长度
len,从 1 开始,直到n。因为长度为1是基础情况。 - 内层循环:遍历区间的起始点
i,从 0 开始,直到i + len - 1 < n。- 计算区间的结束点
j = i + len - 1。 - 如果当前区间长度
len为 1,直接根据基础情况赋值。 - 如果
len > 1,则遍历所有可能的分割点k。k从i+1开始,到j-1结束,并且每次步进 2。因为运算符只出现在符号之间的位置(即索引为奇数的位置,如果符号在偶数索引)。
- 计算区间的结束点
- 设
-
最终结果
整个表达式对应的是字符串s[0..n-1]。我们想要的是结果为 True 的方法数,所以最终答案就是dp[0][n-1][1]。
总结
这个问题的精髓在于将一个大表达式分解为左右两个子表达式,并通过运算符的真值表来组合子表达式的可能结果。通过动态规划自底向上地计算所有子区间的结果,我们避免了重复计算,从而高效地得到了最终答案。