排列组合学习笔记

以下部分内容摘自OI Wiki

排列数

n 个数中选出 m 个数按照一定的顺序排列,用 Anm 表示。排列的计算公式如下:

Anm=n(n1)(n2)...(nm+1)=n!(nm)!

组合数

n 个不同的元素中,选出 m 个元素组成一个集合,用 Cnm 表示,也常用 (nm) 表示。组合的计算公式如下:

Cnm=n!m!(nm)!

特别地,当 m>n 时,Anm=Cnm=0

插板法

插板法(Stars and bars)是用于求一类给相同元素分组的方案数的一种技巧,也可以用于求一类线性不定方程的解的组数。

正整数和的数目

问题一:现有 n 个 完全相同的元素,要求将其分为 k 组,保证每组至少有一个元素,一共有多少种分法?

这个问题的本质就是求 x1+x2+...+xk=n正整数解的组数。

考虑在这些元素的 n1 个空隙中插入 k1 块板子,因为元素完全相同,那么答案就是 (n1k1)

非负整数和的数目

问题二:在问题一的基础上,允许每组元素为空。

本质就是求 x1+x2+...+xk=n非负整数解的组数。

考虑先借来 k 个元素,在 n+k1 个空隙中插入 k1 个空格。最后再把借来的元素从每组中删去,即可满足题意。答案为 (n+k1k1)

不同下界整数和的数目

问题三:在问题二的基础上,要求第 i 组的元素数量至少为 ai,满足 ain

即求满足 xiaix1+x2+...+xk=n 的解的组数。

类比问题二,第 i 组先借来 ai 个元素,令 xi=xiai,那么就有 xi0。得到新方程:

x1+x2+...+xk=nai

那么问题转为求这个方程的非负整数解的组数。答案为 (nai+k1k1)

不相邻的排列

问题四:从 1n 中选 k 个不相邻的数,求方案数。

设第 i 个数与第 i1 个数的间隔xi,其中 x1 表示第一个元素与 0 的间隔,xk+1 表示最后一个元素与 n+1 的间隔。于是 x10,xk+10xi1(i=2,3,4...k)。得到方程:

x1+x2+...+xk+1=nk

x1=x1+1xk+1=xk+1+1

那么就转化为了问题一。答案就是 (nk+1k)

二项式定理

(a+b)n=i=0n(ni)aibni

组合数的性质:

将选出的集合对全集取补集,数值不变:

(nm)=(nnm)(1)

根据定义可以推出吸收恒等式

(nk)=nk(n1k1)(2)

用杨辉三角的表达式,可以推出:

(nm)=(n1m1)+(n1m)(3)

取二项式定理中 a=b=1 的特殊情况,可以得到:

(n0)+(n1)+...+(nn)=i=0n(ni)=2n(4)

同理,取二项式定理中 a=1,b=1 的特殊情况,可以得到:

i=0n(1)i(ni)=[n==0](5)

拆式子,感性理解一下可以得到范德蒙德卷积

i=0m(ni)(mmi)=(n+mm)(6)

(6)n=m 的特殊情况,可以得到:

i=0n(ni)2=(2nn)(7)

通过对 (4) 所对应的多项式函数求导,可以得到:

i=0ni(ni)=n2n1(8)

1n+1 中选出 k+1 个元素,可以考虑枚举最大的元素的值,那么就得到等式:

i=0n(ik)=(n+1k+1)(9)

n 个元素中选取 r 个元素,再在 r 中选取 k 个元素,等价于先选 k 个元素,再在剩下的 nk 个元素中选取 rk 个元素:

(nr)(r[)k]=(nk)(nkrk)(10)

而从杨辉三角上也不难发现:

i=0n(nii)=Fn+1

其中 F 为斐波那契数列。

组合数问题

T 组询问,给定 n,mk,对于所有的 0in,0jmin(i,m) 有多少对 (i,j) 满足 k|(ij)

0n,m2×1031T104

思路

注意到 n,m 的范围,同时 k 对于每一组询问相同。可以考虑 O(nm) 预处理出答案,O(1) 询问。

根据组合数在模 k 意义下的递推公式:

