• 排列组合


    排列组合

      摘要中的问题的答案是$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

  • 相关阅读:
    Java数据结构与算法(24)
    urllib2使用总结
    Python常见文件操作的函数示例
    Java数据结构与算法(23)
    python代码风格检查工具──pylint
    Python抓取框架:Scrapy的架构
    Java数据结构与算法(22)
    【codeforces 431D】Random Task
    【codeforces 449C】Jzzhu and Apples
    【codeforces 20B】Equation
  • 原文地址:https://www.cnblogs.com/shzr/p/9886199.html
Copyright © 2020-2023  润新知