「NOIP Record」#1 树形DP(1)
一切的开始
树形 DP 计数。
luogu8867 建造军营
对于图中一条非割边,看守与否都相同,因此可以用 Tarjan 算法求出边双再缩点,把非树边单独拿出来,讨论每一条树边。
然而貌似不是很好做,子树外是否建造会影响子树内。考虑设 为军营只在以 为根的子树中出现,且至少存在 个军营的方案数。
转移时对于边 ,如果 中没有军营,那么子树 中每条边以及 看守与否均可,方案数 ;否则 一定要看守,方案数 。
还要乘上 所在边双中任意选择的方案数,并且减掉子树中一个都不选的方案。
但这样求出的并不是答案。
直接对一个 求对答案的贡献会产生重复,举个例子,如果 只有 这一个儿子,那么 节点上建造了军营, 没有建造,那么这样的情况就会在 与 中被重复统计到。
考虑将节点贡献在 处统计。在节点 处的贡献,要么是 节点中建造了,要么是 有至少两个儿子所在子树中建造了。这两种情况都在 中被统计过。
非法方案是 没有建造军营,并且只有一个儿子 所在子树中建造了,容易得到这部分就是
最后乘上每条非树边可选可不选的方案。
#include<bits/stdc++.h>using namespace std;#define int long long#define uint unsigned long long#define PII pair<int,int>#define MP make_pair#define fi first#define se second#define pb push_back#define eb emplace_back#define SET(a,b) memset(a,b,sizeof(a))#define CPY(a,b) memcpy(a,b,sizeof(b))#define rep(i,j,k) for(int i=(j);i<=(k);++i)#define per(i,j,k) for(int i=(j);i>=(k);--i)int read() { int a=0, f=1; char c=getchar(); while(!isdigit(c)) { if(c=='-') f=-1; c=getchar(); } while(isdigit(c)) a=a*10+c-'0', c=getchar(); return a*f;}const int N=5e5+5, M=1e6+5, mod=1e9+7;int n, m, num, cnt, pw[M];int ans, bel[N], c[N], sz[N], f[N];
struct Gr { int tot, h[N], to[M<<1], nxt[M<<1]; void add(int x,int y) { to[++tot]=y, nxt[tot]=h[x], h[x]=tot; }} G, T;
int dcc, tp, dfn[N], low[N], st[N];void tarjan(int x,int lst) { dfn[x]=low[x]=++num; st[++tp]=x; for(int i=G.h[x];i;i=G.nxt[i]) { int y=G.to[i]; if(i!=(lst^1)) { if(!dfn[y]) { tarjan(y,i); low[x]=min(low[x],low[y]); } else low[x]=min(low[x],dfn[y]); } } if(dfn[x]==low[x]) { int y=0; ++dcc; do y=st[tp--], ++c[dcc], bel[y]=dcc; while(x!=y); }}void dfs(int x,int fa) { sz[x]=1, f[x]=pw[c[x]]; for(int i=T.h[x];i;i=T.nxt[i]) { int y=T.to[i]; if(y==fa) continue; dfs(y,x); (f[x]*=(f[y]+pw[sz[y]])%mod)%=mod; sz[x]+=sz[y]; } (f[x]-=pw[sz[x]-1]-mod)%=mod; int F=f[x]; for(int i=T.h[x];i;i=T.nxt[i]) { int y=T.to[i]; if(y==fa) continue; (F-=f[y]*pw[sz[x]-sz[y]-1]-mod)%=mod; } (ans+=F*pw[dcc-sz[x]]%mod)%=mod;}void suodian() { rep(x,1,n) { for(int i=G.h[x];i;i=G.nxt[i]) { int y=G.to[i]; if(bel[x]!=bel[y]) { T.add(bel[x],bel[y]); } else ++cnt; } }}signed main() { G.tot=1; n=read(), m=read(); rep(i,1,m) { int x=read(), y=read(); G.add(x,y), G.add(y,x); } tarjan(1,0); suodian(); pw[0]=1; for(int i=1;i<=max(n,m);++i) pw[i]=pw[i-1]*2%mod; dfs(1,0); printf("%lld\n",ans*pw[cnt>>1]%mod);}自己顺着错误的想法推导的过程中,也得到了很多教训。
- 计数的式子,一定再三考虑后再写下
- 不要老是想着容斥掉某些东西
- 对于边双内部的点、边这类“可以提到外面”的东西,尽量不要放到 DP 的过程里面,太容易写错了,而且会让过程复杂化。
- 把板子打熟练
luogu7727 风暴之眼(Eye of the Storm)
对于此类有着复杂定义的题目,不妨先静下心来进行一些观察。
首先如果一个节点是初始为 的 型节点或者初始值为 的 型节点,那么一定不会改变自身的颜色。称其为黑点,其他成为白点。
白点最多变化一次。
同一个初始权值白色连通块内,要么权值都变化一次,要么都不变。证明略。
那么一个白色连通块何时才不会改变权值呢?只需要考虑它周围的一圈黑点。如果是 与 , 与 ,那么右边就不会因为左边改变权值,而两个不同类型的白点 与 或者一黑一白是会影响对方的。因此推出一个白色连通块的权值不改变,当且仅当整个连通块以及周围的一圈黑点,权值都相同。
如何让一个白色连通块达到权值相同?需要满足以下两个条件之一。1. 连通块内存在一个白点,满足初始权值等于最终权值。2. 周围存在一个黑点,满足其与这个连通块的最终权值相同。
考虑用树形 DP 来做。
为了方便采用如下标记
- ,黑点还是白点。
- ,初始权值是否等于最终权值。
- ,此时这个连通块内能否满足最终权值的条件。
每一种状态都存在吗?不然。
首先 和 就存在非法组合。
对于黑点,能接上它的白色连通块一定都满足最终权值的条件,且其初始权值必然等于最终权值。
对于白点,如果其初始权值等于最终权值,那么接上一个合法的白色连通块之后,必然满足最终权值条件。
因此随便钦定一个点为根,然后设 为在以 为根的子树中
然后考虑转移,用上上面的结论。方式是将 的子节点 看作是一个连通块,按照把 接到 上形成新的连通块来计数,同时根据 与 得到对应的转移方案。
开个临时数组 。
对于 和 ,如果
若
然后
答案是
#include<bits/stdc++.h>using namespace std;#define int long long#define SET(a,b) memset(a,b,sizeof(a))#define rep(i,j,k) for(int i=(j);i<=(k);++i)#define per(i,j,k) for(int i=(j);i>=(k);--i)int read() { int a=0, f=1; char c=getchar(); while(!isdigit(c)) { if(c=='-') f=-1; c=getchar(); } while(isdigit(c)) a=a*10+c-'0', c=getchar(); return a*f;}const int N=2e5+5, mod=998244353;int n, a[N], f[N][4], g[4];int tot, h[N], to[N<<1], nxt[N<<1];void add(int x,int y) { to[++tot]=y, nxt[tot]=h[x], h[x]=tot;}void dp(int x,int fa) { f[x][0]=f[x][1]=f[x][3]=1; for(int i=h[x];i;i=nxt[i]) { int y=to[i]; if(y==fa) continue; dp(y,x); SET(g,0); if(a[x]==a[y]) { g[0]=f[x][0]*(f[y][0]+f[y][1])%mod; g[1]=f[x][1]*((f[y][0]+f[y][1])%mod+(f[y][2]+f[y][3])%mod)%mod; g[2]=(f[x][2]*(f[y][1]+f[y][2]+f[y][3])%mod+f[x][3]*(f[y][1]+f[y][2])%mod)%mod; g[3]=f[x][3]*f[y][3]%mod; } else { g[0]=0; g[1]=f[x][1]*(f[y][1]+f[y][2])%mod; g[2]=(f[x][2]*(f[y][1]+f[y][2]+f[y][3])%mod+f[x][3]*(f[y][2]+f[y][3])%mod)%mod; g[3]=f[x][3]*f[y][1]%mod; } memcpy(f[x],g,sizeof(g)); }}signed main() { n=read(); rep(i,1,n) a[i]=read(); rep(i,1,n-1) { int x=read(), y=read(); add(x,y), add(y,x); } dp(1,0); printf("%lld\n",(f[1][0]+f[1][1]+f[1][2])%mod);}- 状态间的转移应仔细推敲。
- 见到这种看起来很吓人的题目,千万不要手足无措白白浪费时间,尝试观察性质,哪怕打个暴力呢。
luogu8973 『GROI-R1』 继续深潜,为了同一个梦想
一个点被这样的点集所包含,只有两种情况。
要么是从子树内到某个祖先的一条链,要么是端点在两个子节点的子树内,跨越这个点的一条链。
考虑一个转化,求 时,将 钦定为根。那么前者转化为子树内到达 的链。
设 为整棵树以 为根,在以 为根的子树内,有多少以 为端点且满足条件的的链,其实就是在子树内除了 选择至少一个节点的方案数。
然后考虑第二种情况,其实就是两条上述的链拼凑而成,
设 ,。
对于一个子节点 ,其贡献为 , 是顺便把第一种情况算上。同时为了避免重复计数,计算完 之后令 。
综上,得到以 为整棵树的根时,以 为根的子树内的答案 。
那么如何得到所有结点的答案呢?考虑换根。
发现对于 ,有
直接求即可。
#include<bits/stdc++.h>using namespace std;#define int long long#define SET(a,b) memset(a,b,sizeof(a))#define rep(i,j,k) for(int i=(j);i<=(k);++i)#define per(i,j,k) for(int i=(j);i>=(k);--i)int read() { int a=0, f=1; char c=getchar(); while(!isdigit(c)) { if(c=='-') f=-1; c=getchar(); } while(isdigit(c)) a=a*10+c-'0', c=getchar(); return a*f;}const int N=5e5+5, mod=1e9+7;int n, f[N], g[N];int tot, h[N], to[N<<1], nxt[N<<1];void add(int x,int y) { to[++tot]=y, nxt[tot]=h[x], h[x]=tot;}int F(int x) { return (2*f[x]%mod+1)%mod; }void dfs(int x,int fa) { for(int i=h[x];i;i=nxt[i]) { int y=to[i]; if(y==fa) continue; dfs(y,x); (f[x]+=2*f[y]+1)%=mod; }}void dfs2(int x,int fa) { int S=0; for(int i=h[x];i;i=nxt[i]) { int y=to[i]; (S+=F(y))%=mod; } for(int i=h[x];i;i=nxt[i]) { int y=to[i]; (g[x]+=F(y)*(S-F(y)+1+mod)%mod)%=mod; (S-=F(y)-mod)%=mod; } for(int i=h[x];i;i=nxt[i]) { int y=to[i]; if(y==fa) continue; int tx=f[x], ty=f[y]; (f[x]-=2*f[y]%mod+1-mod)%=mod; (f[y]+=2*f[x]%mod+1)%=mod; dfs2(y,x); f[x]=tx, f[y]=ty; }}signed main() { n=read(); rep(i,1,n-1) { int x=read(), y=read(); add(x,y), add(y,x); } dfs(1,0); dfs2(1,0); int ans=0; rep(i,1,n) ans^=g[i]*i; printf("%lld\n",ans);}luogu8280 「MCOI-08」Photoelectric Effect
思路并不难想。考虑以 为根的子树和其若干子节点 。
设 颜色为 ,对于 和 ,以 为根的子树中任何一个节点与 子树中的任意一个节点颜色之并等于 。那么两棵子树的颜色集合两两之并都是 ,这个可以预处理,同时也能看出子树颜色集合必须加入状态中。
从样例中能看到这个并运算不满足交换律,因此预处理时要注意如果集合 与 正反两种并不同,那么不合法。
考虑到颜色关系的特殊性,需要一棵一棵地加入子树来统计答案。特别的,第一棵子树不存在颜色限制。
为了避免包含等不必要的麻烦,设 为以 为根的子树中,不含 的颜色集合。同时设 表示 中任意元素并上 中任意元素结果为同一种颜色,且满足 。
设 为以 为根的子树, 的颜色是 ,其他子树内的颜色集合是 的方案数。
考虑 如何转移。
条件是 ,存在 满足 ,贡献为
特别地,对于 的第一棵子树
复杂度
相当恐怖,不过剪掉无用状态后就跑不满,可过。
#include<bits/stdc++.h>using namespace std;#define int long long#define SET(a,b) memset(a,b,sizeof(a))#define rep(i,j,k) for(int i=(j);i<=(k);++i)#define per(i,j,k) for(int i=(j);i>=(k);--i)int read() { int a=0, f=1; char c=getchar(); while(!isdigit(c)) { if(c=='-') f=-1; c=getchar(); } while(isdigit(c)) a=a*10+c-'0', c=getchar(); return a*f;}const int N=1e5+5, mod=1e9+7;int T, n, k, U, son[N], t[5][5], p[32][32], f[N][5][32], g[5][32];int tot, h[N], to[N<<1], nxt[N<<1];void add(int x,int y) { to[++tot]=y, nxt[tot]=h[x], h[x]=tot;}int get(int x,int y) { int res=-2; rep(i,0,k-1) if(x&(1<<i)) { rep(j,0,k-1) if(y&(1<<j)) { if(res==-2) res=t[i][j]; else if(res!=t[i][j]) res=-1; } } swap(x,y); rep(i,0,k-1) if(x&(1<<i)) { rep(j,0,k-1) if(y&(1<<j)) { if(res==-2) res=t[i][j]; else if(res!=t[i][j]) res=-1; } } return res;}void dp(int x,int fa) { if(!son[x]) { rep(i,0,k-1) f[x][i][0]=1; return; } int fg=0; for(int i=h[x];i;i=nxt[i]) { int y=to[i]; if(y==fa) continue; dp(y,x); if(!fg) { rep(nw,0,k-1) rep(S,0,U) rep(w,0,k-1) { (f[x][nw][S|(1<<w)]+=f[y][w][S])%=mod; } fg=1; continue; } rep(nw,0,k-1) rep(S,1,U) g[nw][S]=0; rep(nw,0,k-1) rep(S0,0,U) if(f[y][nw][S0]) rep(S,1,U) { int T=S0|(1<<nw); if(p[S][T]==-1) continue; (g[p[S][T]][S|T]+=f[x][p[S][T]][S]*f[y][nw][S0]%mod)%=mod; } memcpy(f[x],g,sizeof(g)); }}void solve() { n=read(), k=read(); U=(1<<k)-1; tot=0; rep(i,1,n) { h[i]=son[i]=0; SET(f[i],0); } SET(p,-1); rep(i,0,k-1) rep(j,0,k-1) { int x=read()-1; t[i][j]=x; } rep(i,2,n) { int x=read(); ++son[x]; add(x,i), add(i,x); } rep(i,1,U) rep(j,1,U) p[i][j]=get(i,j); dp(1,0); int ans=0; rep(i,0,k-1) rep(j,0,U) (ans+=f[1][i][j])%=mod; printf("%lld\n",ans);}signed main() { T=read(); while(T--) solve();}- 状态压缩,优先使用刷表法
- 考虑再三后再预处理
- 预处理时一定不要把初始状态设成无解状态
- 加入子树的计数思路
- 一开始竟然设 为以 为根的子树的所有颜色,导致很多麻烦且干扰思路与转移。
最后附上我一开始写的 SB 预处理。
之前也在预处理上吃过亏啊,以后要多加小心。
void init() { rep(i,1,U) rep(j,1,U) if(p[i][j]<0) { int kk=-1; // 从头开始就错了 rep(k1,0,4) if(i&(1<<k1)) { int q=-1; rep(k2,0,4) if(j&(1<<k2)) { if(q==-1) q=p[k1][k2]; else if(q!=p[k1][k2]) { kk=-1; goto pq; } } if(kk==-1) kk=q; else if(kk!=q) { kk=-1; break; } } pq: p[i][j]=kk; } rep(i,1,U) rep(j,1,U) if(p[i][j]!=p[j][i]) p[i][j]=p[j][i]=-1;}luogu3349 [ZJOI2016]小星星
题意比较裸。
设 为以 为根的子树,节点 映射到原图中的 ,此时子树内映射到图中的节点集合为 的方案数。
转移则遇到了困难,首先复杂度就很高了,其次 这一维的转移要把 不重不漏地划分成 的子节点个数个非空子集,也就是一个子集卷积。
考虑困难的来源是「每个节点只能映射一次」这个限制导致了必须记录 。不难发现只要有节点映射了超过一次,那么一定有节点没有被映射,这个是可以容斥的。
考虑枚举集合 ,其中 是节点集合,表示不能映射 中的节点,进行最原始的集合间的容斥即可。
考虑此时如何求方案数。设 表示以 为根的子树,节点 映射到了图中节点 的方案数,容易写出
#include<bits/stdc++.h>using namespace std;#define int long long#define uint unsigned long long#define PII pair<int,int>#define MP make_pair#define fi first#define se second#define pb push_back#define eb emplace_back#define SET(a,b) memset(a,b,sizeof(a))#define CPY(a,b) memcpy(a,b,sizeof(b))#define rep(i,j,k) for(int i=(j);i<=(k);++i)#define per(i,j,k) for(int i=(j);i>=(k);--i)int read() { int a=0, f=1; char c=getchar(); while(!isdigit(c)) { if(c=='-') f=-1; c=getchar(); } while(isdigit(c)) a=a*10+c-'0', c=getchar(); return a*f;}const int N=20;int n, m, U, S, popcnt[1<<17];int lg[1<<17];int tot, h[N], to[N*N], nxt[N*N];int t2, h2[N], to2[N*N], nxt2[N*N];uint ans, f[N][N];int lowbit(int x) { return x&-x; }void add(int x,int y) { to[++tot]=y, nxt[tot]=h[x], h[x]=tot;}void add2(int x,int y) { to2[++t2]=y, nxt2[t2]=h2[x], h2[x]=t2;}void dp(int x,int fa) { for(int i=h2[x];i;i=nxt2[i]) { int y=to2[i]; if(y==fa) continue; dp(y,x); } for(int T=S^U;T;T-=lowbit(T)) { int a=lg[lowbit(T)]; f[x][a]=1; for(int i=h2[x];i;i=nxt2[i]) { int y=to2[i]; if(y==fa) continue; int dlt=0; for(int j=h[a];j;j=nxt[j]) dlt+=f[y][to[j]]; f[x][a]*=dlt; } }}signed main() { n=read(), m=read(); U=(1<<n)-1; rep(i,1,m) { int x=read(), y=read(); add(x,y), add(y,x); } lg[1]=1; rep(i,1,n-1) { int x=read(), y=read(); add2(x,y), add2(y,x); lg[1<<i]=i+1; } for(S=0;S<=U;++S) { popcnt[S]=popcnt[S>>1]+(S&1); int ss=0; rep(i,1,n) rep(j,1,n) f[i][j]=0; dp(1,0); for(int i=S^U;i;i-=lowbit(i)) ss+=f[1][lg[lowbit(i)]]; if(popcnt[S]&1) ans-=ss; else ans+=ss; } printf("%llu\n",ans);}
树形背包。
用来解决树形 DP 中,子树之间会互相影响的转移。具体方法是先依次合并各子树信息,再处理根的影响。
为什么是「类」树形背包?因为此类题目一般没有给出作为背包容积的上界,因此必须用当前子树大小卡好复杂度,否则会退化。
因此,在实现上是需要注意的。
ABC287F Components
给定一棵树 ,求对于 ,所有 的子集与 构成的图中,恰好有 个连通块的方案数。对 取模。
考虑在各子树的父节点处统计贡献。
设 表示以 为根的子树,每个节点可选可不选,恰好存在 个连通块,其中 有没有被选择的方案数。
用类似树形背包的方式转移,好处是可以容易考虑一棵子树对其它子树的影响。
如果 不选择,那么对于子节点 ,其
选不选都不收影响,因此
如果 选择,那么如果 选择了,就会减少一个连通块
#include<bits/stdc++.h>using namespace std;#define int long long#define SET(a,b) memset(a,b,sizeof(a))#define rep(i,j,k) for(int i=(j);i<=(k);++i)#define per(i,j,k) for(int i=(j);i>=(k);--i)int read() { int a=0, f=1; char c=getchar(); while(!isdigit(c)) { if(c=='-') f=-1; c=getchar(); } while(isdigit(c)) a=a*10+c-'0', c=getchar(); return a*f;}const int N=5005, mod=998244353;int n, sz[N], f[N][N][2], g[N][2];int tot, h[N], to[N<<1], nxt[N<<1];void add(int x,int y) { to[++tot]=y, nxt[tot]=h[x], h[x]=tot;}void dp(int x,int fa) { sz[x]=f[x][0][0]=f[x][1][1]=1; for(int i=h[x];i;i=nxt[i]) { int y=to[i]; if(y==fa) continue; dp(y,x); SET(g,0);
for(int j=0;j<=sz[x];++j) for(int k=0;k<=sz[y];++k) { (g[j+k][0]+=f[x][j][0]*((f[y][k][0]+f[y][k][1])%mod)%mod)%=mod; (g[j+k][1]+=f[x][j][1]*((f[y][k][0]+f[y][k+1][1])%mod)%mod)%=mod; } rep(j,0,sz[x]+sz[y]) f[x][j][0]=g[j][0], f[x][j][1]=g[j][1]; sz[x]+=sz[y]; }}signed main() { n=read(); rep(i,1,n-1) { int x=read(), y=read(); add(x,y), add(y,x); } dp(1,0); rep(i,1,n) printf("%lld\n",(f[1][i][0]+f[1][i][1])%mod);}luogu8564 ρars/ey
容易设出状态, 表示以 为根的子树,还剩下 个节点,所需要的最小代价。
把转移过程分为两部分,一部分从子树处继承没有被删去的节点数量,另一部分从 处删点。
设当前加入了 棵子树,那么在合并子树的过程中,最少剩下 个节点,这个下界会不断改变,因此要不断更新相应的信息。
#include<bits/stdc++.h>using namespace std;#define int long long#define SET(a,b) memset(a,b,sizeof(a))int read() { int a=0, f=1; char c=getchar(); while(!isdigit(c)) { if(c=='-') f=-1; c=getchar(); } while(isdigit(c)) a=a*10+c-'0', c=getchar(); return a*f;}const int N=5005, inf=0x3f3f3f3f3f3f3f3f;int n, a[N], sz[N], f[N][N], g[N];int tot, h[N], to[2*N], nxt[2*N];void add(int x,int y) { to[++tot]=y, nxt[tot]=h[x], h[x]=tot;}void addedge(int x,int y) { add(x,y), add(y,x);}void dp(int x,int fa) { sz[x]=1, f[x][1]=0; int cnt=0; for(int i=h[x];i;i=nxt[i]) { int y=to[i]; if(y==fa) continue; dp(y,x); for(int j=cnt+1;j<=sz[x];++j) for(int k=1;k<=sz[y];++k) g[j+k]=min(g[j+k],f[x][j]+f[y][k]); ++cnt, sz[x]+=sz[y]; f[x][cnt]=inf; for(int j=cnt+1;j<=sz[x];++j) f[x][j]=g[j], g[j]=inf; } f[x][1]=a[sz[x]]; for(int k=cnt+1;k<=sz[x];++k) f[x][1]=min(f[x][1],f[x][k]+a[k]);}signed main() { n=read(); for(int i=2;i<=n;++i) a[i]=read(); for(int i=1;i<n;++i) { int x=read(), y=read(); add(x,y), add(y,x); } SET(g,0x3f); dp(1,0); printf("%lld\n",f[1][1]);}luogu6478 [NOI Online #2 提高组] 游戏
如果出现了平局,那么一定是两个点在以它们的 为根的,不同的子树中。
在树形 DP 中合并信息是容易的。
设 为以 为根的子树,有 个回合没有平的方案数。
合并完子树的过程中,统计出子树内每种颜色的点的数量,然后只要选择和 颜色相反的就能让非平局回合数量加 。
但是这样得到的是整棵树中,至少有 个非平局回合的方案数。所以套一个二项式反演即可。
#include<bits/stdc++.h>using namespace std;#define int long long#define SET(a,b) memset(a,b,sizeof(a))#define rep(i,j,k) for(int i=(j);i<=(k);++i)#define per(i,j,k) for(int i=(j);i>=(k);--i)int read() { int a=0, f=1; char c=getchar(); while(!isdigit(c)) { if(c=='-') f=-1; c=getchar(); } while(isdigit(c)) a=a*10+c-'0', c=getchar(); return a*f;}const int N=5005, mod=998244353;int n, m, a[N], sz[N], c[N][2], F[N], G[N], f[N][N], g[N], fac[N], inv[N];int tot, h[N], to[N<<1], nxt[N<<1];char s[N];void add(int x,int y) { to[++tot]=y, nxt[tot]=h[x], h[x]=tot;}void dp(int x,int fa) { ++c[x][a[x]], f[x][0]=sz[x]=1; for(int i=h[x];i;i=nxt[i]) { int y=to[i]; if(y==fa) continue; dp(y,x); for(int j=0;j<=sz[x]+sz[y];++j) g[j]=0; for(int j=0;j<=sz[x];++j) for(int k=0;k<=sz[y];++k) (g[j+k]+=f[x][j]*f[y][k]%mod)%=mod; sz[x]+=sz[y]; for(int j=0;j<=sz[x];++j) f[x][j]=g[j]; c[x][0]+=c[y][0], c[x][1]+=c[y][1]; } for(int i=c[x][a[x]^1];~i;--i) (f[x][i]+=f[x][i-1]*(c[x][a[x]^1]-(i-1))%mod)%=mod;}int C(int n,int m) { if(n<m) return 0; return fac[n]*inv[m]%mod*inv[n-m]%mod;}int fp(int a,int b) { int c=1; for(;b;a=a*a%mod,b>>=1) if(b&1) c=c*a%mod; return c;}void init() { fac[0]=inv[0]=1; rep(i,1,m) fac[i]=fac[i-1]*i%mod; inv[m]=fp(fac[m],mod-2); per(i,m-1,1) inv[i]=inv[i+1]*(i+1)%mod;}signed main() { n=read(); m=n>>1; scanf("%s",s+1); rep(i,1,n-1) { int x=read(), y=read(); add(x,y), add(y,x); } rep(i,1,n) a[i]=s[i]-'0'; dp(1,0); init(); rep(i,0,m) G[i]=f[1][i]*fac[m-i]%mod; rep(i,0,m) rep(j,i,m) { int c=((j-i)&1)? -1:1; (F[i]+=c*C(j,i)%mod*G[j]%mod)%=mod; if(F[i]<0) F[i]+=mod; } rep(i,0,m) printf("%lld\n",F[i]);}觉得这篇文章怎么样?
点个赞,让更多人看到!

评论区