组合数学

一、加乘原理

加法原理

一件事,有 n 类方法可以实现它,第 i 类方法有 a[i] 种方法实现,那么总共有 i=1na[i] 种方法实现。

乘法原理

一件事,有 n 个步骤可以实现它,第 i 个步骤有 a[i] 种方法实现,那么总共有 i=1na[i] 种方法实现。

二、排列数与组合数

排列数

n 个不同的数中任选 m 个数,按照一定顺序排成一列的方案数,记作Anm,计算公式为:

Anm=n!m!(nm)!

组合数

n 个不同的数中任选 m 个数组成一个集合的方案数,记作Cnm,计算公式为:

Cnm=n!(nm)!

也可以记为:

(nm)

读作:nm

三、组合数的性质

性质一

Cnm=Cnnm

证明:从 n 个数中选 m 个相当于不选 nm 个。

性质二

Cnm=Cn1m1+Cn1m

证明:对于最后一个元素,如果不选它,那么在剩下的 n1 个元素中选 m 个元素;否则再剩下的 n1 个元素中选 m1 个元素。

这是组合数的递推公式。

性质三

i=0mCmi=2m

证明:根据二项式定理:

(x+y)n=i=0nCnixiyni

显然,i=0mCmi=(1+1)m=2m

性质四

i=0m(1)iCmi=0

证明:若 m 为奇数,则由性质一可知正确

m 为偶数,利用性质二的递推公式,则

i=0m(1)iCmi

=i=0m(1)i(Cm1i+Cm1i1)

=Cm10Cm10Cm11+Cm11+Cm12...+Cm1m1=0

P3197 [HNOI2008] 越狱

运用“正难则反”思想,问题可以转化为“所有状态减去任意相邻犯人宗教都不同的状态”。

先考虑所有状态。每个人信仰的宗教有 m 种可能,且相互无影响,那么 n 个人就有 mn 种可能。

再考虑任意相邻的犯人宗教不同的状态。只考虑第一个人,他信仰的宗教有 m 种可能。如果增加一个犯人,那么他信仰的宗教不能与前面相同,有 m1 种可能。一共有 m×(m1)n1

综上,答案就是 mnm×(m1)n1

P2822 [NOIP2016 提高组] 组合数问题

利用性质二递推求得组合数,然后再对 k 取模,如果模数为 0 说明能被 k 整除。然后再做一个二维前缀和,询问的时候直接 0(1) 输出即可。

P1350 车的放置

将原图分为三个矩形,分别是左上角,左下角,右下角的矩形。

然后先考虑对于 n×m 的矩形,放 k 个不互相攻击的车的方案数。每放一个车,矩形就少了一行,一列能放车的位置,再去重,答案就是

Ank×Amkk!

再考虑原题的答案。假设左下角的矩形放了 i 个车,左上角的矩形放了 j 个车,那么右下角的矩形放了 kij 个车。对于左下角的矩形,它的车会使得左上角的矩形能放车的位置少了 i 列,右下角的矩形能放车的位置少了 i 行。因此,最终答案是:

i=0kj=0kiAai Adi Aaij Abj Ackji Adikiji! j! (kij)!

P3166 [CQOI2014] 数三角形

solution1

问题可以转化为:任选三个点的方案数 三点共线的方案数。

任选三点的方案数: n×m 的方格共有 (n+1)×(m+1) 个点,所以方案数为 C(n+1)×(m+1)3

三点共线的方案数:

先考虑平行于 x 轴或 y 轴的方案数,显然为 Cn+13×(m+1)+Cm+13×(n+1)

再考虑剩下的方案数。三点所在的直线斜率可正可负,但斜率为正的方案数与斜率为负的方案数相等,所以先只考虑斜率为正的方案数,再乘以 2

引理:对于两个点,如果它们的横坐标差为 i ,纵坐标差为 j ,那么它们确定的线段上格点(包括两个端点)的数量为 gcd(i,j)+1

而我们要求的是三点共线的方案数,相当于求固定两个点时,中间的第三个点的数量,所以应该是 gcd(i,j)1 。因此每次枚举横坐标差,纵坐标差,答案就是:

i=1nj=1m(ni+1)×(mj+1)×(gcd(i,j)1)

