P1310 表达式的值
题目描述
给你一个带括号的布尔表达式,其中+
表示或操作|
,*
表示与操作&
,先算*
再算+
。但是待操作的数字(布尔值)不输入。
求能使最终整个式子的值为0的方案数。
题外话
不久之前我在codewars上做过一道类似的题目。
以及把它搬运到了洛谷上。
布尔表达式计数问题
考虑这样一个问题:
有两个布尔变量\(x\)和\(y\)。
我们知道使\(x\)等于1的方案有\(x_1\)种,等于0的方案有\(x_0\)种;使\(y\)等于1的方案有\(y_1\)种,等于0的方案有\(y_0\)种。
那么:
使\(x\&y\)为1的方案数?为0的方案数?
使\(x|y\)为1的方案数?为0的方案数?
使\(x\oplus y\)(通常我们使用\(\oplus\)表示异或)为1的方案数?为0的方案数?
不难发现:
使\(x\&y\)为1,那么\(x\)和\(y\)都要为1,所以方案数为\(x_1*y_1\)。
使\(x\&y\)为0,那么\(x\)和\(y\)不能都为1,所以方案数为\(x_1*y_0+x_0*y_1+x_0*y_0\)。
使\(x|y\)为1的方案数为\(x_1*y_1+x_0*y_1+x_1*y_0\),为0的方案数为\(x_0*y_0\)。
使\(x\oplus y\)为1的方案数为\(x_0*y_1+x_1*y_0\),为0的方案数为\(x_0*y_0+x_1*y_1\)。
表达式树,前缀表达式,中缀表达式,后缀表达式
表达式树
\((1+2)*4\)
如上图,每个叶节点是一个数字,其他节点都是(双目)运算符。
整棵树表示一个表达式。每个子树表示一个子表达式。
计算这个表达式的方式如下图。
所以值为12。
中序遍历
中序遍历这个表达式树,我们发现得到的结果几乎和原来的表达式一样。
只是需要加一些括号罢了。
处理方法:我们可以给每个子树前后都加一对括号。
前/后序遍历
称前序遍历得到的式子为前缀表达式,或者波兰表达式。称后序遍历得到的式子为后缀表达式,或者逆波兰表达式。
前缀表达式和后缀表达式都拥有一个优秀的性质:不需要括号。
(下面仅以后缀表达式为例)
比如上文的\((1+2)*4\),改为后缀表达式就是:\(1\ 2\ +\ 4\ *\)。
如何计算后缀表达式
我们可以用栈来处理:
遇到数字,入栈;遇到符号,从栈里取出两个数字,按照这个符号运算,然后把结果入栈。最后栈里剩下的就是结果。
\(1\ 2\ +\ 4\ *\)的计算过程如下:
1入栈 | 1 | |
2入栈 | 1 | 2 |
1 2出栈,相加得3,3入栈 | 3 | |
4入栈 | 3 | 4 |
3 4出栈,相乘得12,12入栈 | 12 |
所以答案是12。
如何转化为后缀表达式
你可以直接建树,跑后序遍历。
但是这样又不好写,又慢。
我们考虑用栈维护。
遍历中缀表达式:
遇到数字,直接放入答案序列
遇到左括号,入栈
- 遇到右括号,把栈顶到上一个左括号的元素依次出栈并放入答案序列
- 遇到乘号,入栈
- 遇到加号,从栈顶开始弹出这段连续的乘号,并放入答案序列,最后加号入栈
最后把栈里剩下的元素依次放入答案序列
为什么是正确的?
模拟\(1+1*2*(1+2)+3*2*(1*5)+1\)
说明 | 栈 | 答案序列 |
---|---|---|
1放入答案序列 | 1 | |
+入栈 | + | 1 |
1放入答案序列 | + | 11 |
*入栈 | +* | 11 |
2放入答案序列 | +* | 112 |
*入栈 | +** | 112 |
(入栈 | +**( | 112 |
1放入答案序列 | +**( | 1121 |
+入栈 | +**(+ | 1121 |
2放入答案序列 | +**(+ | 11212 |
出现),+出栈并放入答案序列,(出栈 | +** | 11212+ |
出现+,弹出栈顶的*并放入答案序列,然后+入栈 | ++ | 11212+** |
3放入答案序列 | ++ | 11212+**3 |
*入栈 | ++* | 11212+**3 |
2放入答案序列 | ++* | 11212+**32 |
*入栈 | ++** | 11212+**32 |
(入栈 | ++**( | 11212+**32 |
1放入答案序列 | ++**( | 11212+**321 |
*入栈 | ++**(* | 11212+**321 |
5放入答案序列 | ++**(* | 11212+**3215 |
出现),*出栈并放入答案序列,(出栈 | ++** | 11212+**3215* |
出现+,弹出栈顶的*并放入答案序列,然后+入栈 | +++ | 11212+**3215*** |
1放入答案序列 | +++ | 11212+**3215***1 |
剩余栈中元素放入答案序列 | 11212+**3215***1+++ |
所以答案是11212+**3215***1+++。
正确性?
\[ \begin{aligned} 11212+**3215***1+++&=112(12+)**32(15*)**1+++\\ &=1123**325**1+++\\ &=11(23*)*3(25*)*1+++\\ &=116*3(10)*1+++\\ &=1(16*)(3(10)*)1+++\\ &=16(30)1+++\\ &=16(31)++\\ &=1(37)+\\ &=38\\ \\ 1+1*2*(1+2)+3*2*(1*5)+1&=1+2*3+6*5+1\\ &=1+6+30+1\\ &=38\\ \end{aligned} \]
P1310题解
首先在输入的表达式的恰当位置插入未知变量,然后转为后缀表达式。当然也可以一边转,一边插入未知变量。
之后,我们计算这个后缀表达式的值。不过维护的信息不再是表达式的值,而是使表达式值为0或1的方案数。
注意到单个变量为0或1的方案数为1.
#include
using namespace std;
inline void read(int &num)
{
bool flag = 0;
num = 0;
char c = getchar();
while ((c < '0' || c > '9') && c != '-')
c = getchar();
if (c == '-')
{
flag = 1;
c = getchar();
}
num = c - '0';
c = getchar();
while (c >= '0' && c <= '9')
num = (num << 3) + (num << 1) + c - '0', c = getchar();
if (flag)
num *= -1;
}
inline void output(int num)
{
if (num < 0)
{
putchar('-');
num = -num;
}
if (num >= 10)
output(num / 10);
putchar(num % 10 + '0');
}
inline void outln(int num)
{
output(num);
puts("");
}
inline void outln(string str)
{
puts(str.c_str());
}
//以上为头文件和快读
const int mod = 10007;
const int N = 100001;
int n;
char str[N]; //输入的中缀表达式
stack sta; //转后缀表达式时使用的栈
string final; //后缀表达式(答案序列)
stack zero, one; //zero维护使表达式值为0的方案个数,one维护使表达式值为1的方案个数
int main()
{
read(n);
scanf("%s", str + 1);
final.push_back('n'); //后缀表达式最开始应该有一个未知变量
for (int i = 1; i <= n; i++)
{
if (str[i] == '(' || str[i] == '*') //遇到左括号或乘号,入栈
sta.push(str[i]);
if (str[i] == '+') //遇到加号,弹出栈顶的乘号,然后加号入栈
{
while (!sta.empty() && sta.top() == '*')
{
final.push_back(sta.top());
sta.pop();
}
sta.push(str[i]);
}
if (str[i] == ')') //右括号,把到上一个左括号的元素出栈放入答案序列
{
while (sta.top() != '(')
{
final.push_back(sta.top());
sta.pop();
}
sta.pop();
}
if (str[i] != '(' && str[i] != ')') //当不是左括号或者右括号时,应该插入一个未知变量
{
final.push_back('n');
}
}
while (!sta.empty()) //剩下的元素放入答案序列
{
final.push_back(sta.top());
sta.pop();
}
for (char c : final) //遍历后缀表达式,这里使用了c++11的写法,相当于
// for (int i = 0; i < final.size(); i++)
// { char c = final[i];
{
if (c == 'n') //单个变量,方案数为1
{
one.push(1);
zero.push(1);
}
else
{
//rone表示右操作数(即上文中的y)为1的方案数(即上文中的y1),rzero同理
int rone = one.top(), rzero = zero.top();
one.pop();
zero.pop();
//同理
int lone = one.top(), lzero = zero.top();
one.pop();
zero.pop();
if (c == '*') //与操作,为1需要都为1,为0需要不都为1
{
one.push(lone * rone % mod);
zero.push((lone * rzero % mod + lzero * rone % mod + lzero * rzero % mod) % mod);
}
else //或操作,为0需要都为0,为1需要不都为0
{
zero.push(lzero * rzero % mod);
one.push((lone * rzero % mod + lzero * rone % mod + lone * rone % mod) % mod);
}
}
}
outln(zero.top());//需要整个表达式的值为0
}