排列组合学习笔记

以下部分内容摘自OI Wiki

排列数

\(n\) 个数中选出 \(m\) 个数按照一定的顺序排列,用 \(A_{n}^{m}\) 表示。排列的计算公式如下:

\(A_{n}^{m}=n(n-1)(n-2)...(n-m+1)=\dfrac{n!}{(n-m)!}\)

组合数

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

\(C_{n}^{m}=\dfrac{n!}{m!(n-m)!}\)

特别地,当 \(m>n\) 时,\(A_{n}^{m}=C_{n}^{m}=0\)

插板法

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

正整数和的数目

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

这个问题的本质就是求 \(x_1+x_2+...+x_k=n\)正整数解的组数。

考虑在这些元素的 \(n-1\) 个空隙中插入 \(k-1\) 块板子,因为元素完全相同,那么答案就是 \(\binom{n-1}{k-1}\)

非负整数和的数目

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

本质就是求 \(x_1+x_2+...+x_k=n\)非负整数解的组数。

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

不同下界整数和的数目

问题三:在问题二的基础上,要求第 \(i\) 组的元素数量至少为 \(a_i\),满足 \(\sum a_i \leq n\)

即求满足 \(x_i \geq a_i\)\(x_1+x_2+...+x_k=n\) 的解的组数。

类比问题二,第 \(i\) 组先借来 \(a_i\) 个元素,令 \(x_i'=x_i-a_i\),那么就有 \(x_i' \geq 0\)。得到新方程:

\(x_1'+x_2'+...+x_k'=n-\sum a_i\)

那么问题转为求这个方程的非负整数解的组数。答案为 \(\binom{n-\sum a_i+k-1}{k-1}\)

不相邻的排列

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

设第 \(i\) 个数与第 \(i-1\) 个数的间隔\(x_i\),其中 \(x_1\) 表示第一个元素与 \(0\) 的间隔,\(x_{k+1}\) 表示最后一个元素与 \(n+1\) 的间隔。于是 \(x_1 \geq 0,x_{k+1} \geq 0\)\(x_i \geq 1(i=2,3,4...k)\)。得到方程:

\(x_1+x_2+...+x_{k+1}=n-k\)

\(x_1'=x_1+1\)\(x_{k+1}'=x_{k+1}+1\)

那么就转化为了问题一。答案就是 \(\binom{n-k+1}{k}\)

二项式定理

\((a+b)^n=\sum_{i=0}^{n}\binom{n}{i}a^ib^{n-i}\)

组合数的性质:

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

\(\binom{n}{m}=\binom{n}{n-m}\)\((1)\)

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

\(\binom{n}{k}=\frac{n}{k} \binom{n-1}{k-1}\)\((2)\)

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

\(\binom{n}{m}=\binom{n-1}{m-1}+\binom{n-1}{m}\)\((3)\)

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

\(\binom{n}{0}+\binom{n}{1}+...+\binom{n}{n}=\sum_{i=0}^{n} \binom{n}{i}=2^n\)\((4)\)

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

\(\sum_{i=0}^{n}(-1)^i\binom{n}{i}=[n==0]\)\((5)\)

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

\(\sum_{i=0}^{m} \binom{n}{i} \binom{m}{m-i}=\binom{n+m}{m}\)\((6)\)

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

\(\sum_{i=0}^{n} \binom{n}{i}^2=\binom{2n}{n}\)\((7)\)

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

\(\sum_{i=0}^{n} i \binom{n}{i}=n2^{n-1}\)\((8)\)

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

\(\sum_{i=0}^{n} \binom{i}{k}=\binom{n+1}{k+1}\)\((9)\)

\(n\) 个元素中选取 \(r\) 个元素,再在 \(r\) 中选取 \(k\) 个元素,等价于先选 \(k\) 个元素,再在剩下的 \(n-k\) 个元素中选取 \(r-k\) 个元素:

\(\binom{n}{r}\binom{r}[k]=\binom{n}{k}\binom{n-k}{r-k}\)\((10)\)

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

\(\sum_{i=0}^{n}\binom{n-i}{i}=F_{n+1}\)

其中 \(F\) 为斐波那契数列。

组合数问题

\(T\) 组询问,给定 \(n,m\)\(k\),对于所有的 \(0\leq i\leq n,0\leq j\leq \min \left ( i, m \right )\) 有多少对 \((i,j)\) 满足 \(k|\binom{i}{j}\)

\(0 \leq n, m \leq 2 \times 10^3\)\(1 \leq T \leq 10^4\)

思路

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

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

\(\binom{i}{j} =\left\{\begin{matrix} (\binom{i-1}{j-1}+\binom{i-1}{j}) \mod{k},j\in[1,i] \\ 1,j=0 \end{matrix}\right.\)

如果 \(\binom{i}{j}\) 在模 \(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\) 次询问,每次询问求:

\(\sum_{i=1}^n \sum_{j=1}^m C^i_j\),其中当\(i>j\)的时候,钦定\(C^i_j\)\(0\)

\(1 \leq q \leq 10000,1 \leq n,m \leq 1000\)

思路

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

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

\(\sum_{i=1}^n \sum_{j=1}^m \binom{j}{i}=\sum_{i=1}^{n} \binom{m+1}{i+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\times x\)\(x\) 为任意正整数)的矩形各无穷多个和一个 \(2\times n\) 的网格,请求出恰好选择其中 \(k\) 个矩形(可以选择相同的矩形)不重不漏地铺满整个网格的方案数。矩形可以旋转。

\(1\le n\le 2\times 10^7\)\(1\le k\le 5000\)

思路:

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

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

\(Ans=\sum_{i=0}^{b} \binom{a-1}{i-1}\binom{a-1}{b-i-1}\)

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

\(Ans=\binom{2a-2}{b-2}\)

回到原问题,考虑用 \(j\)\(1 \times 2\) 的长木板将原来的大木板分割成 \(i\)\(2 \times a_k\) 个小区间,再套用上面子问题的公式。

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

\(d_0+d_1+d_2+....+d_i=j\),其中 \(d_0,d_i \geq 0\)\(d_1,d_2...d_{i-1} \geq 1\)

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

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

\(a_1+a_2+...a_i=n-j\),其中 \(a_l \geq 1\)

再次应用插板法,得出这里的方案数为 \(\binom{n-j-1}{i-1}\)

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

\(\sum_{(\sum_{l=1}^{i} b_l) =k-j} \prod_{l=1}^{i} \binom{2a_l-2}{b_l-2}\)

考虑范德蒙德卷积,这里实际上可以看成是在 \(\sum (2a_l-2)\) 个空格中选择 \(\sum (b_l-2)\) 个空格插入,那么合起来的方案数就是: \(\binom{2n-2j-2i}{k-j-2i}\)

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

\(Ans=\sum_{j=0}^{k}\sum_{i=1}{k} \binom{2n-2j-2i}{k-j-2i} \binom{j+1}{i}\binom{n-j-1}{i-1}+[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\) 个办公室其中之一,那么这一部分的方案数就为 \(A_{n-m}^{n-i}\)

\(i\) 个办公室之前走的办公室,由于我们已经将 \(i \sim n\) 的顺序确定好了,那么这一部分的方案数就是 \(A_{i-1}^{i-1}\)

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

\(ans=\sum_{i=1}^{n} i \times m \times \binom{n-m}{n-i} \times (n-i)! \times (i-1)!\)

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\)。一开始,所有 \(a_i\) 均为 \(0\)。给出一个同样长度为 \(n\) 的目标数组 \(b\)。求有多少种方案,使得通过若干次以下操作,可以让 \(a\) 数组变成 \(b\)

  • 选出两个不同的下标 \(1\leq i<j\leq n\),并将 \(a_i\)\(a_j\) 同时增加 \(1\)

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

答案对 \(\bm{998244353}\) 取模。

\(1\le n\le5~000\)\(1\leq b_i\le30~000\)\(\sum b_i\le30~000\)

思路:

注意到总的操作次数一定为 \(\frac{\sum b_i}{2}\),令 \(m=\frac{\sum b_i}{2}\)。由于直接统计每次操作的下标比较复杂,考虑依次将 \(b_i\) 个数填入到每次操作中。

\(s[i]=\sum_{k=1}^{i} b_i\)。设 \(f[i][j]\) 表示将前 \(s_i\) 个数全部填入操作之后,有 \(j\) 个操作已经被填入 \(2\) 个数。同时也就可以得出,当前有 $k=s[i]-j*2 $ 个操作被填入了 \(1\) 个数,剩下的 \(l=m-j-k\) 个操作还没有被填入过数字。

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

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\),有:

\(\binom{n}{m} \mod {p}=\binom{\lfloor n/p \rfloor}{\lfloor m/p \rfloor}\binom{n \mod {p}}{m \mod {p}} \mod {p}\)

吉夫特

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

思路:

应用 Lucas 定理, \(\binom{a_i}{a_j} \mod {2}=\binom{\lfloor a_i/2 \rfloor}{\lfloor a_j/2 \rfloor}\binom{a_i \mod {2}}{a_j \mod {2}} \mod {2}\)

对于后面的 \(\binom{a_i \mod {2}}{a_j \mod {2}}\),其取值有 \(\binom{1}{1},\binom{1}{0},\binom{0}{1},\binom{0}{0}\) 四种,其中只有 \(\binom{0}{1}=0\)

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

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;
}

排列计数

称一个 \(1 \sim n\) 的排列 \(p_1,p_2, \dots ,p_n\) 是 Magic 的,当且仅当

\[\forall i \in [2,n],p_i > p_{\lfloor i/2 \rfloor} \]

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

\(1\le n \le 10^6\), \(1\le m \le 10^9\)\(m\) 是一个质数。

思路:

考虑将排列中的大小关系构成一棵二叉树,那么对于 \(i\) 号节点,它的两个儿子分别为 \(2i\)\(2i+1\)。表示 \(p_i<p_{2i}\)\(p_i<p_{2i+1}\)

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

\(f[i]=\binom{siz[i-1]}{siz[i*2]}*f[2*i]*f[2*i+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 @ 2023-03-02 21:18  曙诚  阅读(339)  评论(0)    收藏  举报