Algorithm: Permutation & Combination

組合計數

組合數學主要是研究一組離散對象知足必定條件的安排的存在性、構造及計數問題。計數理論是狹義組合數學中最基本的一個研究方向,主要研究的是知足必定條件的排列組合及計數問題。組合計數包含計數原理、計數方法、計數公式。ios

組合計數基本原理

加法原理

\[ 若是一個目標的實現能夠在n種不一樣的狀況下完成,且對於第i種狀況又有m_i種不一樣的方法,\\ 那麼總的方法數N爲:N=m_1+m_2+...+m_n=\sum_{i=1}^n m_i \]c++

其中,每種條件達成都能單獨實現目標,而不依賴其餘條件;任意狀況的任兩種方法都是惟一的。函數

乘法原理

\[ 若是一個目標的實現須要通過n個步驟,對於第k步有m_k種不一樣的方式實現,\\ 那麼總的方法數N爲:N=m_1\times m_2\times...\times m_n=\prod_{i=1}^n m_i \]spa

其中,步驟之間可能存在拓撲關係,但每一個步驟都必不可少;每一步內選擇何種方式不受其餘步驟影響。.net

容斥原理

組合計數最重要的事不重複不遺漏。但通常狀況下總會出現不少的重複計算,又或者在分類討論中遺漏某種狀況。這時咱們就須要容斥原理。容斥原理的基本思想是:先不考慮重複,得出全部的狀況數,而後再排除重複計算的部分,因爲每一步都有理可循,正確運用便可作到不遺漏不重複。code

公式:
\[ 設S是有限集合,A_i \subseteq S,i\in N^+,則\\ |\bigcup_{i=1}^n A_i |=\sum_{k=1}^n (-1)^{k-1}\sum_{1\le i_1<i_2<...<i_k\le n} |A_{i_1}\bigcap A_{i_2}\bigcap ...\bigcap A_{i_k}| \]
容斥公式能夠由以下集合運算的基本公式(德摩根公式)以及數學概括法證實獲得:
\[ \overline{\bigcup_{i=1}^n A_i}=\bigcap_{i=1}^n\overline{A_i}\\ \overline{\bigcap_{i=1}^n A_i}=\bigcup_{i=1}^n\overline{A_i} \]對象

組合計數基本公式

排列數公式

從n個不一樣元素中任取m(m≦n)個元素排成一列(考慮元素前後出現次序),叫作從n個不一樣元素中取出m個元素的一個排列。排列的總數即爲排列數,即叫作從n個不一樣元素中取出m個元素的排列數(number of permutations)。排列數用符號P(Permutation)或者A(Arrangement)表示。
\[ A_n^m(P_n^m)表示從n個元素裏取出m個並排列(要考慮順序)獲得的方案數。\\ A_n^m=n(n-1)...(n-m+1)={n!\over(n-m)!} \]blog

拓展

\[ \begin{aligned}& 1.循環排列:又稱圓排列,指從n個元素裏取出m個元素構成循環排列的排列數N,N={A_n^m\over m} \\& 2.把n個元素分紅k類,第i類元素的個數爲n_i,此時n個元素的排列數N={n!\over \prod_{i=1}^k n_i!}\\\end{aligned} \]排序

組合數公式

從n個不一樣元素中任取m(m≦n)個元素構成一組(不考慮順序),叫作從n個不一樣元素中取出m個元素的一個組合。組合的總數即爲組合數,即叫作從n個不一樣元素中取出m個元素的組合數(number of combinations)。組合數用符號C表示:
\[ C_n^m表示從n個元素裏取出m個構成一組(不考慮順序)獲得的方案數,也能夠用二項式係數(_n^m)表示。\\ C_n^m={A_n^m\over m!}={n!\over (n-m)!m!} \]ci

拓展

\[ k類元素,每類元素個數均爲無限,從這些元素中取出m個的組合數爲C_{k+m-1}^m \]

組合數的性質

\[ \begin{align} & 1.C_n^m=C_n^{n-m}\\ & 2.mC_n^m=nC_{n-1}^{m-1}\\ & 3.組合數遞推式:C_n^m=C_{n-1}^{m-1}+C_{n-1}^m \\ & 4.二項式係數:\sum_{i=0}^n C_n^i=C_n^0+C_n^1+...+C_n^n=2^n \\ &\space\space C_n^0+C_n^2+...=C_n^1+C_n^3+...=2^{n-1} \\ & 5.C_m^m+C_{m+1}^m+...+C_{m+n}^m=C_{m+n+1}^{m+1} \\ & 6.C_n^0+C_{n+1}^1+...+C_{n+m}^m=C_{n+m+1}^m\\ & 7.C_{2n}^2=2C_n^2+n^2\\ \end{align} \]

