容斥与二项式反演

感谢 hotpotcondiment 对此页面的贡献

容斥原理

n 个集合 S1,S2,,Sn(可能集合有交),则至少在一个集合的元素为

在任意一个集合内的元素总和在任意两个集合交内的元素总和+在任意三个集合交内的元素总和

应用

考虑一个由多个条件组成的集合 S,且满足这个由多个条件组成的集合 p 的元素个数为 f(p),(特别地,记 f() 为元素的全集 U 的大小 |U|,感谢 black_trees 的指出)则

满足 S 中某些条件之一的元素个数为

pS,p(1)|p|+1f(p)

同时不满足 S 中任意条件的元素个数为

pS(1)|p|f(p)

例题1:CF997C Sky Full Of Stars

题意

有一个 n×n 的矩阵,用三种颜色染色,求至少有多少种方案使得至少有一行或一列是同一种颜色,对 998244353 取模。n106

解法

考虑容斥。可以把题目中“至少有一行或一列是同一种颜色”的条件看成“满足 2n 个条件之一:有一行或有一列为同一种颜色”。此时设至少 i 行和 j 列为同色的方案数为 f(i,j),则答案为:

i=0nj=0n(1)i+j+1f(i,j)+f(0,0)

显然有 f(i,0)=(ni)3i3n(ni)f(0,j)=(nj)3j3n(nj)。注意 ij0 的情况,由于一行的颜色相同决定 j 列的颜色必须相同,一列的颜色相同决定了 i 行的颜色必须相同;故而最后 i 行和 j 列的颜色必须相同。故而 f(i,j)=3(ni)(nj)3(ni)(nj)

故而答案可以表示成:

j=1n(1)j+1(nj)3j3n(nj)+i=1n(1)i+1(ni)3i3n(ni)+i=1nj=1n(1)i+j+1(3(ni)(nj)3(ni)(nj))

前面两项可以 O(n) 算,关键是优化 i=1nj=1n(3(ni)(nj)3(ni)(nj)) 的计算。考虑把与 j 无关的项移到 i=1n 后,化简则有

=i=1nj=1n(1)i+j+1(3(ni)(nj)3(ni)(nj))=3i=1n((1)i+1(ni)j=1n((1)j(nj)3n2ninj+ij))=3n2+1i=1n((1)i+1(ni)3nij=1n((1)j(nj)3(in)j))=3n2+1i=1n((1)i+1(ni)3ni((j=0n((1)j(nj)3(in)j))1))=3n2+1i=1n((1)i+1(ni)3ni((j=0n((nj)(3in)j1nj))1))=3n2+1i=1n((1)i+1(ni)3ni((13in)n1))

其中最后两步用到了二项式定理,即 (a+b)n=i=0n(ni)aibni

代码

点此查看代码
#include <bits/stdc++.h>
using namespace std;
#define ll long long
const int maxn=1000010;
const int md=998244353;
int fac[maxn],inv[maxn];
int n,i,b=1;
inline ll Pow(ll d,ll z){
ll ret=1;
do{
if(z&1) ret=(ret*d)%md;
d=(d*d)%md;
}while(z>>=1);
return ret;
}
inline ll C(int x,int y){return ((1LL*fac[y]*inv[x])%md)*inv[y-x]%md;}
ll ans,tmp,sum;
int main(){
fac[0]=inv[0]=1;
for(i=1;i<maxn;++i) fac[i]=(1LL*fac[i-1]*i)%md;
inv[maxn-1]=Pow(fac[maxn-1],md-2);
for(i=maxn-2;i;--i) inv[i]=(1LL*inv[i+1]*(i+1))%md;
scanf("%d",&n);
for(i=1;i<=n;++i,b=-b){
sum+=b*((C(i,n)*Pow(Pow(3,1LL*n*i),md-2))%md*
(Pow(1-Pow(Pow(3,n-i),md-2),n)-1))%md;
ans+=(2*b*C(i,n)*Pow(3,1LL*n*(n-i)+i))%md;
}
ans=(ans%md+md)%md;
sum=((sum%md+md)%md)*Pow(3,1LL*n*n+1);
printf("%lld",(ans+sum)%md);
return 0;
}

例题2:ARC101E Ribbons on Tree

题意

