https://vjudge.net/problem/CodeForces-461B
题目
某人有一颗树,上面有些节点是黑色的,他可以去掉一些边,让这颗树变成森林,并且每一个树都有且只有1个黑色节点,问有多少种选择边的方法。
$nleqslant 10^5$
题解
首先把树转换为有根树
然后dp[x][1/0]表示节点x上面那条边下面的子树只有一个黑节点或没有黑节点的方案数
规定边可以断开:
假设x没有颜色,v0表示从x开始的子树都是白色的方案个数,v1表示x的子树有一个黑色节点的方案个数
那么[egin{array}{cc}v1=&dp[s_1][1]*dp[s_2][0]*...*dp[s_n][0]\+&dp[s_1][0]*dp[s_2][1]*dp[s_3][0]*...*dp[s_n][0]\+&...\+&dp[s_1][0]*dp[s_2][0]*...*dp[s_{n-1}][0]*dp[s_n][1]end{array}]
$v0=dp[s_1][0]*dp[s_2][0]*...*dp[s_n][0]$
如果x是白色,$dp[x][1]=v1$,$dp[x][0]=v1+v0$(因为边可以断开)
如果x是黑色,$dp[x][1]=v0$,$dp[x][0]=v0$(因为即使断开子树也只能有一个黑节点)
但是为了计算v1需要花$mathcal{O}(n^2)$的时间,肯定超时
[egin{array}{cccccc}&1& imes&0& imes&0\+&0& imes&1& imes&0\+&0& imes&0& imes&1end{array}]
其实可以递推计算v1:我们可以从左上角的$k imes k$的正方形递推到$(k+1) imes(k+1)$的正方形
那么计算v1只需要花$mathcal{O}(n)$,每个点最多访问两次,时间复杂度$mathcal{O}(n)$
AC代码
#include<cstdio> #include<cstring> #include<algorithm> #include<cmath> #include<vector> #include<map> #define REP(i,a,b) for(register int i=(a); i<(b); i++) #define REPE(i,a,b) for(register int i=(a); i<=(b); i++) #define PERE(i,a,b) for(register int i=(a); i>=(b); i--) using namespace std; typedef long long ll; #define MAXN 100007 int n; int hd[MAXN], nxt[MAXN*2], to[MAXN*2], m; bool b[MAXN]; inline void adde(int a, int b) { nxt[m]=hd[a]; hd[a]=m; to[m]=b; m++; } #define MO (int(1e9)+7) ll dp[MAXN][2]; bool vis[MAXN]; void dfs(int x) { ll v1=0, v0=1; vis[x]=1; int cnt=0; for(int i=hd[x]; ~i; i=nxt[i]) if(!vis[to[i]]) { cnt++; int o=to[i]; dfs(o); v1=v1*dp[o][0]%MO; v1=(v1+v0*dp[o][1])%MO; v0=v0*dp[o][0]%MO; } if(b[x]) { dp[x][1]=v0; dp[x][0]=v0; } else { dp[x][1]=v1; dp[x][0]=(v1+v0)%MO; } } int main() { scanf("%d", &n); m=0; memset(hd,-1,sizeof(int)*n); memset(vis,0,sizeof(bool)*n); REP(i,1,n) { int x; scanf("%d", &x); adde(i,x); adde(x,i); } REP(i,0,n) { int t; scanf("%d", &t); b[i]=t; } dfs(0); printf("%lld ", dp[0][1]); }