組合數學主要是研究一組離散對象知足必定條件的安排的存在性、構造及計數問題。計數理論是狹義組合數學中最基本的一個研究方向,主要研究的是知足必定條件的排列組合及計數問題。組合計數包含計數原理、計數方法、計數公式。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} \]
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。 \]
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個節目,保持其相對順序不變,再加入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} \]
將8個徹底相同的球放到3個不一樣的盒子中,要求每一個盒子至少放一個球,有多少種方法?
\[ 隔板法:把8個球排成一列,球之間的空位數爲7,假設咱們如今有兩個隔板,\\ 在7個空位插入兩個隔板,就能把球分紅有序的三組,分別對應三個盒子,方案數爲C_7^2。 \]
(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; }
(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; }
(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; }