题解:LuoguP1310 [NOIP 2011 普及组] 表达式的值

题目大意

给定一个中缀表达式(所有数均为未知数),求填出未知数使得表达式的值为 $0$ 的方案数。

思路

对于中缀表达式,有如下想法:

  1. DFS 遍历直接做运算
  2. 转为后缀表达式用栈运算。

由于枚举方案数时间复杂度为 $O(2^n)$,其中 $n$ 为需要填空的数量。且方案一没有中间过程,无法对其进行优化。我们排除方案一。果断转为后缀表达式。

假如找到了一种算法求得了答案,倒过来推会发现,运算位置在后,就越能够决定答案的值。可以联系到树的依赖关系。

在考虑方案二的中间过程后缀表达式,显然是能够转化为表达式树,这样对树的叶子节点进行填充就可以了。

对于树的计数,有三种方法:

1. 暴力,时间复杂度为 $O(2^n)$,同上,可排除此方案。

2. 排列组合,当然这个没有任何排列组合可以判定这个式子的值是否为 $0$,排除此方案。

3. 树形计数 DP,由于有子结构最优且父亲的值本身有子节点推导而来,所以这个方法可能可行。

由上述过程可知,我们应该在表达式树上树形计数 DP,由于节点值只有可能为二进制,即可能性较小,我们可以将其设为状态中的一维。考虑只有左右子树影响了本节点,所以定义时应当把一个子树看作一个整体作 DP。

这个时候我们可以定义出来 $dp_{i,0/1}$ 为以 $i$ 为节点的子树,计算值为 $0/1$ 时候的总方案数。

转移时可以用子树进行递推,下表为题目中的表格:

运算符

运算规则

0⊕0=0

1⊕0=1

0⊕1=1

1⊕1=0

\times

0\times0=0

0\times 1=0

1\times0=0

1\times1 =1

即运算符也应当影响计算值,分类讨论左子树为多少,右子树为多少时转移给该节点的 $0$ 状态还是 $1$ 状态即可。

最后答案输出因为根节点包含一切节点和自己,所以最终答案一定在根节点的 DP 值上,又问有多少种填法可以使得表达式的值为 0,所以答案应该为 ans=dp_{1,0}

注意:树形 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;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值