(ij)={((i1j1)+(i1j))modk,j[1,i]1,j=0

如果 (ij) 在模 k 意义下为 0,那么显然就是一对满足题意的数对。只需要处理前缀和即可。

code:

#include<bits/stdc++.h>
using namespace std;
const int M=2e3+5;
int c[M][M],s[M][M],t,k,n,m;
int main()
{
    cin>>t>>k;
    for(int i=1;i<M;i++) c[i][0]=c[i][i]=1;
    for(int i=1;i<M;i++)
        for(int j=1;j<i;j++) c[i][j]=(c[i-1][j]+c[i-1][j-1])%k;
    for(int i=1;i<M;i++)
    {
        for(int j=1;j<=i;j++)
        {
            s[i][j]=s[i-1][j]+s[i][j-1]-s[i-1][j-1];
            if(c[i][j]==0) s[i][j]++;
        }
        s[i][i+1]=s[i][i];
    }
    while(t--)
    {
        cin>>n>>m;
        if(n<m) m=n;
        cout<<s[n][m]<<endl;
    }
}

回忆京都

q 次询问,每次询问求:

i=1nj=1mCji,其中当i>j的时候,钦定Cji0

1q10000,1n,m1000

思路

可以直接预处理出 n,m 范围内的组合数,再用前缀和来统计答案。那么就是 O(nm) 预处理,单次询问的复杂度为 O(1)

但我们也可以利用上面得到的等式 (9),对原式进行变换:

i=1nj=1m(ji)=i=1n(m+1i+1)

那么也可以做到 O(qn)

code:

#include<cstdio>
using namespace std;
const int mod=19260817,N=1050;
int c[N][N],s[N][N],n,m,q;
int main()
{
	for(int i=0;i<N;i++)
	    for(int j=0;j<=i;j++)
	        if(!j) c[i][j]=1;
	        else c[i][j]=(c[i-1][j-1]+c[i-1][j])%mod;
	for(int i=1;i<N;i++) for(int j=1;j<N;j++) s[i][j]=(s[i][j]+s[i-1][j]+s[i][j-1]-s[i-1][j-1]+c[i][j]+mod)%mod;
	scanf("%d",&q);
	while(q--)
	{
		scanf("%d%d",&m,&n);printf("%d\n",s[n][m]);
	}
	return 0;
}

一个仇的复

你有 1×xx 为任意正整数)的矩形各无穷多个和一个 2×n 的网格,请求出恰好选择其中 k 个矩形(可以选择相同的矩形)不重不漏地铺满整个网格的方案数。矩形可以旋转。

1n2×1071k5000

思路:

首先可以考虑原问题的简化版,用 b横着的长方形铺满 2×a 的方格的方案数。

考虑上面一行放置 i 个长方形,那么下面一行就放置 bi 个长方形,利用插板法,可以得到方案数为:

Ans=i=0b(a1i1)(a1bi1)

根据等式 (6),即范德蒙德卷积,原式可以简化为:

Ans=(2a2b2)

回到原问题,考虑用 j1×2 的长木板将原来的大木板分割成 i2×ak 个小区间,再套用上面子问题的公式。

设第 l 个分割的地方有 dl 个长方形,规定 d0 表示第左边界开始的木块数,di 表示右边界开始的木块数,那么就可以得到下述方程:

d0+d1+d2+....+di=j,其中 d0,di0d1,d2...di11

应用插板法,不难得出这里的方案数为:(j+1i)

其次,假设分割后第 l 块区域的长度为 al,可以得到方程:

a1+a2+...ai=nj,其中 al1

再次应用插板法,得出这里的方案数为 (nj1i1)

最后考虑填满剩下的 i 个小区间,套用最开始推出的公式,可以得到:

(l=1ibl)=kjl=1i(2al2bl2)

考虑范德蒙德卷积,这里实际上可以看成是在 (2al2) 个空格中选择 (bl2) 个空格插入,那么合起来的方案数就是: (2n2j2ikj2i)

最终还需要考虑特判 n=k 的情况,只有这种情况下可以用 k1×2 的长方形竖着填满大木板。那么最终的答案就是:

Ans=j=0ki=1k(2n2j2ikj2i)(j+1i)(nj1i1)+[n==k]

code:

#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int N=4e7+10,mod=998244353;
int fac[N],infac[N],inv[N],n,k,ans;
void init()
{
	fac[0]=infac[0]=fac[1]=infac[1]=inv[0]=inv[1]=1;
	for(int i=2;i<N;i++) fac[i]=1ll*fac[i-1]*i%mod;
	for(int i=2;i<N;i++) inv[i]=1ll*(mod-mod/i)*inv[mod%i]%mod;
	for(int i=2;i<N;i++) infac[i]=1ll*infac[i-1]*inv[i]%mod;
}
int C(int n,int m){if(n<0||m<0||n<m) return 0;return 1ll*fac[n]*infac[m]%mod*infac[n-m]%mod;}
void add(int &a,int b){a+=b;if(a>=mod) a-=mod;}
int main()
{
	scanf("%d%d",&n,&k);init();ans=(n==k);
	for(int j=0;j<=k;j++) for(int i=1;i<=k;i++) add(ans,1ll*C(2*n-2*j-2*i,k-j-2*i)*C(j+1,i)%mod*C(n-j-1,i-1)%mod);
	printf("%d\n",ans);
	return 0;
}

