• 暑期集训 排列组合


    暑期集训 排列组合概览

    tags:排列组合 取模运算 递推


    写在前面的话:请记住,一定要看清楚题目给的:数据范围有多大,取模数字是不是质数,输出有没有要求在前面加个case i,。还有,所有公式定理均不予证明。我讨厌死它们了。ps:突然过了一道题心情大好简单写了一点证明过程

    一、基础

    (一)基本公式

    (1)加法原理

    做一件事有(n)类方法,第(i)类下面又有(a_i)种方法,则总方法数为(SUM=a_1+a_2+a_3+cdots +a_n)
    即$$SUM = sum_{i=1}^na_i$$

    (2)乘法原理

    做一件事情有(n)个步骤,第(i)个步骤有(a_i)种方法,则总方法数为(SUM=a_1*a_2*a_3cdots *a_n)
    即$$SUM = prod_{i=1}^na_i $$

    (3)排列数

    (n)个元素中选(m)个元素排成一列,问一共有多少种不同序列。
    公式: (A_n^m = frac{n!}{(n-m)!}) 当n=m时, 有(A_n^n=n!)
    在一些地方,(C_n^m) 写作 (n choose m),这个要注意一下。(MathJax代码是 $n choose m$,懂我的意思吧: ) )

    (4)组合数

    (n)个元素中选(m)个元素,问一共有多少种组合。
    公式:(C_n^m = frac{A_n^m}{A_m^m} = frac{n!}{m!(n-m)!}) 公式是对排列公式去重得到的,去重方式可以参考下面的变体公式思路
    变体:(A_n^m = C_n^m * A_m^m) 其含义是:从n个元素中取出m个元素组合,再对这m个元素进行排列

    (5)多重集的排列和组合

    • 多重集的全排列
      设集合(S = { n_1cdot a_1,n_2cdot a_2,cdots ,n_kcdot a_k})是由(n_1)(a_1)(n_2)(a_2)(cdots)(n_k)(a_k)组成的多重集。则其全排列的个数为:
    (推导过程)

    [SUM = frac{(sum^{k}_{i=1}{n_i})!}{n_1!n_2!dots n_k!} ]

    • 多重集的组合
      设集合(S)(k)种元素组成的集合,每种元素个数都是无限的,从中选出(s)个元素构成另一多重集,问产生的不同多重集的个数:隔板法 - 百度百科
    (公式推导利用隔板法)
        先将问题转化为:把$s$个元素放进$k$个盒子,允许盒子为空,问有多少种放法。再利用隔板法,向$s$个元素插入$k-1$个板子。即$C_{s-1}^{k-1}$,但是要求可以有空盒子,所以我们得再加入$k$个元素,插完板子之后再分别从$k$个盒子中拿走这$k$个元素即可,所以公式为$$C_{k+s-1}^{k-1}$$

    [SUM = C_{k+s-1}^{k-1} ]

    (6)奇怪的排列 - 没有证明

    • 不相邻的排列
      从自然数1~n中选取(k)个,使这(k)个元素两两不相邻的组合有:$$SUM=C_{n+k-1}^k$$
    • 错位排列
      编号为1n的元素分别放到编号为1n的盒子中,要求每个盒子只能放一个元素,且该元素编号不能和盒子编号相同,求不同方案数。设答案为d[n],则有:

    [d[n] = (n-1)(d[n-1]+d[n-2]) (n>3)$$或者 $$d[n] = nd[n-1] + (-1)]

    • 圆排列
      从n个元素选出m个元素排成一个圈,不同的排列方法有$$Q_nm=frac{A_nm}{m} = frac{n!}{rcdot (n-r)!}$$

    (7)全概率公式 - 百度百科

    若事件(A_1,A_2,...)构成一个完备事件组且都有正概率,则对于任意一个事件B,有如下公式成立:

    [P(B) = P(BA_1)+P(BA_2)+cdots +P(BA_n) ]

    [= P(B|A_1)P(A_1)+P(B|A_2)P(A_2)+cdots +P(B|A_n)P(A_n) ]

    这就是全概率公式。
        在第四道习题中用到
    > **解题过程中,发现某事件是伴随着一个完备事件组的发生而发生,则马上联想到该事件的发生概率是用全概率公式计算的。**

    特别地,对于任意随机事件A和B,有如下公式成立:$$P(B) = P(B|A)P(A)+P(B|overline A)P(overline A)$$
    其中(A)(overline A)为对立事件

    (二)数据过大的处理方法

    (1)组合数递推公式

    [C_n^m = C_{n-1}^{m-1} + C_{n-1}^m ]

    从n个元素中取出m个元素的组合数等于 从n-1个元素中取出m-1个元素(取第n个元素)的组合数,加上从n-1个元素中取出m个元素(不取第n个元素)的组合数。

    // const long long MOD = 1007; //模数
    const int MAX = 2000+5;     //范围
    int c[MAX][MAX] = {1};
    for(int i=1; i<MAX; i++) {
        c[i][0] = 1;
        for(int j=1; j<=i; ++j)
            c[i][j] = c[i-1][j-1] + c[i-1][j];
            //c[i][j] = (c[i-1][j-1] + c[i-1][j])%MOD;
    }
    

    (2)Lucas(卢卡斯定理) - 百度百科

    卢卡斯定理用于求 (C_n^m mod p) 的值

    (n=sp+q, m=tp+r (q,rleqslant p))
    那么$$C_{sp+q}^{tp+r} equiv C_stC_qr pmod p$$
    或者通俗的写法?$$C_{n}^{m} equiv C_{frac{n}{p}}^{frac{m}{p}}{C_{mmod p}^{nmod p}} pmod p$$
    (equiv)是同余符号,某种意义上就是模p余数相同的意思

    //只有MOD是质数才能用lucas定理。
    const long long MOD = 1e9+7/*质数*/
    //组合数计算函数。     不贴这个的代码了。比较更优的写法参见|运用乘法逆元
    long long C(long long n,long long m);
    /*--------------------------------------Markdown嵌套代码块不能空行*/
    long long lucas(int n,int m) {
        if(!m) return 1;
        return lucas(n/MOD,m/MOD)*C(n%MOD,m%MOD)%MOD;
    }
    

    (3)快速幂 - 百度百科

    幂运算的更快的代码

    const long long MOD = 1e9+7;
    long long qpow(int a,int n) {   //计算a的n次方
        //不写证明了=-=
        if(!n) return 1;
        long long res = 1;
        while(n) {
            //a&b是位运算 这里的表达式是判断最后一位是否为1(判断是否为基)
            if(n&1) res = res*a%MOD;
            //位运算 右移一位,相当于除以2
            n>>=1;
            a = a*a%MOD;
        }
        return res;
    }
    

    (4)运用乘法逆元 - 百度百科

    运用乘法逆元把除法的模运算转换为乘法的模运算
    乘法逆元:(有a,b,p)
    (当a与p互素时,exists x,bxequiv 1pmod p,即b关于1模p的逆元为x)。此时$ frac ab mod p = a*xmod p$

    特别地,$当p为质数时,有 x = b^{p-2}$
        来自费马小定理$b^{p-1}equiv 1pmod p$
    long long qpow(long long a,long long n);    //幂运算    //快速幂运算
    long long C(int n,int m) {
        if(n<0 || m<0 || n<m) return 0;
        if(m > n-m) m = n-m;      //运用性质C(n,m) = C(n,n-m)简化运算
        if(!m) return 1;
        long long res=1,down=1;
        for(int i=1; i<=m; i++) {
            res = res * (n-i+1) % MOD;      //运用了乘法逆元,所以不存在除法了,所以可以放心的取模
            down = down * i % MOD;  
        }
        return res * qpow(down,MOD-2) % MOD;    //运用乘法逆元
    }
    

    二、做题 - 持续更新 一共有十四道题要弄 还有新专题 _

    卑微地补题嘤嘤嘤

    (一) Devu and Flowers - Codeforces 451E

    思路:
    题意:有n个种有不同颜色花朵的花坛,第(i)个花坛有(f_i)朵花。现需要从中选出s朵花。问有多少种组合。
    首先不考虑花朵上限。那就变成了:

    从有n类元素的几何中选出s个元素的多重集的组合数运算。
    得到$$res = C_{s+n-1}^{n-1}$$
    再讨论每个花坛的上限。我们枚举每一种花坛超限的情况,利用容斥定理奇加偶减即可。
    代码中使用的是二进制枚举

    (另一个要点,当某一个花坛超限的情况的总个数怎么计算)
        当第$i$个花坛超限,将其上限$f_i$从$s$中减去,但花坛(盒子)数目不变,再对剩下的花进行运算(代码第42行)
    /**/#include<bits/stdc++.h>
    using namespace std;
    const long long MOD = 1e9+7;
    //--------------------------快速幂
    long long qpow(long long a,long long b) {
        long long ans = 1;
        while(b) {
            if(b&1)ans=(ans*a)%MOD;
            a=a*a%MOD;
            b>>=1;
        }
        return ans;
    }
    //--------------------------运用乘法逆元的组合数计算函数
    long long C(long long n,long long m) {
        if(n<m || n<0 || m<0) return 0;
        if(m>n-m) m = n-m;
        if(!m) return 1;
        long long ans = 1;
        long long down = 1;
        for(long long i=1;i<=m;i++) {
            ans = ans * (n-i+1)%MOD;
            down = down * i % MOD;
        }
        return ans*qpow(down,MOD-2)%MOD;
    }
    //--------------------------Lucas定理
    long long lucas(long long s,long long t) {
        if(!t) return 1;
        return C(s%MOD,t%MOD)*lucas(s/MOD,t/MOD)%MOD;
    }
    int main() {
        long long n;long long s;
        long long ans = 0;
        long long f[20+5];  //n不大于20
        cin>>n>>s;
        for(int i=0;i<n;i++) {
            cin>>f[i];
        }
        //二进制枚举
        for(long long x = 0;x<(1<<n);x++) {
            long long sum = s;
            long long key = 1;
            for(int i=0;i<n;i++) {
                if(x&(1<<i)) {
                    sum -= (f[i]+1);
                    key*=-1;    //计数,奇加偶减
                }
            }
            if(sum<0) continue;
            ans = (ans + key*lucas(sum+n-1,n-1))%MOD;
        }
        //结果加上MOD的整数倍再模MOD,防止出现负数情况。
        cout<<((ans+MOD)%MOD)<<endl;
        return 0;
    }
    

    要注意写代码之前先尽量理解Lucas定理,大数取模,乘法逆元等知识点。

    (二)Critical Mass - UVA 580

    思路:
    题意:
    有两种方块:U和L,砖家认为,当三个U挨在一起时,就会爆炸。现给定方块总数N,输出会爆炸的排列种类总数

    求出不会爆炸的排列数再和总数相减得到答案。
    方块数为 (i) 时不会爆炸的排列有三种后缀:({dp[i][0]*{L},dp[i][1]*{LU},dp[i][2]*{LUU}})
    则运用递推,方块数为(i+1)时不会爆炸的排列数是这样的:$$dp[i+1][0] = dp[i][0]+dp[i][1]+dp[i][2]$$$$dp[i+1][1] = dp[i][0]$$$$dp[i+1][2] = dp[i][1]$$

    /**/#include<bits/stdc++.h>
    using namespace std;
    int dp[100][3];
    int main() {
        dp[1][0] = 1;dp[1][1] = 1;dp[1][2] = 0;
        for(int i=2;i<100;i++) {
            dp[i][0] = dp[i-1][0] + dp[i-1][1] + dp[i-1][2];
            dp[i][1] = dp[i-1][0];
            dp[i][2] = dp[i-1][1];
        }
        int N;
        while(cin>>N && N) {
            cout<<(1<<N)-dp[N][0]-dp[N][1]-dp[N][2]<<endl;
        }
        return 0;
    }
    
    代码能承受的输入就在30以内,这是1~30的输出,题目大概不会超过这个范围,毕竟我AC了=-=
    0
    0
    1
    3
    8
    20
    47
    107
    238
    520
    1121
    2391
    5056
    10616
    22159
    46023
    95182
    196132
    402873
    825259
    1686408
    3438828
    6999071
    14221459
    28853662
    58462800
    118315137
    239186031
    483072832
    974791728

    (三)Race - UVA 12034

    思路:
    题意:有(N)匹马赛跑,每个名次可以并列任意多匹马。问可能出现的所有名次情况。

    我是这么想的,枚举名次数(盒子数),算出(N)匹马排(1)个名次,排(2)个名次,……,排(N)个名次的情况,然后再加起来。
    怎么算,考虑可能不大好用其实应该可能是我不会用 : )多重集的全排列公式。那就直接硬算,(N)匹马排(i)个名次:(i^N)
    但是这样肯定会有重复的,重复计算了很多次(N)匹马排(i-1,i-2,...1)个名次的情况。
    所以我使用递推。令(N)匹马(i)个名次的排列数为(dp[i]),就有:$$dp[i] = i^N - sum_{j=1}{i}C_i{j}dp[j]$$其中(C_i^j)是枚举原本有(i)个盒子但只使用了(j)个盒子的组合。(i^N)就是计算了这么多次,所以需要减去。

    /**/#include<bits/stdc++.h>
    using namespace std;
    const long long MOD = 10056;
    long long qpow(int a,int n) {
        if(!n) return 1;
        long long res = 1;
        while(n) {
            if(n&1) res = res*a%MOD;
            n>>=1;
            a = a*a%MOD;
        }
        return res%MOD;
    }
    long long c[1005][1005] = {0};
    int main() {
        c[0][0] = 1;
        for(int i=1;i<1005;i++) {
            c[i][0] = 1;
            for(int j=1;j<=i;++j) c[i][j]=(c[i][j]+c[i-1][j]+c[i-1][j-1])%MOD;
        }
        int N;cin>>N;int no=N;
        while(N--) {
            int x;cin>>x;
            long long sum = 1;
            long long dp[1005] = {0};
            dp[1] = 1;
            for(int i=2;i<=x;i++) {
                dp[i] = qpow(i,x);
                for(int j=1;j<i;j++) {
                    dp[i] = (dp[i] - c[i][j] * dp[j] + MOD*1000)%MOD;
                }
                dp[i] = (dp[i]+MOD*100000)%MOD;
                sum += (dp[i] + MOD*100000)%MOD;
            }
            cout<<"Case "<<no-N<<": "<<(sum+MOD*100000)%MOD<<endl;
        }
        return 0;
    }
    

    写到这里我突然想到另一种思路。但是我不说,排列组合杀我。over.

    (附一份数据(数据是用我自己的代码随便跑出来的,不一定全面ORZ))
    Input:
    1000
    970
    940
    910
    880
    850
    820
    790
    760
    730
    700
    670
    640
    610
    580
    550
    520
    490
    460
    430
    400
    370
    340
    310
    280
    250
    220
    190
    160
    130

    Output:
    Case 1: 6219
    Case 2: 1467
    Case 3: 1971
    Case 4: 8955
    Case 5: 3963
    Case 6: 2595
    Case 7: 3075
    Case 8: 1203
    Case 9: 1899
    Case 10: 3291
    Case 11: 4371
    Case 12: 5715
    Case 13: 9963
    Case 14: 387
    Case 15: 411
    Case 16: 3603
    Case 17: 5667
    Case 18: 9027
    Case 19: 867
    Case 20: 339
    Case 21: 3459
    Case 22: 6435
    Case 23: 7323
    Case 24: 8691
    Case 25: 3459
    Case 26: 651
    Case 27: 1899
    Case 28: 4275
    Case 29: 4131
    Case 30: 10011

    (四)麻球繁衍 - cogs-1487

    思路:
    题意:有一种神奇的生物叫麻球。每只麻球只能活一天,且一只麻球死前有 (P_i) 概率生下 (i) 只孩子。现给 (k) 只麻球,问 (m) 代(这个是代不是天,注意一下还是有区别的)后麻球死光的概率是多少?
    麻球不需要交配不分公母balabalabala。所以我们只需要计算初始情况一只麻球(i)代后死光的概率(f[i])。那么 (k) 只麻球 (m) 代后死光的概率即 (f[i]^k)。现在问题就只有如何求 (f[i])
    嗯,根据全概率公式递推得:
    (f(i)=P_0+f(i-1) imes P_1+f(i-1)^{2} imes P_2+f(i-1)^{3} imes P_3 +.....+f(i-1)^{n-1} imes P_{n-1})

    好了求完了上代码。
        公式该怎么理解。
        我是这么理解的:其实是这样:首先明白一件事:麻球只能活一天。(这句话语境是把题目当成问多少天后死光,我已经把所有的"天"改成代了)
        所以要问一只麻球第$i$代全部死光的概率,等于问他的后代在$i-1$代后死光的概率。依此便得到了上面的递推公式。到头来我还是没明白全概率公式怎么用的。概率杀我。
        再补充,i-1的意思是已经过了一代,还剩i-1代,第一代的麻球已经死掉了。
        实际上还有一点,不是隔多少天而是隔多少代。这个仔细想想是有一点不同的。隔多少代才对,不需要考虑杂七杂八的包括时差啥的问题。
    /**/#include<bits/stdc++.h>
    using namespace std;
    int main() {
        int N;scanf("%d",&N);int no = N;
        while(N--) {
            double p[1005] = {0};
            double f[1005] = {0};
            int n,k,m;scanf("%d%d%d",&n,&k,&m);
            for(int i=0;i<n;i++) {
                scanf("%lf",&p[i]);
            }
            f[1] = p[0];
            for(int i=2;i<=m;i++) {
                for(int j=0;j<n;j++) {          //枚举前代生孩子的每种情况
                    f[i] += p[j]*pow(f[i-1],j); //前代生多少死多少(i-i代后死)
                }
            }
            printf("Case #%d: %.7lf
    ",no-N,pow(f[m],k));
        }
        // system("pause");
        return 0;
    }
    
    题目数据(题目给了四组数据还是蛮良心的)估计是觉得我们拿着这种数据就算发现错了也不知道正确答案怎么改
    Input:
    4
    3
    1
    1
    0.33
    0.34
    0.33
    3
    1
    2
    0.33
    0.34
    0.33
    3
    1
    2
    0.5
    0.0
    0.5
    4
    2
    2
    0.5
    0.0
    0.0
    0.5

    Output:
    Case #1: 0.3300000
    Case #2: 0.4781370
    Case #3: 0.6250000
    Case #4: 0.3164062

    便利贴模板

    ----
        这里藏的有东西哟

  • 相关阅读:
    传奇版本自动穿背包中的装备脚本
    MaxM2引擎各种人物触发脚本
    异或加密解密代码
    中元节
    网络加密
    MCMD Commander 命令解释及说明
    Ubuntu字符界面与图形界面切换
    传奇服务端各文件用途说明
    传奇数据库说明
    传奇MAP地图说明
  • 原文地址:https://www.cnblogs.com/xglync/p/11249633.html
Copyright © 2020-2023  润新知