有一棵大小为 n 的树。现在要对树上的点两两配对。然后对于每一对点 (u,v),将 uv 的简单路径上的所有边染色,定义一组合法的配对方案为能够把树上的所有边染色。求合法的配对方案数,对 109+7 取模。n5000 且保证 n 为偶数。

解法

考虑容斥。设整棵树的边集为 E,且令对应的染色的所有边均在 ET 边集内的方案数为 g(T),则可得总合法方案数为 TEg(T)(1)|T|。(可以把每一条边未被染色视为一个条件,然后需要所有边均被染色)

考虑 ET 满足怎样的条件。显然它们构成了 |T|+1 个连通块,然后每对点均必须在同一个连通块中。

考虑每个连通块内有多少种配对方法。若该连通块大小为 S(显然 S 为偶数),则第 1 个点有 S1 个点进行配对,第 2 个未配对的点有 S3 个点进行配对,以此类推可得该连通块内的总配对方案为 i=1S2(2i1)(记为 h(S)。特别地,S 为奇数时,定义 h(S)0)。同时若某个选边方案对应的所有连通块的集合为 P,则最后这种选边方案对应的配对方案为 SPh(S)

考虑使用 dp 计算所有的方案数。设 dpu,i,j 表示在 u 的子树内,选定了 i 个连通块 (不包括 u 所在的连通块,因为最后还没有统计好 u 所在连通块的大小),且 u 目前所在的连通块大小为 j 的对应部分的统计完的连通块的对应方案数。从 u 的某个儿子 v 转移时,若选择保留 (u,v) 边,则 uv 所在连通块相连接,有 dpu,i+x,j+ydpu,i+x,j+y+dpu,i,jdpv,x,y。(此时不需要乘上有关 h 的系数,因为这个连通块还有和其他连通块合并且扩大的机会)否则,由于 v 所在的连通块只能通过 (u,v) 边继续扩大,故而此时 v 所在的连通块不会再次扩大,已经统计完成,可以乘上 v 所在的连通块大小对应的 h 值,故而有 dpu,i+x+1,j+ydpu,i+x+1,j+y+dpu,i,jdpv,x,yh(y)。统计完成后,需要将 dp1,x,yh(y) 累加到 dp1,x+1,0 上,答案即为 i=1n(1)i1dp1,i,0。注意:在具体转移 dp 值时,需要把转移的 dp 值赋在另一个数组上,避免之前的 dp 值(现在已经非法,由于我们每次多扫描到 u 的一棵子树就要强制将其影响体现在 dpu 上)造成影响;在转移结束后再赋回来。

这样对于 u 子树内的 dp,设目前将 dp 值转移到 u 子树的所有子树大小之和为 s,且即将合并到 u 子树上的子树大小为 t,则此次统计会对总时间复杂度造成 O(s2t2)=O((s2)(t2)) 的贡献。考虑组合意义,可得总时间复杂度等效于在树上选取任意两对不同的点(但是一对点可以是两个相同的点)的数量(它们会在它们的 lca 处对总时间复杂度有 O(1) 的贡献),时间复杂度即为 O(n4)。空间复杂度显然为 O(n3)

考虑优化。发现其实最后 dp 值对答案的贡献的容斥系数只与连通块的数量的奇偶性有关,所以可以把 dp 数组的第二维变成选定的连通块的奇偶性(第二维的大小就只会有 2),同时把第二维涉及的加法转为异或,空间复杂度即可降为 O(n2)

考虑时间复杂度的计算。对于 u 子树内的 dp,设目前将 dp 值转移到 u 子树的所有子树大小之和为 s,且即将合并到 u 子树上的子树大小为 t,则此次统计会对总时间复杂度造成 O(st) 的贡献。考虑组合意义,可得总时间复杂度等效于在树上选取任意一对不同的点的数量,时间复杂度即为 O(n2)

代码