乘以 (ni+1)×(mj+1) 的含义是,横坐标差为 i ,纵坐标差为 j 的点对共有这么多个。

时间复杂度: O(nm) ,足以通过本题。

solution2

但这还不够!

看到 gcd(i,j) ,可以考虑用欧拉反演优化。

i=1nj=1m(ni+1)×(mj+1)×(gcd(i,j)1)

=i=1nj=1m(ni+1)×(mj+1)×(k|gcd(i,j)ϕ(k)1)

=n(n+1)m(m+1)4+d=1min(n,m)ϕ(d)×[nd(n+1)dnd(nd+1)2][md(m+1)dmd(md+1)2]

时间复杂度 O(min(n,m)) ,通过本题绰绰有余。

code:

int work(int d){
    return ((m/d)*(m+1)-d*((m/d)*(m/d+1)/2))*((n/d)*(n+1)-d*((n/d)*(n/d+1)/2));
}
signed main(){
    pre_work();
    scanf("%lld%lld",&n,&m);
    for(int d=1;d<=min(n,m);++d)
        ans=(ans+(phi[d]*work(d)));
    ans-=(n*(n+1)/2)*(m*(m+1)/2);
    ans=ans*2;
    ans=(ans+((n+1)*c(m+1,3)+(m+1)*c(n+1,3)));
    ans=(c((n+1)*(m+1),3)-ans);
    printf("%lld\n",ans);
    return 0;
}

四、容斥原理

S1,S2,S3... 为有限集合,|S|表示集合大小,则

|i=1nSi|=i=1n|Si|1<=i<j<=nn|SiSj|+1<=i<j<k<=nn|SiSjSk|+...+(1)n1|S1S2...Sn|

P1450 硬币购物

问题可以转化为:选任意多的硬币购买的方案数 不符题意的方案数

对于前半部分,只需要预处理一个完全背包即可。

对于后半部分,先状态压缩枚举哪些硬币用的次数超过限制,然后可以强制让它们选 d[i]+1 个,来代表它们是超过限制的,而此时还需要凑 s(d[i]+1)×c[i] 的钱,还需要凑的钱任意凑即可。

code:

while(n--){
	scanf("%lld%lld%lld%lld%lld",&d[1],&d[2],&d[3],&d[4],&s);
	ans=0;
	for(int i=0;i<(1<<4);++i){
		int tmp=i,num=1,cnt=0,sum=0;
		while(tmp){
			if(tmp&1) ++cnt,sum+=(1+d[num])*c[num];
			tmp>>=1;++num;
		}
		if(s-sum>=0){
			if(cnt&1) ans-=f[s-sum];
			else ans+=f[s-sum];
		} 
	}
	printf("%lld\n",ans);
}

五、二项式反演

形式 0

f(n)=i=0n(1)iCnig(i)g(n)=i=0n(1)iCnif(i)

形式 1

f(n)=i=0nCnig(i)g(n)=i=0n(1)niCnif(i)

形式 2

f(n)=i=nmCing(i)g(n)=i=nm(1)inCinf(i)

其中形式1与形式2较为常用。形式1常用于“至多”与“恰好”的相互转化,形式2常用与“至少”与“恰好”的相互转化。

