排列组合
排列组合
摘要中的问题的答案是$int$;
组合:$C_n^k=\frac{n!}{k!(n-k)!}$
排列:$A_n^k=\frac{n!}{(n-k)!}$
求组合数的方法看起来好像没有什么价值,但是事实上还是挺重要的。
1.暴力求就不用介绍了吧,一般只适用于数据范围非常小的题目;
2.取模或不取模都可以用的方法:$C_n^m=C_{n-1}^{m-1}+C_{n-1}^m$.这个做法的思路是这样的:考虑新加进来的第$n$个数的影响,如果选它,那么前$n-1$个中只好少选一个,如果不选它,那么前面就得把$m$个选够,其实是一种动态规划.因为加法可以直接取模,所以对于取模的题目也是非常好用的.注意如果某道题要求采用高精度,就要谨慎的考虑要不要用这种方法了,因为加上高精度之后复杂度会变得非常高,空间开销也非常大.
3.如果要求取模:预处理阶乘和逆元,$O(N)$预处理,$O(1)$出解,效率非常棒.如果$n$的范围比$p$大很多,可以考虑使用$Lucas$定理;
4.最坑人的一种:不要求取模,数据范围还挺大的,此时最好,也只能用高精度了.用高精度也是讲求技巧的,如果直接套用公式就需要做很多高精度乘除,而高精度除法的计算效率非常低下.但是没有关系,虽然组合数可能很大,但是它进行唯一分解之后一定是可以用普通的数组存下来的,为什么?因为组合数的所有因子都是之前在$n,k$中出现过的,所以不会太大.而且因为组合数最终是一个整数,所以所有分母上出现的因子都可以在因子中找到并提前除去,虽然除因子的复杂度不是很低,但是总比用高精度除快多了.来一份这种做法的板子吧:
1 void add (int x,int v) 2 { 3 for (R i=2;i*i<=x;++i) 4 while(x%i==0) y[i]+=v,x/=i; 5 if(x!=1) y[x]+=v; 6 } 7 8 void mul (int x) 9 { 10 c[0]+=3; 11 for (R i=1;i<=c[0];++i) 12 c[i]*=x; 13 for (R i=1;i<=c[0];++i) 14 c[i+1]+=c[i]/10,c[i]%=10; 15 while(c[ c[0] ]==0&&c[0]) c[0]--; 16 } 17 18 void print() 19 { 20 for (R i=c[0];i>=1;--i) 21 printf("%d",c[i]); 22 } 23 24 for (R i=2;i<=g;++i) add(i,1); 25 for (R i=2;i<=k;++i) add(i,-1); 26 for (R i=2;i<=g-k;++i) add(i,-1); 27 28 c[0]=c[1]=1; 29 for (R i=2;i<=1000;++i) 30 for (R j=1;j<=y[i];++j) 31 mul(i); 32 print();
先看一个比较妙的题目:
圆连线三角形:也许$codevs$上有,不过我也找不到了.
在一个圆周上点$n$个点,两两连线,保证不会有三点交于一线,这样的线可以划分出很多小的三角形,求三点都不在圆周上的三角形个数.
乍一看感觉很难,其实利用了反向思维。首先拿出一个满足条件的三角形,可以发现它的三边分别延长后一定来自六个不同的点,事实上这不仅是必要条件也是充分条件。即:圆上任意$6$点都可以连出一个这样的三角形来,答案即为$C_n^6$.
序列统计:https://www.lydsy.com/JudgeOnline/problem.php?id=4403
题意概述:统计长度在$1$到$n$之间,元素大小都在$L$到$R$之间的单调不降序列的数量,并对$10^6+3$取模(是质数).$n,l,r<=10^9$
首先可以发现这个$l,r$就是吓唬人的,我们实际关心的只是这一段中有多少个数字,那么设$m=(r-l+1)$.
一开始说的那个做法好像假掉了,根本就不对,但是...推出来的式子竟然是对的???这里还是把假做法说一下吧:事实上任意选出一些数进行重排后总能排出一个单调不降序列,而数两两不同,所以这一点根本不限制我们的取数,只要不取重复元素即可。这里只有一点是真正要想一下的,就是序列长度不一定要严格等于$n$,而是可以小于它,那么设置$n$个虚点表示这个位置不选数,答案就是$C_{n+m}^n-1$,因为不能一个都不选.智慧的$asuldb$告诉我...因为我算重了一些(两个点选到同一个虚点还是同一种方案),算少了一些(选重复元素)所以答案正好是对的,负负得正?
下面是两个真实的做法:
$1$:考虑一个$dp$做法,用$dp[i][j]$表示当前写到第$i$位,最后一个小于等于$j$的方案数,发现每个状态只能由下面的和左边的转移过来,就是一个方格走步方案数的问题(很多人学的$dp$第一道题),然而这个可以不用$dp$,考虑要往右走$n$步,往上走$m$步,这$m$步要穿插在$n$步里边走,但是还可以一次穿插好多个,所以就是插板法稍微扩展一下,答案是$C_{n+m}^{n}-1$;
$2$:把$m$个数列出来,进行插板,每个位置选择的就是它的板之前的那个数,因为可以重复选,就加入$n$个虚的点,还有就是可以不选,那么插到$0$之前就视为不选,答案都是一样的.
1 # include <cstdio> 2 # include <iostream> 3 # define p 1000003 4 # define R register int 5 6 using namespace std; 7 8 const int maxp=1000005; 9 int T; 10 int n,l,r,m; 11 int f[maxp],inv[maxp]; 12 13 int c(int n,int m) 14 { 15 if(n<m) return 0; 16 return (1LL*f[n]*inv[m]%p)*inv[n-m]%p; 17 } 18 19 int Lucas(int n,int m) 20 { 21 if(m==0) 22 return 1; 23 else 24 return (long long)Lucas(n/p,m/p)*c(n%p,m%p)%p; 25 } 26 27 int qui (int x,int c) 28 { 29 int s=1; 30 while (c) 31 { 32 if(c&1) s=1LL*s*x%p; 33 x=1LL*x*x%p; 34 c=c>>1; 35 } 36 return s%p; 37 } 38 39 void init() 40 { 41 f[0]=inv[0]=1; 42 for (R i=1;i<=p;++i) 43 f[i]=1LL*f[i-1]*i%p; 44 inv[p-1]=qui(p-1,p-2); 45 for (R i=p-1;i>=1;--i) 46 inv[i-1]=1LL*inv[i]*i%p; 47 } 48 49 int main() 50 { 51 scanf("%d",&T); 52 init(); 53 while (T--) 54 { 55 scanf("%d%d%d",&n,&l,&r); 56 m=r-l+1; 57 printf("%d\n",(Lucas(n+m,n)-1+p)%p); 58 } 59 return 0; 60 }
组合数学四合一:NULL
最近学校流感横行,于是周日咕掉了一天课,在家里休息。没想到信息组竟然考了两轮试,还要看这个成绩分竞赛班?那我岂不是退役预定...这是那天上午的第三题。
题意概述:有一个平面直角坐标系,起点是(0,0),要求上下左右地走,走n步后回到起点,求方案数。这道题由四部分组成,分别计分。形式化地说,对于每一部分,能到达的点集和数据范围如下。
1.$\{ (x,y)|x,y\in Z \}$ $n<=10^5$
2.$\{ (x,y)|x\in N^*,y=0 \}$ $n<=10^5$
3.$\{(x,y)|xy=0 \}$ $n<=10^3$
4.$\{(x,y)|x,y\in N^* \}$ $n<=10^5$
乍一看有点困难?一步一步分开来做就好了。
Case 1:对于这道题的每一问,都有一个显然的结论,就是上下步数相等,左右步数相等,水平与垂直互不干扰。由于数据范围不大,可以考虑枚举向上走的步数,可以直接推出另外三个步数。将每种操作视为一种颜色的小球,首先第一种先顺着放好,再用插板法依次将另外三种插入就可以了。还有一种做法,首先将左插入右,再将上插入下,最后两个整体再插入一次,显然这两种做法是等价的,但是后者对于下面的题可能更有启发性一点。
Case 2:任意时刻,左不能超过右,直接上卡特兰;
Case 3:这一问稍微难做一点,因为它的两个方向开始出现干扰了,所以考虑dp。最显然的思路是dp(i,j,k)表示目前走了多少步,在哪里,但是这样太慢了。一种比较简单的想法是dp[i]表示目前走了多少步且在原点的方案数,转移时枚举走多少步,强行要求走这么多步后还得回到原点。但是有时候还是不对,比如第一次走了两步,是上下,第二次走了两步,也是上下,这样与一次走四步上下上下是等价的,但是会被记成两种。因为每次走都是在某一根轴上走,所以可以想到,如果两次都在同一根轴上走就会算重,所以再强行加一个限制,如果上一次是走的x轴,这一次就必须走y轴,反之亦然。
Case 4:这一问其实是第一问和第二问的综合。对于横向和纵向,分别需要满足第二问的要求,所以依旧枚举纵向步数,用卡特兰数算出横纵分别的答案后插板法合并答案。
1 # include <cstdio> 2 # include <iostream> 3 # include <cstring> 4 # include <iostream> 5 # define R register int 6 # define mod 1000000007 7 # define ll long long 8 9 using namespace std; 10 11 const int maxn=200010; 12 int n,typ,x,y; 13 ll finv[maxn],f[maxn],dp[1005][2],inv[maxn]; 14 ll ans=0; 15 16 ll C (int x,int y) 17 { 18 if(y>x||x==0||y==0) return 1; 19 return 1LL*f[x]*finv[y]%mod*finv[x-y]%mod; 20 } 21 22 ll qui (ll a,ll b) 23 { 24 ll s=1; 25 while(b) 26 { 27 if(b&1) s=s*a%mod; 28 a=a*a%mod; 29 b>>=1; 30 } 31 return s; 32 } 33 34 void init (int n) 35 { 36 f[0]=finv[0]=inv[0]=1; 37 for (R i=1;i<=n;++i) f[i]=f[i-1]*i%mod; 38 finv[n]=qui(f[n],mod-2); 39 for (R i=n-1;i>=1;--i) finv[i]=finv[i+1]*(i+1)%mod; 40 for (R i=1;i<=n;++i) inv[i]=qui(i,mod-2); 41 } 42 43 int main() 44 { 45 scanf("%d%d",&n,&typ); 46 init(n); 47 n/=2; 48 if(typ==0) 49 { 50 for (R x=0;x<=n;++x) 51 { 52 y=n-x; 53 ans=(ans+C(2*x,x)*C(2*x+y,y)%mod*C(2*x+2*y,y)%mod)%mod; 54 } 55 } 56 else if(typ==1) 57 { 58 ans=((C(n*2,n)-C(n*2,n-1))%mod+mod)%mod; 59 } 60 else if(typ==2) 61 { 62 dp[0][0]=dp[0][1]=1; 63 for (R i=0;i<=n;++i) 64 for (R k=0;k<=1;++k) 65 { 66 if(!dp[i][k]) continue; 67 for (R j=1;i+j<=n;++j) 68 dp[i+j][k^1]=(dp[i+j][k^1]+dp[i][k]%mod*C(2*j,j)%mod)%mod; 69 } 70 ans=(dp[n][0]+dp[n][1])%mod; 71 } 72 else if(typ==3) 73 { 74 for (R x=0;x<=n;++x) 75 { 76 y=n-x; 77 ans=(ans+1LL*C(2*x,x)*inv[x+1]%mod*C(2*y,y)%mod*inv[y+1]%mod*C(2*x+2*y,2*y)%mod)%mod; 78 } 79 } 80 printf("%lld",ans); 81 return 0; 82 }
---shzr