錯排公式

錯排問題也是組合數學的經典問題之一。錯排即錯誤的排列:對於n個元素構成的一種排列,對其從新排序使得每個元素都不在原來的位置上,這樣的排列就稱爲原排列的一個錯排。n個元素的一個排列的錯排數記爲D(n),則:
\[ \begin{align} &D(n)\\ =&n!\left[{1\over0!}-{1\over1!}+{1\over2!}-...+{\left( -1^n \right) \over n!} \right]\\ =&n!\cdot\sum_{i=0}^n{(-1)^i\over i!}=n!\cdot\sum_{i=2}^n{(-1)^i\over i!}\\ =&\left[{n!\over e}+0.5\right](取整,由{1\over e}的展開式推出) \end{align} \]

組合計數經常使用技巧

捆綁法

當要求某些元素必須相鄰時,先把他們看做一個總體,而後把總體當成一個元素和其餘元素一塊兒考慮。要注意:總體內部也有順序。

插空法

當要求某些元素不能相鄰時,能夠先把其餘元素排好,而後再把要求不相鄰的元素插入到已排好的元素的空隙或兩端。

隔板法

在解決若干相同元素分組問題時,若要求每組至少一個元素,則能夠轉化爲在排成一列的這些元素中插入組數減1個「隔板」,達到分組目的。
\[ 例:n個小球m個盒子,每一個盒子不爲空,則有C_{n-1}^{m-1}種方案;若盒子不要求非空,\\ 則可由引入m個球,隔板法解決後從每一個盒子裏各取走一個球,方案數爲C_{n+m-1}^{m-1} \]

例題

  1. A、B、C、D、E五人排成一排,其中A、B不站一塊兒,一共有多少種站法?
    \[ 插空法:由於A、B不能站一塊兒,因此咱們能夠考慮先給C、D、E排序,方案數爲A_3^3;\\3人排好序後留出4個空位,A、B插空,方案數爲A_4^2,根據乘法原理,總方案數爲A_3^3\cdot A_4^2。 \]

  2. A、B、C、D、E五人排成一排,其中A、B必須站一塊兒,一共有多少種站法?
    \[ 捆綁法:既然A、B必須站在一塊兒,那麼咱們索性把A、B「捆綁」,當作一個總體看待,\\ 而後同等看待這個總體和C、D、E。對於A、B的內部來講,方案數爲A_2^2;\\ 對於外部,方案數爲A_4^4。總方案數爲:A_2^2\cdot A_4^4。 \]

  3. 一張節目表上有3個節目,保持其相對順序不變,再加入2個新節目,有多少種方案?
    \[ \begin{align} &捆綁法+插空法:\\ &分兩種狀況:\\ &1.兩個新節目相鄰,那麼3個節目有4個空,再考慮內部順序,總方案數爲C_4^1\cdot A_2^2;\\ &2.兩個節目不相鄰,2個節目插4個空,方案數爲A_4^2;\\ &那麼總方案數即C_4^1\cdot A_2^2+A_4^2。 \end{align} \]

  4. 將8個徹底相同的球放到3個不一樣的盒子中,要求每一個盒子至少放一個球,有多少種方法?
    \[ 隔板法:把8個球排成一列,球之間的空位數爲7,假設咱們如今有兩個隔板,\\ 在7個空位插入兩個隔板,就能把球分紅有序的三組,分別對應三個盒子,方案數爲C_7^2。 \]

  5. (hdu 6397)題意:給三個數n、m、k, 在0~n-1中選出m個數排成一排使得他們的和等於k,這m個數能夠相同,只要排列不一樣便可。求一共有多少種排列方式是知足題意的。

