题目大意
给定一个中缀表达式(所有数均为未知数),求填出未知数使得表达式的值为 $0$ 的方案数。
思路
对于中缀表达式,有如下想法:
- DFS 遍历直接做运算
- 转为后缀表达式用栈运算。
由于枚举方案数时间复杂度为 ,其中
为需要填空的数量。且方案一没有中间过程,无法对其进行优化。我们排除方案一。果断转为后缀表达式。
假如找到了一种算法求得了答案,倒过来推会发现,运算位置在后,就越能够决定答案的值。可以联系到树的依赖关系。
在考虑方案二的中间过程后缀表达式,显然是能够转化为表达式树,这样对树的叶子节点进行填充就可以了。
对于树的计数,有三种方法:
1. 暴力,时间复杂度为
,同上,可排除此方案。
2. 排列组合,当然这个没有任何排列组合可以判定这个式子的值是否为
,排除此方案。
3. 树形计数 DP,由于有子结构最优且父亲的值本身有子节点推导而来,所以这个方法可能可行。
由上述过程可知,我们应该在表达式树上树形计数 DP,由于节点值只有可能为二进制,即可能性较小,我们可以将其设为状态中的一维。考虑只有左右子树影响了本节点,所以定义时应当把一个子树看作一个整体作 DP。
这个时候我们可以定义出来 为以
为节点的子树,计算值为
时候的总方案数。
转移时可以用子树进行递推,下表为题目中的表格:
| 运算符 |
运算规则 |
|---|---|
| ⊕ |
0⊕0=0 1⊕0=1 0⊕1=1 1⊕1=0 |
|
|
即运算符也应当影响计算值,分类讨论左子树为多少,右子树为多少时转移给该节点的 $0$ 状态还是 状态即可。
最后答案输出因为根节点包含一切节点和自己,所以最终答案一定在根节点的 DP 值上,又问有多少种填法可以使得表达式的值为 ,所以答案应该为
。
注意:树形 DP 的时候,应该将子树看为一个叶子节点,不要管它的内部运算,你只需要知道现在它是多少即可。
综上,此题得解。
Code(含注释)
#include<bits/stdc++.h>
using namespace std;
int l;
string s;
int mp[200001],hp[200001],mpc,hpc;//中缀表达式位置和后缀表达式位置,定位表达式树。
struct Prior{
char op;
int id;
int prior(){//运算符优先级(特别的,左括号为 0)。
if(op=='(')return 0;
else if(op=='+')return 1;
else if(op=='*')return 2;
return -1;
}
Prior(char c,int idx){
op=c;id=idx;
}
};
stack<Prior>stk;
struct Node{
char op;
int lc,rc;
}tree[200001];
int pntc;
void Getexp(void){//求得后缀表达式
for(int i=1;i<=l;i++){
if(s[i]=='('){//左括号直接入栈。
stk.push(Prior('(',i));
}
else if(s[i]=='+'){
while(!stk.empty()&&stk.top().prior()>=1){//把所有的加号和乘号都弹走。
hp[++hpc]=stk.top().id;
stk.pop();
}
stk.push(Prior('+',i));
}
else if(s[i]=='*'){//把所有的乘号都弹走。
while(!stk.empty()&&stk.top().prior()>=2){
hp[++hpc]=stk.top().id;
stk.pop();
}
stk.push(Prior('*',i));
}
else{//一直弹一直爽,弹出第一个左括号时停止弹出。
while(!stk.empty()&&stk.top().op!='('){
hp[++hpc]=stk.top().id;
stk.pop();
}
stk.pop();
}
}
while(!stk.empty()){
hp[++hpc]=stk.top().id;
stk.pop();
}
}
int zh[100001];
int BuildTree(int lm,int rm,int lh,int rh){//建立表达式树,我用的是中缀后缀确定唯一的树。
if(lm>rm||lh>rh)return 0;
int rt=hp[rh],mrt=zh[rt];
tree[++pntc].op=s[rt];
int rid=pntc;
tree[rid].rc=BuildTree(mrt+1,rm,rh-(rm-mrt),rh-1);//右子树
tree[rid].lc=BuildTree(lm,mrt-1,lh,rh-(rm-mrt)-1);//左子树
return rid;
}
int dp[100001][2];//i节点为0/1的可能数
void DP(int u){
if(u==0){
dp[0][0]=dp[0][1]=1;
return;
}
DP(tree[u].lc);
DP(tree[u].rc);
//下面的转移方程根据表格可以得到。
if(tree[u].op=='+'){
dp[u][0]=dp[tree[u].lc][0]*dp[tree[u].rc][0]%10007;
dp[u][1]=(dp[tree[u].lc][1]*dp[tree[u].rc][0]%10007+dp[tree[u].lc][0]*dp[tree[u].rc][1]%10007+dp[tree[u].lc][1]*dp[tree[u].rc][1]%10007)%10007;
}
else{
dp[u][1]=dp[tree[u].lc][1]*dp[tree[u].rc][1]%10007;
dp[u][0]=(dp[tree[u].lc][1]*dp[tree[u].rc][0]%10007+dp[tree[u].lc][0]*dp[tree[u].rc][1]%10007+dp[tree[u].lc][0]*dp[tree[u].rc][0]%10007)%10007;
}
return;
}
int main(){
ios::sync_with_stdio(false);
cin.tie(0);cout.tie(0);
// freopen("exp.in","r",stdin);
// freopen("exp.out","w",stdout);
cin>>l>>s;
s=' '+s;
//中序遍历
for(int i=1;i<=l;i++){
if(s[i]=='+'||s[i]=='*'){
mp[++mpc]=i;zh[i]=mpc;
}
}
Getexp();
BuildTree(1,mpc,1,hpc);
DP(1);
cout<<dp[1][0];//根节点即为总值,由题可知。
return 0;
}

被折叠的 条评论
为什么被折叠?



