• 组合数学小结


    组合数学

    一些公式

    • 组合数定义: (C_n^m=frac{n!}{m!(n-m)!}) .意义是从 (n) 个元素中选出 (m) 个元素的方案数,可记作(n choose m).
    • 常用性质: (C_n^m=C_n^{n-m},C_n^0=1,C_n^1=n,C_n^n=1,C_n^m=0 when m>n).
    • 递推公式:(C_n^m=C_{n-1}^m+C_{n-1}^{m-1}).不选最后一个元素的方案数加选最后一个元素的方案数.
    • 求和:(sum_{i=0}^n {m+ichoose i}={m+n+1choose n}).(sum_{i=0}^n {ichoose m}={n+1choose m+1}).
    • 二项式定理:((a+b)^n=sum_{i=0}^n {nchoose i}*a^i*b^{n-i}).
    • 牛顿广义二项式定理:((a+b)^alpha=sum_{i=0}^{infty} {alphachoose i}a^k b^{alpha-k},alpha in mathbb{R}).({alphachoose i}=frac{(alpha)(alpha-1)...(alpha-k-1)}{i!}).这个在做生成函数的时候会用到.
    • (Lucas) 定理:({nchoose m} equiv {n\%p choose m\%p}*{lfloor n/p floor choose lfloor m/p floor} (mod p),pin P).

    常用求组合数方法

    • 由于组合数涉及到阶乘, (n,m) 稍大一些结果就会很大,一般在模意义下求,下面只在模 (p) 意义下进行讨论.
    • 设要求的是({n_0choose m_0},n_0leq n).有时候只需要求一个或少个,有时候需要求很多个.

    定义法

    • 求一个:([m_0+1,n_0])的积做分子,([1,n_0-m_0])的积做分母,边乘边除.
    • 分子分母都只有 (n-m) 个. (m) 可与 (n-m) 取较大值.
    • 时间复杂度:每次求(O(m)).
    • 求多个:预处理出 ([0,n]) 内的阶乘及阶乘逆元,阶乘逆元先求 (n!) 的逆元,再一个个乘回来得到其他的.需要计算时直接用定义计算即可.
    • 时间复杂度:预处理(O(n+logp)),每次求(O(1)).

    杨辉三角/递推法

    • 求多个:利用公式 ({nchoose m}={n-1choose m}+{n-1choose m-1}) 递推计算.初始化为({0choose 0}=1,{nchoose 1}=n).
    • 时间复杂度:递推预处理(O(n^2)),每次求(O(1)).
    • 直接使用此法空间为 (O(n^2)) ,可滚动数组优化,但无法查询多个.
    • 不容易中途溢出.

    (Lucas)定理法

    • 求多个:若(pin P),预处理 (p) 以内的阶乘和阶乘逆元,根据(Lucas)定理递归,当 (n_0<p) 时使用定义直接计算.
    • 时间复杂度:预处理(O(p)),每次求(O(logp)).
    • 适用于 (pin P)且较小,(n,m)较大的情况.
    • (p otin P),但 (p) 的每个质因数次数不超过 (1) ,可将 (p) 质因数分解,(p=prod p_i),对每个(p_i)分别用(Lucas)定理计算,最后使用 (crt) 合并答案. 适用于(n,m)较大,且 (p) 在题面中已知,分解后每个质因数次数为(1),且都较小的情况.
    • 条件比较苛刻,但确实某些题(古代猪文)会用到.

    题目

    古代猪文

    • 题意:给出(G,n),求(G^{sum_{k|n} {nchoose k}} mod 999911659,G,nleq 1e9).
    • 题目中所给模数为质数,考虑使用费马小定理,则只需计算出(sum_{k|n} {nchoose k} mod 999911658),然后使用快速幂即可.
    • 考虑(O(sqrt n))暴力枚举约数 (k) ,每次计算 ({nchoose k}) ,但模数, (n,k) 都是 (10^9) 数量级的,算一个也无法接受.
    • 注意到 (999911658) 是个合数,将其质因数分解,我们惊奇发现:(999911658=2*3*4679*35617),最大的质因子只有 (10^4) 级.
    • 考虑使用 (Lucas) 定理,计算出 ({nchoose k}) 对这 (4) 个质因子分别取模得到的结果,最后使用 (crt) 合并答案即可.
    • 时间复杂度为(O(p_{max}+sqrt n * log p_{max}),p_{max}=35617).
    • 在洛谷水双倍经验的时候发现了一个问题,当 (G)(P=999911659) 不互质时,指数对 (varphi (P)) 取模的推论不能使用,需特判.这里 (P) 为质数,不互质的情况答案显然为 (0) .
    • 涉及知识点较多.
    #include"bits/stdc++.h"
    #define int long long
    using namespace std;
    typedef long long LoveLive;
    inline int read()
    {
    	int out=0,fh=1;###
    	char jp=getchar();
    	while ((jp>'9'||jp<'0')&&jp!='-')
    		jp=getchar();
    	if (jp=='-')
    		{
    			fh=-1;
    			jp=getchar();
    		}
    	while (jp>='0'&&jp<='9')
    		{
    			out=out*10+jp-'0';
    			jp=getchar();
    		}
    	return out*fh;
    }
    const int P=999911659;
    int prime[4]={2,3,4679,35617};
    int mul(int a,int b,int mod)
    {
    	return 1LL* a * b % mod;
    }
    int add(int a,int b,int mod)
    {
    	return (a + b) % mod;
    }
    int fpow(int a,int b,int mod)
    {
    	int res=1;
    	while(b)
    		{
    			if(b&1)
    				res=mul(res,a,mod);
    			a=mul(a,a,mod);
    			b>>=1;
    		}
    	return res;
    }
    int inv(int x,int mod)
    {
    	return fpow(x,mod-2,mod);
    }
    int N,G;
    int a[4];
    int fac[4][36000];
    void pre(int x)
    {
    	fac[x][1]=fac[x][0]=1;
    	for(int i=2;i<prime[x];++i)
    		fac[x][i]=mul(fac[x][i-1],i,prime[x]);
    }
    int C(int n,int m,int x)//n choose m
    {
    	if(n<m)
    		return 0;
    	int res=fac[x][n];
    	res=mul(res,inv(fac[x][m],prime[x]),prime[x]);
    	res=mul(res,inv(fac[x][n-m],prime[x]),prime[x]);
    	return res;
    }
    int Lucas(int n,int m,int x)
    {
    	if(!m)
    		return 1;
    	return mul(C(n%prime[x],m%prime[x],x),Lucas(n/prime[x],m/prime[x],x),prime[x]);
    }
    void solve(int k)
    {
    	for(int x=0;x<4;++x)
    		a[x]=add(a[x],Lucas(N,k,x),prime[x]);
    }
    void exgcd(int a,int b,int &x,int &y)
    {
    	if(!b)
    		{
    			x=1;
    			y=0;
    			return;
    		}
    	exgcd(b,a%b,x,y);
    	int t=x;
    	x=y;
    	y=t-(a/b)*y;
    }
    int crt()
    {
    	int res=0;
    	int x,y;
    	for(int i=0;i<4;++i)
    		{
    			int tmp=(P-1)/prime[i];
    			exgcd(prime[i],tmp,x,y);
    			res=(res+y*a[i]%(P-1)*tmp)%(P-1);
    		}
    	return (res+P-1)%(P-1);
    }
    signed main()
    {
    	N=read(),G=read();
    	if(G%P==0)
    		return puts("0")&1;
    	for(int i=0;i<4;++i)
    		pre(i);
    	for(int i=1;i*i<=N;++i)
    		{
    			if(N%i==0)
    				{
    					solve(i);
    					if(i*i!=N)
    						solve(N/i);
    				}
    		}
    	int tp=crt();
    	int ans=fpow(G,tp,P);
    	cout<<ans;
    	return 0;
    }
    

    牡牛和牝牛

    • 题意:求长度为 (n) 且任意两个 (1) 之间至少有 (k)(0)(01) 串数目,(n,kleq 1e5),答案取模.
    • (dp) 做法:令 (f[i]) 表示最后一个 (1) 在位置 (i) 上的方案数.有(forall i leq k+1,f[i]=1).(前面只能都是 (0) ).(forall i>k+1,f[i]=1+sum_{j=1}^{i-k-1} f[j])(前面都是 (0) 或枚举上个 (1) 的位置).
    • 维护 (f) 的前缀和即可做到 (O(n)).加上全为 (0) 的情况,答案为(1+sum f[i]).
    • 排列做法:枚举这个串中共有 (i)(1),那么有 (max(0,(i-1)k))(0) 的位置是确定的.
    • 只用考虑 (i)(1)(n-i-max(0,(i-1)k))(0) 的位置,是一个有重复元素的排列数目.
    • 记每种元素有 (t_i) 个,易知排列数目为(frac{(sum t_i)!}{prod (t_i!)}).
    #include"bits/stdc++.h"
    using namespace std;
    typedef long long LoveLive;
    inline int read()
    {
    	int out=0,fh=1;
    	char jp=getchar();
    	while ((jp>'9'||jp<'0')&&jp!='-')
    		jp=getchar();
    	if (jp=='-')
    		{
    			fh=-1;
    			jp=getchar();
    		}
    	while (jp>='0'&&jp<='9')
    		{
    			out=out*10+jp-'0';
    			jp=getchar();
    		}
    	return out*fh;
    }
    const int P=5000011;
    inline int add(int a,int b)
    {
    	return (a + b) % P;
    }
    inline int mul(int a,int b)
    {
    	return 1LL * a * b % P;
    }
    const int MAXN=1e5+10;
    int k,n;
    int f[MAXN],sum[MAXN];
    void solve_dp()
    {
    	for(int i=1;i<=k+1;++i)
    		f[i]=1,sum[i]=add(sum[i-1],f[i]);
    	for(int i=k+2;i<=n;++i)
    		f[i]=add(sum[i-k-1],1),sum[i]=add(sum[i-1],f[i]);
    	printf("%d
    ",add(sum[n],1));
    }
    int fpow(int a,int b)
    {
    	int res=1;
    	while(b)
    		{
    			if(b&1)
    				res=mul(res,a);
    			a=mul(a,a);
    			b>>=1;
    		}
    	return res;
    }
    int inv(int x)
    {
    	return fpow(x,P-2);
    }
    int fac[MAXN];
    void solve_math()
    {
    	fac[0]=1;
    	for(int i=1;i<=n;++i)
    		fac[i]=mul(fac[i-1],i);
    	int ans=0;
    	for(int i=0;i<=n;++i)
    		{
    			int j=n-i-max(0,(i-1)*k);
    			if(j<0)
    				continue;
    			int up=fac[i+j];
    			int down=mul(fac[i],fac[j]);
    			ans=add(ans,mul(up,inv(down)));
    		}
    	printf("%d
    ",ans);
    }
    int main()
    {
    	n=read(),k=read();
    	solve_math();
    //	solve_dp();
    	return 0;
    }
    

    方程的解

    • 题意:给定 (k,x) ,求不定方程(a_1+a_2+a_3...+a_k=(x^x mod 1000)) 的正整数解组数.(kleq 100,xleq 2^{31}-1).
    • 先使用快速幂求出方程右侧的值,记为 (n) .组数用隔板法解决,有 (n) 个小球,中间有 (n-1) 个空位,向这些空位中插入 (k-1) 个隔板,得到的 (k) 段小球数就是一组解.总组数为(n-1choose k-1).
    • (PS):若要求非负整数解,加入(k) 个小球,最后从每段中减去一个即得.组数为(n+k-1choose k-1).
    • 最后答案要用高精度.懒得写了.代码中的是对 (1e9+7) 取模.
    #include"bits/stdc++.h"
    using namespace std;
    typedef long long LoveLive;
    inline int read()
    {
    	int out=0,fh=1;
    	char jp=getchar();
    	while ((jp>'9'||jp<'0')&&jp!='-')
    		jp=getchar();
    	if (jp=='-')
    		{
    			fh=-1;
    			jp=getchar();
    		}
    	while (jp>='0'&&jp<='9')
    		{
    			out=out*10+jp-'0';
    			jp=getchar();
    		}
    	return out*fh;
    }
    const int P=1000,p=1e9+7;
    inline int mul(int a,int b,int mod)
    {
    	return 1LL * a * b % mod;
    }
    int fpow(int a,int b,int mod)
    {
    	int res=1;
    	while(b)
    		{
    			if(b&1)
    				res=mul(res,a,mod);
    			a=mul(a,a,mod);
    			b>>=1;
    		}
    	return res;
    }
    int inv(int x)
    {
    	return fpow(x,p-2,p);
    }
    int C(int n,int m)//n choose m
    {
    	if(n<m)
    		return 0;
    	int up=1,down=1;
    	if(m>n-m)
    		m=n-m;
    	for(int i=0;i<m;++i)
    		{
    			up=mul(up,n-i,p);
    			down=mul(down,m-i,p);
    		}
    	return mul(up,inv(down),p);
    }
    int main()
    {
    	int k=read(),x=read();
    	int ans=C(fpow(x,x,P)-1,k-1);
    	printf("%d
    ",ans);
    	return 0;
    }
    

    序列统计

    • 题意:给定 (n,l,rleq 1e9) ,问长度不超过 (n) ,元素为整数且在 ([l,r]) 内的不下降序列的数目,答案对 (1e6+3) 取模.(100) 组数据.
    • 容易发现,答案与区间长度 (L=r-l+1) 有关.不妨考虑所有元素在 ([1,L]) 内,考虑将所有长度的不下降子序列数目加起来.
    • 要得到一个长度为 (i) 的上升序列,我们可以把第 (k) 个数权值 (+k) ,变为求上升序列的问题,元素在 ([2,L+i]) 内,共 (L+i-1) 个数.
    • 随意取出 (i) 个数正确排列即可,方案数为(L+i-1choose i).
    • 所有的加起来,据求和公式得到答案(sum_{i=1}^{n} {L+i-1choose i}={n+Lchoose L}-{L+0-1choose 0}).
    • 模数较小,使用 (Lucas) 定理计算即可.
    #include"bits/stdc++.h"
    using namespace std;
    typedef long long LoveLive;
    inline int read()
    {
    	int out=0,fh=1;
    	char jp=getchar();
    	while ((jp>'9'||jp<'0')&&jp!='-')
    		jp=getchar();
    	if (jp=='-')
    		{
    			fh=-1;
    			jp=getchar();
    		}
    	while (jp>='0'&&jp<='9')
    		{
    			out=out*10+jp-'0';
    			jp=getchar();
    		}
    	return out*fh;
    }
    const int P=1e6+3;
    inline int add(int a,int b)
    {
    	return (a + b) % P;
    }
    inline int mul(int a,int b)
    {
    	return 1LL * a * b % P;
    }
    int fpow(int a,int b)
    {
    	int res=1;
    	while(b)
    		{
    			if(b&1)
    				res=mul(res,a);
    			a=mul(a,a);
    			b>>=1;
    		}
    	return res;
    }
    int inv(int x)
    {
    	return fpow(x,P-2);
    }
    int fac[P+10],invfac[P+10];
    void pre()
    {
    	fac[0]=1;
    	for(int i=1;i<=P;++i)
    		fac[i]=mul(fac[i-1],i);
    	invfac[P-1]=inv(fac[P-1]);
    	for(int i=P-2;i>=0;--i)
    		invfac[i]=mul(invfac[i+1],i+1);
    }
    int C(int n,int m)
    {
    	if(n<m)
    		return 0;//加上,利用lucas计算容易出现这种情况 
    	int res=fac[n];
    	res=mul(res,invfac[m]);
    	res=mul(res,invfac[n-m]);
    	return res;
    }
    int Lucas(int n,int m)
    {
    	if(!m)
    		return 1;
    	return mul(C(n%P,m%P),Lucas(n/P,m/P));
    }
    int L,n;
    int main()
    {
    	pre();
    	int T=read();
    	while(T--)
    		{
    			n=read();
    			int l=read(),r=read();
    			L=r-l+1;
    			int ans=(Lucas(n+L,n)-1+P)%P;
    			printf("%d
    ",ans);
    		}
    	return 0;
    }
    

    超能粒子炮・改

    • 题意:给出 (n,kleq 1e18) ,求(sum_{i=0}^k {nchoose i} mod p, p=2333.)(1e5) 组数据.
    • 下面的式子运算都在模意义下进行.
    • (S(n,k)=sum_{i=0}^k {nchoose i}).利用 (Lucas) 定理将其展开,有

    [egin{align*} S(n,k)&=sum_{i=0}^k {nchoose i}\ &=sum_{i=0}^k {n/p choose i/p}*{n\%p choose i\%p}\ &=sum_{i=0}^{(k/p)-1} ({n/pchoose i}*sum_{j=0}^{p-1} {n\%p choose j})+sum_{i=(k/p)*p}^{k} {n/pchoose i/p}*{n\%p choose i\%p}\ &=sum_{i=0}^{(k/p)-1} {n/pchoose i}*sum_{j=0}^{p-1} {n\%p choose j}+sum_{i=0}^{k\%p}{n\%p choose i}*{n/p choose k/p}\ &=S(n/p,(k/p)-1)*S(n\%p,p-1)+S(n\%p,k\%p)*{n/p choose k/p}. end{align*} ]

    • (O(p^2)) 预处理出规模在 (p) 内的 (S) 和组合数.利用上式子及 (Lucas) 定理递归计算,显然单次为(O(log _pn)).总时间复杂度为(O(p^2+Tcdot log_pn))
    #include"bits/stdc++.h"
    #define int long long
    using namespace std;
    typedef long long LoveLive;
    inline int read()
    {
    	int out=0,fh=1;
    	char jp=getchar();
    	while ((jp>'9'||jp<'0')&&jp!='-')
    		jp=getchar();
    	if (jp=='-')
    		{
    			fh=-1;
    			jp=getchar();
    		}
    	while (jp>='0'&&jp<='9')
    		{
    			out=out*10+jp-'0';
    			jp=getchar();
    		}
    	return out*fh;
    }
    const int P=2333;
    inline int add(int a,int b)
    {
    	return (a + b) % P;
    }
    inline int mul(int a,int b)
    {
    	return a * b % P;
    }
    int s[P+10][P+10],c[P+10][P+10];
    void init()
    {
    	for(int i=0;i<=P;++i)
    		s[i][0]=c[i][0]=1;
    	for(int i=1;i<=P;++i)
    		s[0][i]=1;//注意需加上
    	for(int i=1;i<=P;++i)
    		{
    			for(int j=1;j<=i;++j)
    				c[i][j]=add(c[i-1][j],c[i-1][j-1]),s[i][j]=add(s[i][j-1],c[i][j]);
    			for(int j=i+1;j<=P;++j)
    				s[i][j]=s[i][j-1];//注意需加上 
    		}
    }
    int C(int n,int m)// n choose m
    {
    	if(n<m)
    		return 0;
    	if(!m)
    		return 1;
    	return mul(c[n%P][m%P],C(n/P,m/P));
    }
    int S(int n,int k)
    {
    	if(n<P)
    		return s[n][k];
    	int res=mul(S(n/P,k/P-1),S(n%P,P-1));
    	res=add(res,mul(S(n%P,k%P),C(n/P,k/P)));
    	return res;
    }
    signed main()
    {
    	init();
    	int T=read();
    	while(T--)
    		{
    			int n=read(),k=read();
    			printf("%lld
    ",S(n,k));
    		}
    	return 0;
    }
    
  • 相关阅读:
    mysql聚合函数
    轮播图与定时器
    3.23 参数 DOM 鼠标点击跟移入事件
    循环+数组
    for循环
    JS讲解跟遇到的问题
    CSS标签和属性回顾,JS初步了解
    2018.03.14理工大网站问题及解决办法
    2018.3.13 浮动 定位
    2018.3.12CSS样式
  • 原文地址:https://www.cnblogs.com/jklover/p/10322917.html
Copyright © 2020-2023  润新知