游戏

题意比较复杂,可以看原题面。

思路:

首先不难想到枚举 t(p) 的取值来计算贡献。设 t(p)=i,如果顺序枚举走的办公室,比较困难,可以考虑枚举第 i 个走到的办公室。由于之后走的办公室都已经被提醒过,那么第 i 次走到的办公室一定不会被 [l,r] 中的任意一个办公室提醒,换言之,第 i 次走的办公室在 [l,r] 区间中不存在约束。可以直接用埃氏筛预处理出这部分数字,设一共有 m 个这样的数。

i 个办公室可以在 m 中任意取,方案数为 m

i 个办公室之后走的办公室,显然就不能是这 m 个办公室其中之一,那么这一部分的方案数就为 Anmni

i 个办公室之前走的办公室,由于我们已经将 in 的顺序确定好了,那么这一部分的方案数就是 Ai1i1

最后别忘了乘以贡献 i。最终的表达式为:

ans=i=1ni×m×(nmni)×(ni)!×(i1)!

code:

#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int N=1e7+10,mod=1e9+7;
int fac[N],infac[N],inv[N],l,r,m,ans;bool vis[N];
void init()
{
	fac[0]=infac[0]=fac[1]=infac[1]=inv[0]=inv[1]=1;
	for(int i=2;i<=r;i++) fac[i]=1ll*fac[i-1]*i%mod;
	for(int i=2;i<=r;i++) inv[i]=1ll*(mod-mod/i)*inv[mod%i]%mod;
	for(int i=2;i<=r;i++) infac[i]=1ll*infac[i-1]*inv[i]%mod;
	for(int i=l;i<=r;i++)
    {
        if(vis[i]) continue;m++;
        for(int j=i;j<=r;j+=i) vis[j]=1;
    }
}
int C(int n,int m){if(n<0||m<0||n<m) return 0;return 1ll*fac[n]*infac[m]%mod*infac[n-m]%mod;}
void add(int &a,int b){a+=b;if(a>=mod) a-=mod;}
int main()
{
	scanf("%d%d",&l,&r);init();int n=r-l+1;
	for(int i=1;i<=n;i++) add(ans,1ll*i*m%mod*C(n-m,n-i)%mod*fac[n-i]%mod*fac[i-1]%mod);printf("%d\n",ans);
	return 0;
}

构造数组

你现在有一个长度为 n 的数组 a。一开始,所有 ai 均为 0。给出一个同样长度为 n 的目标数组 b。求有多少种方案,使得通过若干次以下操作,可以让 a 数组变成 b

  • 选出两个不同的下标 1i<jn,并将 aiaj 同时增加 1

两种方案被称之为不同的,当且仅当存在一个 x 使得一种方案中第 x 次操作选择的两个下标 (i,j) 与另一种方案中的不同。

答案对 998244353 取模。

1n5 0001bi30 000bi30 000

思路:

注意到总的操作次数一定为 bi2,令 m=bi2。由于直接统计每次操作的下标比较复杂,考虑依次将 bi 个数填入到每次操作中。

s[i]=k=1ibi。设 f[i][j] 表示将前 si 个数全部填入操作之后,有 j 个操作已经被填入 2 个数。同时也就可以得出,当前有 k=s[i]j2 个操作被填入了 1 个数,剩下的 l=mjk 个操作还没有被填入过数字。

考虑枚举将当前 bi 个数全部填入操作后,新增加了 x 个被全部填满的操作。不难发现,这些操作在此之前已经被填入过 1 个数,那么剩下的 bix 个数就被填入没有被填过数字的操作中。对答案的贡献就是 (kx)(lbix)

code:

#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int N=5050,M=30010,mod=998244353;
int n,m,b[N],s[N],f[2][M],fac[M],infac[M],inv[M];
void init()
{
	fac[0]=infac[0]=fac[1]=infac[1]=inv[0]=inv[1]=1;
	for(int i=2;i<M;i++) fac[i]=1ll*fac[i-1]*i%mod;
	for(int i=2;i<M;i++) inv[i]=1ll*(mod-mod/i)*inv[mod%i]%mod;
	for(int i=2;i<M;i++) infac[i]=1ll*infac[i-1]*inv[i]%mod;
}
int C(int n,int m){if(n<0||m<0||n<m) return 0;return 1ll*fac[n]*infac[m]%mod*infac[n-m]%mod;}
void add(int &a,int b){a+=b;if(a>=mod) a-=mod;}
int main()
{
	scanf("%d",&n);for(int i=1;i<=n;i++) scanf("%d",&b[i]),s[i]=s[i-1]+b[i];if(s[n]%2) return puts("0"),0;
	m=s[n]/2;f[0][0]=1;init();
	for(int i=1;i<=n;i++)
	{
		for(int j=0;j<=(s[i-1])/2;j++)
		{
			int c2=j,c1=s[i-1]-j*2,c0=m-c2-c1;
			if(c0<0) continue;f[i&1][j]=0;
		}
		for(int j=0;j<=(s[i-1])/2;j++)
		{
			int c2=j,c1=s[i-1]-j*2,c0=m-c2-c1;if(c0<0) continue;
			for(int k=0;k<=b[i];k++)
			{
				if(c1<k||c0<b[i]-k) continue;
				if(f[i-1&1][c2]) add(f[i&1][c2+k],1ll*f[i-1&1][c2]*C(c1,k)%mod*C(c0,b[i]-k)%mod);
			}
		}
	}
	printf("%d\n",f[n&1][m]);
	return 0;
}

Lucas定理

对于质数 p,有:

(nm)modp=(n/pm/p)(nmodpmmodp)modp

吉夫特

题意比较繁琐,可以看原题题面。

思路:

应用 Lucas 定理, (aiaj)mod2=(ai/2aj/2)(aimod2ajmod2)mod2

对于后面的 (aimod2ajmod2),其取值有 (11),(10),(01),(00) 四种,其中只有 (01)=0

不难发现,应用 Lucas 定理后就是将 aiaj 在二进制下的每一位单独拎出来,为了避免出现 (01) 的情况,需要满足在二进制下,ajai 的子集。那么就可以用枚举子集的方法记录以每一个数结尾的方案数,最终的复杂度为 O(3log2max(ai))

code:

#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int N=3e5+10,mod=1e9+7;
int n,x,f[N],ans;
int main()
{
	scanf("%d",&n);
	for(int i=1;i<=n;++i)
	{
		scanf("%d",&x);
		for(int j=x-1&x;j;j=j-1&x) f[j]=(f[j]+f[x]+1)%mod;
		ans=(ans+f[x])%mod;
	}
	printf("%d\n",ans);
	return 0;
}

排列计数

称一个 1n 的排列 p1,p2,,pn 是 Magic 的,当且仅当

i[2,n],pi>pi/2

计算 1n 的排列中有多少是 Magic 的,答案对 m 取模。

1n106, 1m109m 是一个质数。

思路:

考虑将排列中的大小关系构成一棵二叉树,那么对于 i 号节点,它的两个儿子分别为 2i2i+1。表示 pi<p2ipi<p2i+1

注意到左右儿子中的节点互不影响,且当前节点是最小的点,假设 siz[i] 表示 i 子树的大小。f[i] 表示填满子树 i 的方案数,不难得出表达式:

f[i]=(siz[i1]siz[i2])f[2i]f[2i+1]

需要注意,本题中可能存在 n>m 的情况,所以求组合数要用到Lucas。

code:

#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int N=2e6+10;
int fac[N],infac[N],n,inv[N],mod,ans,siz[N];
void init()
{
	fac[0]=infac[0]=fac[1]=infac[1]=inv[0]=inv[1]=1;
	for(int i=2;i<=n;i++) fac[i]=1ll*fac[i-1]*i%mod;
	for(int i=2;i<=n;i++) inv[i]=1ll*(mod-mod/i)*inv[mod%i]%mod;
	for(int i=2;i<=n;i++) infac[i]=1ll*infac[i-1]*inv[i]%mod;
}
int C(int n,int m){if(n<0||m<0||n<m) return 0;return 1ll*fac[n]*infac[m]%mod*infac[n-m]%mod;}
int lucas(int n,int m){return m?1ll*C(n%mod,m%mod)*lucas(n/mod,m/mod)%mod:1; }
void dfs(int u){siz[u]=1;for(int v=u*2;v<=u*2+1;v++) if(v<=n) dfs(v),siz[u]+=siz[v];}
int calc(int u){if(u>n) return 1;return 1ll*lucas(siz[u]-1,siz[u*2])*calc(u*2)%mod*calc(u*2+1)%mod;}
void add(int &a,int b){a+=b;if(a>=mod) a-=mod;}
int main()
{
	scanf("%d%d",&n,&mod);init();dfs(1);printf("%d\n",calc(1));
	return 0;
}
posted @   曙诚  阅读(253)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】
点击右上角即可分享
微信分享提示