P6478 [NOI Online #2 提高组] 游戏

题目中要求的“恰好”,直接求较难,可以转化为“至少”。令 g(k) 表示“恰好有 k 次非平局”的方案数,f(k) 表示“钦定 k 次非平局”(也就是至少有 k 次非平局)的方案数。根据二项式反演的形式二,有:

f(k)=i=kmCikg(i)g(k)=i=km(1)ikCikf(i)

因此,只需要求出 f ,就能求出 g

f 可以用树形 DP 。令 dp[i][j] 表示,以 i 为根的子树中,至少有 j 次非平局的方案数。

如果只考虑子树,那么有状态转移方程:

dp[i][j]=k1+k2+...+kn=jdp[son1][k1]×dp[son2][k2]...×dp[sonn][kn]

用树形背包即可。

然后再考虑第 i 个点的贡献。设 A[i],B[i] 分别表示以 i 为根的子树内,属于小 A 和小 B 的点的个数。假设节点 i 属于小 A ,则有状态转移方程:

dp[i][j]=dp[i][j]+dp[i][j1]×max{B[i](j1),0}

含义是,小 A 用一次 i 节点,小 B 用一次 i 子树内的节点,构成一次非平局。因为已经有了 j1 次非平局,所以 B[i] 要减去 (j1) ,并检查它减完以后是否大于 0 。注意这个转移需要倒序转移,因为转移时用的是转移前的 dp[i][j1]

求出 dp 以后,有 f[k]=dp[1][k]×(mk)! ,含义是钦定了 k 局非平局以后,剩下的 mk 局自由组合。

code:

void dfs(int x,int fa){//预处理出a[i]和b[i]
    a[x]=(s[x-1]=='0');
    b[x]=1-a[x];
    for(int i=head[x];i;i=nxt[i])
        if(ver[i]!=fa){
            dfs(ver[i],x);
            a[x]+=a[ver[i]];b[x]+=b[ver[i]];
        }
}
void solve(int x,int fa){//DP部分
    f[x][0]=1;siz[x]=0;//siz[x]不是子树大小,而是状态转移的上界
    for(int i=head[x];i;i=nxt[i])
        if(ver[i]!=fa){
            solve(ver[i],x);
            for(int j=0;j<=siz[x]+siz[ver[i]];++j)
                g[j]=f[x][j],f[x][j]=0;
            for(int j=0;j<=siz[x];++j)
                for(int k=0;k<=siz[ver[i]];++k)
                    f[x][j+k]=(f[x][j+k]+(g[j]*f[ver[i]][k]%mod))%mod;
            siz[x]+=siz[ver[i]];

        }
    int tmp=((s[x-1]=='0')?b[x]:a[x]);
    for(int i=siz[x];i>=0;--i)
        f[x][i+1]=(f[x][i+1]+(f[x][i]*max(0ll,tmp-i)%mod))%mod;
    if(f[x][siz[x]+1])
        ++siz[x];
}

六、斯特林数

第一类斯特林数

n 个有标号元素放入 m 个相同的环当中的方案数。(环可以旋转)

递推公式:

[nm]=[n1m1]+(n1)[n1m]

其含义是:考虑最后一个元素,它可以放到前面的环中,也可以新开一个环

性质一

i=1n[ni]=n!

一个置换会有若干个环,而置换的总数是 n!

性质二

xn=i=0n[ni]xi

证明可以考虑归纳法,然后结合递推公式。这个性质给出了同一行的第一类斯特林数的生成函数。

P5408 第一类斯特林数·行

由性质二可知,只需要求出 xn 的各项系数即可。

考虑倍增,加上已经求出了 xn ,那么接下来考虑求解 x2n

x2n=xn

P5395 第二类斯特林数·行

n 个不同元素放入 m 个相同集合(非空)的方案数。

首先考虑把 n 个不同元素放入 m 个不同集合(可以有空集)的方案数。每个元素有 m 种放法,所以方案数为 mn

上述问题可以看成“至多有 m 个空集”的情况,而斯特林数是“恰好有0个空集”的情况。因此,有

mn=i=0m{ni}i!Cmi

其中,i!是指这 m 个集合的全排列,将相同的集合转化为不同的集合。

根据二项式反演的形式一,有:

{nm}m!=i=0min(1)mim!i!(mi)!

也就是:

{nm}=i=0min(1)mii!(mi)!

然后令F(x)=i=0nini!G(x)=i=0n(1)ii!,于是{nm}的值就是FG的第m项的系数。NTT计算多项式乘法即可。

code:

signed main(){
    scanf("%lld",&n);
    inv[0]=1;
    for(int i=1;i<=n;++i)
        inv[i]=inv[i-1]*fpow(i,mod-2)%mod;
    int w=1;
    for(int i=0;i<=n;++i){
        a[i]=fpow(i,n)*inv[i]%mod;
        b[i]=(w*inv[i]+mod)%mod;
        w*=-1;
    }
    mul(a,b,n);//多项式乘法
    for(int i=0;i<=n;++i)
        printf("%lld ",a[i]);
    printf("\n");
    return 0;
}
posted @   andy_lz  阅读(7)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】
点击右上角即可分享
微信分享提示