点此查看代码
#include <bits/stdc++.h>
using namespace std;
const int maxn=5010;
const int md=1000000007;
int n,i,j,u,v,tot;
int head[maxn],ver[maxn<<1],nxt[maxn<<1];
int h[maxn],dp[maxn][2][maxn],tdp[2][maxn];
int dfs(const int p,const int f){
int lp,to,siz=1,ssi;
long long tmp;
dp[p][0][1]=1;
for(lp=head[p];lp;lp=nxt[lp]){
to=ver[lp];
if(to==f) continue;
ssi=dfs(to,p);
for(i=siz;i;--i){
for(j=1;j<=ssi;++j){
tdp[0][i+j]=(1LL*dp[p][0][i]*dp[to][0][j]
+1LL*dp[p][1][i]*dp[to][1][j]+tdp[0][i+j])%md;
tdp[1][i+j]=(1LL*dp[p][0][i]*dp[to][1][j]
+1LL*dp[p][1][i]*dp[to][0][j]+tdp[1][i+j])%md;
tdp[0][i]=((1LL*dp[p][0][i]*dp[to][1][j]
+1LL*dp[p][1][i]*dp[to][0][j])%md*h[j]+tdp[0][i])%md;
tdp[1][i]=((1LL*dp[p][0][i]*dp[to][0][j]
+1LL*dp[p][1][i]*dp[to][1][j])%md*h[j]+tdp[1][i])%md;
}
}
siz+=ssi;
for(i=1;i<=siz;++i){
dp[p][0][i]=tdp[0][i];
dp[p][1][i]=tdp[1][i];
tdp[0][i]=tdp[1][i]=0;
}
}
return siz;
}
int main(){
h[2]=1;
for(i=4;i<maxn;i+=2) h[i]=(1LL*h[i-2]*(i-1))%md;
scanf("%d",&n);
for(i=1;i<n;++i){
scanf("%d%d",&u,&v);
ver[++tot]=v;
nxt[tot]=head[u];
head[u]=tot;
ver[++tot]=u;
nxt[tot]=head[v];
head[v]=tot;
}
dfs(1,0);
u=dp[1][1][0];
v=dp[1][0][0];
for(i=1;i<=n;++i){
u=(u+1LL*h[i]*dp[1][0][i])%md;
v=(v+1LL*h[i]*dp[1][1][i])%md;
}
printf("%d",(u-v+md)%md);
return 0;
}

引入:矩阵相关知识

单位矩阵

定义

满足 i,j[1,n],Ii,j=[i=j]n×n 的矩阵为 n 阶单位矩阵。

性质

A×I=I×A=A

逆矩阵

定义

An×n 的矩阵,若存在一个 n×n 的矩阵 B 满足 A×B=B×A=I,则 BA 的逆矩阵,记为 B=A1(有些矩阵不存在逆矩阵,例如全 0 矩阵)

高斯-若尔当法求逆矩阵

An×n 的矩阵,可以构造矩阵如下:

[A1,1A1,2A1,n[1=1][1=2][1=n]A2,1A2,2A2,n[2=1][2=2][2=n]An,1An,2An,n[n=1][n=2][n=n]]

将矩阵高斯消元,得到下面的矩阵:

[[1=1][1=2][1=n]B1,1B1,2B1,n[2=1][2=2][2=n]B2,1B2,2B2,n[n=1][n=2][n=n]Bn,1Bn,2Bn,n]

此时 B 即为 A 的逆矩阵。

转置矩阵

定义

An×m 的矩阵,若存在一个 m×n 的矩阵 B 满足 i[1,n],j[1,m],Ai,j=Bj,i,则 BA 的转置矩阵,记为 B=AT

性质

AT×(A1)T=I

关系矩阵

定义

已知 fg 为定义在 [1,n]N+ 上的函数,构造 n×1 矩阵 FG 如下:

F=[f(1)f(2)f(n)],G=[g(1)g(2)g(n)]

若存在一个 n×n 的矩阵 A 满足 A×F=G,则称 AFG 的关系矩阵。

引入:反演原理

FG 的关系矩阵与 GF 的关系矩阵互为逆矩阵。

证明:

AFG 的关系矩阵,BGF 的关系矩阵,则 B×G=FA×F=G,从而有 B×A×F=FB×A×F×F1=F×F1=I。由矩阵乘法满足结合律即有 B×A=I

二项式反演

内容

f(n)=i=0n(1)i(ni)g(i)g(n)=i=0n(1)i(ni)f(i)(1)f(n)=i=nm(1)i(in)g(i)g(n)=i=nm(1)i(in)f(i)(2)f(n)=i=0n(ni)g(i)g(n)=i=0n(1)ni(ni)f(i)(3)f(n)=i=nm(in)g(i)g(n)=i=nm(1)in(in)f(i)(4)

其中 m 可以为任何的大于 n 的常数,甚至可以是正无穷。

证明

F=[f(0)f(1)]G=[g(0)g(1)]