\[ 相似盒子可空的n球m盒問題。不考慮數字的大小限制,總方案數爲C_{k+m-1}^{m-1}。\\ 設數字爲x,有x_1+x_2+...+x_m=k; \\ 令y_i=x_i+1,用隔板法分析:y_1+y_2+...+y_m=k+m。\\ 可是題目限制數的大小隻能在n之內,那麼能夠這樣考慮:\\ 假設在咱們的枚舉中有i個數超出限制,即在m個數裏,有x個大於等於n,那麼這時的方案數爲:\\ C_m^i\cdot C_{k+m-1-i\times n}^{m-1},對應的分析爲:\\ x_1'+x_2'+...+x_m'=k-i\times n。\\ 最後結合容斥奇減偶加就能得出答案。 \]

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;

const int maxn = 2e5 + 100;
const int mod = 998244353;

ll inv[maxn], F[maxn], Finv[maxn];

void init() {
    inv[1] = 1;
    for(int i = 2; i < maxn; i++)
        inv[i] = (mod - mod / i) * inv[mod % i] % mod;
    F[0] = Finv[0] = 1;
    for(int i = 1; i < maxn; i++) {
        F[i] = F[i - 1] * i % mod;
        Finv[i] = Finv[i - 1] * inv[i] % mod;
    }
}

ll C(ll n, ll m) {
    if(n < 0 || m < 0 || m > n) return 0;
    return F[n] * Finv[m] % mod * Finv[n - m] % mod;
}

int main() {
    int t, n, m, k;
    init();
    cin >> t;
    while(t--) {
        cin >> n >> m >> k;
        ll ans = 0;
        for(int i = 0; i * n <= k; i++) {
            int r = C(m, i) * C(k - i * n + m - 1, m - 1) % mod;
            if(i & 1) ans = (ans - r + mod) % mod;
            else ans = (ans + r) % mod;
        }
        cout << ans << endl;
    }
    return 0;
}
  1. (uva 10943)求把k個不超過n的非負整數加起來使得和爲n的方案數。盒子可空隔板法。由於數據範圍比較小,因此直接dp預處理出全部結果,O(1)查詢便可。

    #include<bits/stdc++.h>
    using namespace std;
    const int mod = 1e6;
    int n, k, dp[111][111]; // dp[i][j]表示用j個數使得和爲i的方案數
    int main() {
     for (int i = 1; i <= 100; i++) {
         dp[i][1] = 1;
         dp[1][i] = i;
     }
     for (int i = 2; i <= 100; i++)
         for (int j = 2; j <= 100; j++)
             dp[i][j] = (dp[i - 1][j] + dp[i][j - 1]) % mod;
     while (cin >> n >> k && (n|k)) { 
            cout << dp[n][k] << endl;
     }
        return 0;
    }
  2. (CF GYM 100548)n盆花排成一列用m種顏色塗,要求相鄰顏色不一樣,且使用顏色數恰爲k。求方案數%1e9+7。

    分析:乘法原理+容斥。
    \[ \begin{align}&首先從m種顏色裏選出k種備選顏色,方案數爲C_m^k;\\&乘法原理:對於第一盆花,有k種選擇,對於以後的每一盆花,\\&爲了保證不重複,只有(k-1)種選擇,\\&方案數爲k\times (k-1)^{n-1}。\\&但這時包含了許多使用顏色額數不足k的方案,須要容斥原理排除。\\&考慮從k種顏色裏取i種塗色,方案數爲C_k^i\cdot p(p-1)^{n-1},結合容斥奇減偶加,能夠得出:\\&總方案數N=C_m^k\cdot k(k-1)^{n-1}+\sum_{i=2}^{k-1}(-1)^iC_k^i\cdot i(i-1)^{n-1}\\&顯然須要用到逆元,組合數,快速冪,下面是標程。\end{align} \]

    #include <iostream>
    using namespace std;
    typedef long long ll;
    
    const int mod = 1e9 + 7;
    const int maxn = 1000010;
    
    ll ans, inv[maxn], F[maxn], Finv[maxn];
    
    ll qpow(ll a, ll b) {
        ll ans = 1;
        while(b) {
            if(b & 1) ans = (ans * a) % mod;
            b >>= 1; a = (a * a) % mod;
        }
        return ans;
    }
    
    void init() {
        inv[1] = 1;
        for(int i = 2; i < maxn; i++)
            inv[i] = (mod - mod / i) * inv[mod % i] % mod;
        F[0] = Finv[0] = 1;
        for(int i = 1; i < maxn; i++) {
            F[i] = F[i - 1] * i % mod;
            Finv[i] = Finv[i - 1] * inv[i] % mod;
        }
    }
    
    ll C(ll n, ll m) {
        if(n < 0 || m < 0 || m > n) return 0;
        return F[n] * Finv[m] % mod * Finv[n - m] % mod;
    }
    
    int main() {
     init();
     int t, kase = 0;
        cin >> t;
        while (t--) {
            cin >> n >> m >> k;
            ans = C(m, k) * k % mod * qpow(k - 1, n - 1) % mod;
            ll sign = 1;
            for(ll i = 2; i < k; i++) {
             ans = (ans + C(k, i) * i % mod * qpow(i - 1, n - 1) % mod * sign + mod) % mod;
             sign = -sign;
            }
            cout << "Case #" << ++kase << ": " << ans << endl;
        }
        return 0;
    }
相關文章
相關標籤/搜索