排列组合

排列组合

  摘要中的问题的答案是$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
",(Lucas(n+m,n)-1+p)%p);
58     }
59     return 0;
60 }
序列统计

 

  组合数学四合一:NULL

  最近学校流感横行,于是周日咕掉了一天课,在家里休息。没想到信息组竟然考了两轮试,还要看这个成绩分竞赛班?那我岂不是退役预定...这是那天上午的第三题。

  题意概述:有一个平面直角坐标系,起点是(0,0),要求上下左右地走,走n步后回到起点,求方案数。这道题由四部分组成,分别计分。形式化地说,对于每一部分,能到达的点集和数据范围如下。

  1.${ (x,y)|x,yin Z }$ $n<=10^5$

  2.${ (x,y)|xin N^*,y=0 }$ $n<=10^5$

  3.${(x,y)|xy=0 }$ $n<=10^3$

  4.${(x,y)|x,yin 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

原文地址:https://www.cnblogs.com/shzr/p/9886199.html