(1):显然 FGGF 的关系矩阵均为 A(其中 Ai,j=(1)j1(i1j1))。现在只需要证明 A×A=I 即可。推式子可得如下:

(A×A)i,j=k=1n+1Ai,kAk,j=k=1n+1((1)k1(i1k1)×(1)j1(k1j1))=k=ji((1)k+j(i1)!(k1)!(ik)!×(k1)!(j1)!(kj)!)=k=ji((1)k+j(i1)!(ij)!(ik)!(kj)!(j1)!(ij)!)=k=ji((1)kj(ijkj)(i1ij))=(i1ij)k=0ij((ijk)(1)k1ijk)=[ij](i1ij)(1+1)ij=[ij][i=j](i1ij)=[i=j](i10)=[i=j]

上式中第 35 步可以用组合意义直接得出。

(3)FG 的关系矩阵 A 满足 Ai,j=(i1j1)GF 的关系矩阵 B 满足 Bi,j=(1)ij(i1j1)。推式子可得如下:

(A×B)i,j=k=1n+1Ai,kBk,j=k=1n+1((1)kj(i1k1)(k1j1))=k=ji((1)kj(ijkj)(i1ij))=(i1ij)k=0ij((ijk)(1)k1ijk)=[ij](i1ij)(1+1)ij=[ij][i=j](i1ij)=[i=j](i10)=[i=j]

(2):因为 FGGF 的关系矩阵均为 A(其中 Ai,j=(1)j1(j1i1)),考虑把 A 转置得 (AT)i,j=(1)i1(i1j1)。然后推式子基本与 (1) 的证明中的内容相似,只不过第六步为 (i1ij)k=0ij((ijk)(1)ijk1k)。同样可以证明 AT×AT=I,所以 A×A=I

(4):因为 FG 的关系矩阵 A 满足 Ai,j=(j1i1)GF 的关系矩阵 B 满足 Bi,j=(1)ij(j1i1);所以 (AT)i,j=(i1j1)(BT)i,j=(1)ij(i1j1)。由 (3) 的证明过程可得 AT×BT=I,所以 A×B=I

用法

某些题目可能需要求 恰好 满足若干条件的元素数量,但是 钦定 满足若干条件的元素数量更为好求,则可以通过二项式反演求得 恰好 满足若干条件的元素数量。

例如:设恰好满足 i 个条件的元素个数为 f(i)钦定 满足 i 个条件的元素个数为 g(i),(条件总共有 n 个)则一般有:

g(i)=j=in(ji)f(j)

使用二项式反演简化计算即可。

注意:钦定 满足若干条件的元素中可能有重复的情况,和 至少 不同。

同时由于 (3),(4) 式中左式不带有形如 (1)i 的系数,所以较 (1),(2) 式更为常用。

例题1:错排问题

题意

求长为 n{1,2,,n} 的排列 P 的数量,需要满足 i[1,n],Pii

解法

fi 为恰好有 i 个数满足 值 = 下标 的排列数,gi 为钦定有 i 个数满足 值 = 下标 的排列数;则显然有 gi=(ni)(ni)!gi=j=in(ji)fj。由二项式反演可得 fi=j=in(1)ji(ji)gj。故而 f0=j=0n(1)jgj=n!j=0n(1)jj!。这样同时变相地证明了容斥原理的一个公式。

例题2:CF1342E Placing Rooks

题意

n×n 的国际象棋棋盘上放 n 个车,要求满足两个条件:

  • 所有的空格子都能被至少一个车攻击到。
  • 恰好k 对车可以互相攻击到。

答案对 998244353 取模。1n2105,0kn(n1)2

解法

显然必须要每一行或每一列需要有车放置,否则一定存在有格子不会被任何车攻击到。同时若存在 k 行不放车(k0),由于车的总数为 n,故而必须在每一列放恰好一个车,同时一定有 k 对车能相互攻击;存在 k 列不放车同理。下面只讨论存在不放车的行的情况。

fi 为存在恰好 i 行不放车的方案数,gi 为钦定 i 行不放车的方案数;则 gi=j=infj(ji),同时由于可以任选 i 行且 n 个车均有 ni 种选择(可能存在有不放车的行),则 gi=(ni)(ni)n。故而 fi=j=in(1)jigj(ji)=j=in(1)ji(ji)(nj)(nj)n

代码

点此查看代码
#include <bits/stdc++.h>
using namespace std;
#define ll long long
const int maxn=200010;
const int md=998244353;
int n,i,b=1,t;
int fac[maxn],inv[maxn];
ll k,ans;
inline int Pow(int d,int z){
int ret=1;
do{
if(z&1) ret=(1LL*ret*d)%md;
d=(1LL*d*d)%md;
}while(z>>=1);
return ret;
}
inline int C(int x,int y){return ((1LL*fac[y]*inv[x])%md*inv[y-x])%md;}
int main(){
fac[0]=1;
for(i=1;i<maxn;++i) fac[i]=(1LL*fac[i-1]*i)%md;
inv[maxn-1]=Pow(fac[maxn-1],md-2);
for(i=maxn-1;i;--i) inv[i-1]=(1LL*inv[i]*i)%md;
scanf("%d%lld",&n,&k);
if(k>=n){
putchar('0');
return 0;
}
for(i=k;i<n;++i,b=-b) ans+=b*(((1LL*C(k,i)*C(i,n))%md)*Pow(n-i,n)%md);
printf("%lld",((ans+ans*(!!k))%md+md)%md);
return 0;
}

例题3:P6478 [NOI Online #2 提高组] 游戏

题意

给定一棵 2m 个节点的树,其中有 m 个白点,m 个黑点。现在要将黑白两点两两配对,对于 k[0,m],求恰好有 k 对点存在祖先后代关系的方案数,对 998244353 取模。1m2500

解法

fi 为恰好有 i 对黑白点配对的方案数,gi 为钦定 i 对黑白点配对的方案数(也就是说能够找出 i 对黑白点配对的方案数)。显然 gi=j=im(ji)fj

考虑 gi 的求法,可以使用树形 dp。设 dpu,p 表示在 u 的子树中配对了 p 对有祖先后代关系的黑白点的方案数,则在求 dpu 时,可以顺次把 u 的每个儿子的 dp 值加入 dpu,转移有 dpu,p+qdpu,p+q+dpu,pdpv,q。同时可以处理出 u 子树内(不包括 u 的)的黑点个数 Bu,白点个数 Wu;最后考虑 u 和哪些点配对时,若 u 为白点,则 p<Bu,dpu,p+1dpu,p+1+dpu,p(Bup)u 为黑点同理。注意:最后有 gi=(mi)!dp1,i ,未配对的点任意配对的方案需要算上。 时间复杂度显然为 O(m2)

代码

点此查看代码
#include <bits/stdc++.h>
using namespace std;
const int maxn=5010;
const int md=998244353;
int n,i,j,b,u,v,tot;
int C[maxn][maxn],fac[maxn];
int h[maxn],ver[maxn<<1],nxt[maxn<<1];
int dp[maxn][maxn],cnt[maxn][2],tmp[maxn];
long long ans;
bool col[maxn];
int dfs(const int p,const int f){
int lp,to,siz=1,ssi;
dp[p][0]=1;
for(lp=h[p];lp;lp=nxt[lp]){
to=ver[lp];
if(to==f) continue;
ssi=dfs(to,p);
for(i=0;i<=(siz>>1);++i)
for(j=0;j<=(ssi>>1);++j)
tmp[i+j]=(1LL*dp[p][i]*dp[to][j]+tmp[i+j])%md;
siz+=ssi;
cnt[p][0]+=cnt[to][0];
cnt[p][1]+=cnt[to][1];
for(i=0;i<=(siz>>1);++i){
dp[p][i]=tmp[i]%md;
tmp[i]=0;
}
}
++cnt[p][col[p]];i=cnt[p][!col[p]];j=1;
for(;i;--i) dp[p][i]=(1LL*dp[p][i-1]*(j++)+dp[p][i])%md;
return siz;
}
int main(){
C[0][0]=fac[0]=1;
for(i=1;i<maxn;++i){
C[0][i]=1;
fac[i]=(1LL*fac[i-1]*i)%md;
for(j=1;j<=i;++j){
C[j][i]=C[j][i-1]+C[j-1][i-1];
if(C[j][i]>=md) C[j][i]-=md;
}
}
scanf("%d",&n);getchar();
for(i=1;i<=n;++i) col[i]=getchar()-'0';
for(i=1;i<n;++i){
scanf("%d%d",&u,&v);
ver[++tot]=v;nxt[tot]=h[u];h[u]=tot;
ver[++tot]=u;nxt[tot]=h[v];h[v]=tot;
}
dfs(1,0);n>>=1;
for(i=0;i<=n;++i) dp[1][i]=(1LL*dp[1][i]*fac[n-i])%md;
for(i=0;i<=n;++i){
b=1;
for(j=i;j<=n;++j,b=-b) ans+=(1LL*b*dp[1][j]*C[i][j])%md;
printf("%lld\n",(ans%md+md)%md);
ans=0;
}
return 0;
}

例题4

题意

给定一棵 n 个点的有标号无根树,你可以进行至多 k 次操作,每次操作是删除一条边再加上一条边,使得这个图还是一棵树。问能得到多少种本质不同的有标号无根树,答案对 109+7 取模。n5000

解法

听说 TopCoder13369 TreeDistanceCF917D Stranger Trees 这个题很像。

啊这个东西需要 矩阵树定理

fi 为删去 i 条边,重新连接 i 条边 且不能选择删去的边进行连接 的能得到的不同树的数量;gi钦定 删去 i 条边再连接 i 条边能得到的不同树的数量。(此处的 钦定 为不考虑去重,例如删去任意 i 条边再连上同样的 i1 条边的同种方案会被考虑 i 次,故而最终答案是 i=0kfk)此时 gi=j=0i(ij)fj

考虑删去 k 条边会形成 k+1 个连通块,连接 k 条边会将这 k+1 个连通块变成 1 个;故而如果定好了删边和连边的方案,则具体到操作上可以先删除一条边,再把连接这条边的两个端点最后所在的连通块的边连上;保证了每次操作合法,即可以把删边和连边的操作分开进行。

考虑把这 k+1 个连通块连接成的树有多少种。


引入:Prüfer 序列

定义

对于一棵大小为 n 的有标号无根树(n2),可以进行若干次操作,每次操作可以把标号最小的叶子节点删除,同时在一个序列末端加入 这个点的父亲,直到最后树上只剩两个点。我们称形成的这个长为 n2 的序列为这棵树的 Prüfer 序列。

性质

显然最后整个序列值域为 [1,n] 且一棵树对应着唯一一个 Prüfer 序列。

Cayley 公式:n 个点的有标号无根树总共有 nn2 种。(n 个点的有标号完全图有 nn2 棵生成树)

证明:考虑如何从 Prüfer 序列反推出这棵树。

摘自 OI-wiki:据 Prüfer 序列的性质,我们可以得到原树上每个点的度数。然后你也可以得到度数最小的叶结点编号,而这个结点一定与 Prüfer 序列的第一个数连接。然后我们同时删掉这两个结点的度数。每次我们选择一个度数为 1 的最小的结点编号,与当前枚举到的 Prüfer 序列的点连接,然后同时减掉两个点的度。到最后我们剩下两个度数为 1 的点,其中一个是结点 n。把它们连接起来,则可以构造这棵唯一的树。

故而任意一个 Prüfer 序列均有对应的唯一的树,n 个点的有标号无根树总共有 nn2 种。


另外一个结论:有 n 棵有标号无根树,第 i 棵树大小为 ai,现在要把这 n 棵树用 n1 条边连接起来,得到的本质不同的有标号无根树数量为 (i=1nai)n2i=1nai

证明:模拟 Prüfer 序列的生成过程。我们可以把每个连通块的“权值”看成是其中最小的点的编号;然后在最后生成的树中每次删除“权值”最小的只与另外一个连通块连接的连通块 x,把与其连接的连通块的连向 x 的点加入一个序列中。可以发现序列的值域为 i=1nai,同时由 Prüfer 序列的构造树的方法可以构造一棵唯一的树;并且由于它没有维护每个连通块在成为叶子节点时向外连边的点(我们称其为连通块的代表元素),故而本质不同的有标号无根树数量为 (i=1nai)n2i=1nai

故而最后 gi 即为把树划分的所有连通块对应的 ni1j=1i+1aj 的和。考虑如何维护 j=1i+1aj

可以使用树形 dp。设 dpu,x,y 表示在 u 子树内划分了 x 个连通块(不包括 u 所在的未统计完的连通块),且 u 所在的连通块的大小为 y 的对应 j=1xaj。转移统计方式可以参考容斥原理部分的 ARC101E Ribbons on Tree 部分题解。同时时间复杂度还是 O(n4) 的。

考虑把 dp 数组的一维进行压缩。由于最后计算 gi 需要树上的连通块个数,所以只能压缩 y 所在一维。有一种不太容易想到的 dp 方式:设 dpu,x,y 为在 u 子树上,划分了 x 个连通块(包括 u 所在的未统计完的连通块),且 y 表示是否在 u 所在的连通块中选择了上述的代表元素;则转移为 dpu,x+i1,a|bdpu,x+i1,a|b+dpu,x,adpv,i,b(ab1)(合并 uv 所在连通块,注意两者所在的连通块不能同时有代表元素。可以用组合意义理解,也就是 j=1iai 等效于在每个连通块中选一个点的方案数),dpu,x+i,adpu,x+i,a+dpu,x,adpv,i,1(不合并 uv 所在的连通块,注意 v 所在的连通块必须要统计完成)。初值有 dpu,1,0=dpu,1,1=1。时间复杂度显然为 O(n2)

(11/23:终于把上面的一些错漏之处改正了,参考了 这篇题解

代码

点此查看代码
#include <bits/stdc++.h>
using namespace std;
const int maxn=5010;
const int md=1000000007;
int n,i,j,u,v,t;
int h[maxn],g[maxn],pw[maxn],nxt[maxn];
int f[maxn][2],dp[maxn][maxn][2],C[maxn][maxn];
inline void Add(int &x,int y){x-=((x+=y)>=md)*md;}
int dfs(int p){
dp[p][1][0]=dp[p][1][1]=1;
int to,sz,rt=1;
for(to=h[p];to;to=nxt[to]){
sz=dfs(to);
for(i=1;i<=rt;++i){
for(j=1;j<=sz;++j){
Add(f[i+j][0],(1LL*dp[p][i][0]*dp[to][j][1])%md);
Add(f[i+j][1],(1LL*dp[p][i][1]*dp[to][j][1])%md);
Add(f[i+j-1][0],(1LL*dp[p][i][0]*dp[to][j][0])%md);
Add(f[i+j-1][1],(1LL*dp[p][i][0]*dp[to][j][1]+
1LL*dp[p][i][1]*dp[to][j][0])%md);
}
}
rt+=sz;
memcpy(dp[p],f,sizeof(f));
memset(f,0,sizeof(f));
}
return rt;
}
class TreeDistance{
public:
int countTrees(vector<int> p,int k){
n=p.size()+1;
k=n-1-min(k,n-1);
for(i=2;i<=n;++i){
u=p[i-2]+1;
nxt[i]=h[u]; h[u]=i;
}
pw[0]=1;
for(i=0;i<=n;++i) C[0][i]=1;
for(i=1;i<=n;++i){
C[0][i]=1; pw[i]=(1LL*pw[i-1]*n)%md;
for(j=i;j<=n;++j)
Add(C[i][j],C[i][j-1]+C[i-1][j-1]);
}
dfs(1);
for(i=0;i<n-1;++i) g[i]=(1LL*dp[1][n-i][1]*pw[n-i-2])%md;
g[n-1]=1; u=0;
for(i=n-1;i>=k;--i){
for(j=i,v=0;j<n;++j){
t=(1LL*C[i][j]*g[j])%md;
if(v) t=md-t; Add(u,t); v=!v;
}
}
return u;
}
};

练习题:

DarkBzoj 2839 集合计数

考虑把交集之外的元素进行考虑,可以看作交集之外的 nk 个元素的分配方法分配给 p 个集合(p0),方法显然为 p=12nk(2nkp)=22nk1(分配方法包括不分配任何元素)。二项式反演即可。

注意:2nk 部分需要对 109+71 取模(2109+711 mod (109+7))。(这个部分卡了我很久)

P4859 已经没有什么好害怕的了

升序排序,模拟匹配过程。发现可以用双指针 + dp 得出钦定有 c 个糖果的能量大于药片的方案数,二项式反演即可。

P5505 分特产

考虑容斥。在至少有 c 个同学分不到特产的情况下,分开讨论每种特产的分配方案(即把无标号球放进有标号盒子且能有空盒的方案数)。

CF1228E Another Filling the Grid

讨论如何放 1 的情况比较困难(至少我只知道 O(n4) 的方法),可以讨论如何放 2k 的元素。然后容斥即可,做法可参考 CF997C

posted @   Fran-Cen  阅读(121)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!
点击右上角即可分享
微信分享提示