組合數學

Yaosicheng124發表於2024-04-11

逆元

\(i \cdot x = 1\),則 \(i^{-1}=x\)

遞推求乘法逆元

\[令模數為 p,正整數 i,a=\lfloor\frac{p}{i}\rfloor,b=p\operatorname{mod} i,且b\ne0。\\ a\cdot i+b=p\\ \Downarrow\\ a\cdot i+b\equiv0 (\operatorname{mod}p)\\ \frac{i}{b}+\frac{1}{a}\equiv0(\operatorname{mod}p)\\ \frac{i}{b}\equiv-\frac{1}{a}(\operatorname{mod}p)\\ \frac{b}{i}\equiv-a(\operatorname{mod}p)\\ i^{-1}\equiv(-a)\cdot b^{-1}(\operatorname{mod}p)\\ \]

\(inv_i=i^{-1}\),則我們可以由上面的結論得到 \(inv_i = \lfloor-\frac{p}{i}\rfloor \cdot inv_{p \operatorname{mod} i} \operatorname{mod} p\)

程式碼

void C() {
  inv[1] = 1;
  for(int i = 2; i < MAXV; ++i) {
    inv[i] = 1ll * (p - p / i) * inv[p % i] % p;
  }
}

C();

平方求組合數

可以發現,\(C_n^m\) 有兩種情況:

  • 選最後一個:\(C_{n-1}^{m-1}\)
  • 不選最後一個:\(C_{n-1}^m\)

所以 \(C_n^m=C_{n-1}^{m-1}+C_{n-1}^m\)。(實際上這就是楊輝三角)

程式碼

void C() {
  for(int i = 0; i < MAXV; ++i) {
    c[i][0] = 1;
    for(int j = 1; j <= i; ++j) {
      c[i][j] = (c[i - 1][j - 1] + c[i - 1][j]) % MOD;
    }
  }
}

預處理求組合數

眾所周知,\(C_n^m=\frac{n!}{m!(n-m)!}\)。所以我們只需預處理出階乘的逆元。

\(f_i=i!,g_i=i!^{-1}\),則:

\[f_i=f_{i-1}\cdot i \operatorname{mod} p\\ g_i=g_{i-1}\cdot inv_i \operatorname{mod} p\\ C_n^m=f_n\cdot g_m\cdot g_{n-m} \operatorname{mod} p \]

void C() {
  inv[1] = f[0] = Inv[0] = 1;
  for(int i = 1; i < MAXV; ++i) {
    f[i] = 1ll * f[i - 1] * i % p;
    inv[i] = (i > 1 ? 1ll * (p - p / i) * inv[p % i] % p : 1);
    Inv[i] = 1ll * Inv[i - 1] * inv[i] % p;
  }
}

C();

題目

CSES P1715

題目描述

給定一個字串 \(S\),求將其重新排列後能得到多少種不同的字串。

思路

首先在不考慮字元相同的情況下答案明顯就是 \(|S|!\),而每種相同字元會重複計算 \(cnt_x!\) 次,其中 \(cnt_x\) 表示字元 \(x\) 的出現次數,所以答案為:\(|S|! \cdot \prod \limits_{i=0}^{25} cnt_i!^{-1}\)

時空複雜度均為 \(O(N)\)

細節

無。

程式碼

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

const int MAXV = 1000001, MOD = 1000000007;

int f[MAXV], nv[MAXV], inv[MAXV], cnt[26], ans;
string s;

void C() {
  f[0] = f[1] = nv[1] = inv[0] = inv[1] = 1;
  for(int i = 2; i < MAXV; ++i) {
    f[i] = 1ll * f[i - 1] * i % MOD;
    nv[i] = 1ll * (MOD - MOD / i) * nv[MOD % i] % MOD;
  }
  for(int i = 2; i < MAXV; ++i) {
    inv[i] = 1ll * inv[i - 1] * nv[i] % MOD;
  }
}

int main() {
  ios::sync_with_stdio(false), cin.tie(0), cout.tie(0);
  C();
  cin >> s;
  for(int i = 0; i < int(s.size()); ++i) {
    cnt[s[i] - 'a']++;
  }
  ans = f[s.size()];
  for(int i = 0; i < 26; ++i) {
    ans = 1ll * ans * inv[cnt[i]] % MOD;
  }
  cout << ans;
  return 0;
}

CSES P1716

題目描述

\(N\) 個小朋友和 \(M\) 個蘋果,求有多少中分配方案。

思路

可以看做是 \(M\) 個蘋果之間有 \(M-1\) 個空位,有 \(N-1\) 個板子要插在空位中,相鄰兩塊板子之間的蘋果就是屬於同一個小朋友的。可是小朋友可以不拿蘋果,所以我們再加入 \(N\) 個虛擬蘋果,使得每個小朋友都能拿到蘋果,所以答案為 \(C_{N+M-1}^{N-1}\)

時空複雜度均為 \(O(N)\)

細節

程式碼

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

const int MAXV = 2000001, MOD = 1000000007;

int n, m, f[MAXV], nv[MAXV], inv[MAXV], cnt[26], ans;

void C() {
  f[0] = f[1] = nv[1] = inv[0] = inv[1] = 1;
  for(int i = 2; i < MAXV; ++i) {
    f[i] = 1ll * f[i - 1] * i % MOD;
    nv[i] = 1ll * (MOD - MOD / i) * nv[MOD % i] % MOD;
  }
  for(int i = 2; i < MAXV; ++i) {
    inv[i] = 1ll * inv[i - 1] * nv[i] % MOD;
  }
}

int main() {
  ios::sync_with_stdio(false), cin.tie(0), cout.tie(0);
  C();
  cin >> n >> m;
  cout << ((1ll * f[m + n - 1] * inv[n - 1]) % MOD * inv[m]) % MOD;
  return 0;
}

GYM 104386 C

題目描述

有一個陣列 \(A=\{1,1,\dots\}\),每次對 \(A\) 進行字首和,求 \(K\) 次操作後 \(A_N\) 的值。

思路

令第 \(i\) 次第 \(j\) 項為 \(f_{i,j}\),可以得到 \(f_{i,j}=f_{i,j-1}+f_{i-1,j}\),現在我們轉換一下座標系,變為斜方向的,即 \(f_{i,j}=f_{i-1,j-1}+f_{i-1,j}\),很容易發現這就是組合數的遞推式。因為轉換了座標系,所以答案為 \(C_{N+K-1}^{N-1}\)

時空複雜度均為 \(O(N+K)\)

細節

注意第一維是 \(1\) 下標,第二維是 \(0\) 下標。

程式碼

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

const int MAXV = 2000001, MOD = 1000000007;

int t, n, k, f[MAXV], nv[MAXV], inv[MAXV];

void C() {
  f[0] = f[1] = nv[1] = inv[0] = inv[1] = 1;
  for(int i = 2; i < MAXV; ++i) {
    f[i] = 1ll * f[i - 1] * i % MOD;
    nv[i] = 1ll * (MOD - MOD / i) * nv[MOD % i] % MOD;
  }
  for(int i = 2; i < MAXV; ++i) {
    inv[i] = 1ll * inv[i - 1] * nv[i] % MOD;
  }
}

void Solve() {
  cin >> n >> k;
  int x = n + k - 1, y = n - 1;
  cout << ((1ll * f[x] * inv[y]) % MOD * inv[x - y]) % MOD << "\n";
}

int main() {
  ios::sync_with_stdio(false), cin.tie(0), cout.tie(0);
  C();
  cin >> t;
  while(t--) {
    Solve();
  }
  return 0;
}

CSES P1717

題目描述

\(N\) 個小朋友送禮物,不能給自己送禮物,求每個小朋友都收到禮物的方案數。

思路

由於每個小朋友都要收到禮物,所以這就是求錯排數,使用 DP。

\(dp_x\) 表示長度為 \(x\) 的錯排數,\(A_1=i(2 \le i \le x)\)。則第 \(i\) 位上有兩種情況:

  • \(A_i=1\),則方案數為 \(dp_{x-2}\),因為 \(A_1\)\(A_i\) 已經不會對答案造成影響。
  • \(A_i\ne 1\),則方案數為 \(dp_{x-1}\),因為 \(A_1\) 已經沒有作用,可以把 \(A_i\) 看做 \(A_1\)

所以 \(dp_x = (x-1)(dp_{x-1}+dp_{x-2})\)

細節

\(dp_0=1,dp_1=0\)

程式碼

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

const int MAXV = 2000001, MOD = 1000000007;

int n, f[MAXV], nv[MAXV], inv[MAXV], dp[MAXV];

void F() {
  f[0] = f[1] = nv[1] = inv[0] = inv[1] = 1;
  for(int i = 2; i < MAXV; ++i) {
    f[i] = 1ll * f[i - 1] * i % MOD;
    nv[i] = 1ll * (MOD - MOD / i) * nv[MOD % i] % MOD;
  }
  for(int i = 2; i < MAXV; ++i) {
    inv[i] = 1ll * inv[i - 1] * nv[i] % MOD;
  }
}

int C(int x, int y) {
  return ((1ll * f[x] * inv[y]) % MOD * inv[x - y]) % MOD;
}

int A(int x, int y) {
  return (1ll * f[x] * inv[y]) % MOD;
}

int Pow(int a, int b) {
  int res = 1;
  while(b) {
    if(b & 1) {
      res = (1ll * res * a) % MOD;
    }
    a = (1ll * a * a) % MOD;
    b >>= 1;
  }
  return res;
}

int main() {
  ios::sync_with_stdio(false), cin.tie(0), cout.tie(0);
  F();
  cin >> n;
  dp[0] = 1, dp[1] = 0;
  for(int i = 2; i <= n; ++i) {
    dp[i] = 1ll * (i - 1) * (dp[i - 1] + dp[i - 2]) % MOD;
  }
  cout << dp[n];
  return 0;
}

Luogu P4071

題目描述

求有多少種 \(1\)\(N\) 的排列 \(A\),使得恰好有 \(M\)\(i\) 滿足 \(A_i=i\)

思路

由於有 \(M\) 個位置 \(A_i=i\),則剩下的肯定不滿足,即錯排,所以方案數為 \(dp_{N-M} \cdot C_{N}^{M}\)

時空複雜度均為 \(O(N)\)

細節

無。

程式碼

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

const int MAXV = 2000001, MOD = 1000000007;

int t, n, m, f[MAXV], nv[MAXV], inv[MAXV], dp[MAXV];

void F() {
  f[0] = f[1] = nv[1] = inv[0] = inv[1] = 1;
  for(int i = 2; i < MAXV; ++i) {
    f[i] = 1ll * f[i - 1] * i % MOD;
    nv[i] = 1ll * (MOD - MOD / i) * nv[MOD % i] % MOD;
  }
  for(int i = 2; i < MAXV; ++i) {
    inv[i] = 1ll * inv[i - 1] * nv[i] % MOD;
  }
  dp[0] = 1, dp[1] = 0;
  for(int i = 2; i <= MAXV; ++i) {
    dp[i] = 1ll * (i - 1) * (dp[i - 1] + dp[i - 2]) % MOD;
  }
}

int C(int x, int y) {
  return ((1ll * f[x] * inv[y]) % MOD * inv[x - y]) % MOD;
}

int A(int x, int y) {
  return (1ll * f[x] * inv[y]) % MOD;
}

int Pow(int a, int b) {
  int res = 1;
  while(b) {
    if(b & 1) {
      res = (1ll * res * a) % MOD;
    }
    a = (1ll * a * a) % MOD;
    b >>= 1;
  }
  return res;
}

int main() {
  ios::sync_with_stdio(false), cin.tie(0), cout.tie(0);
  F();
  cin >> t;
  while(t--) {
    cin >> n >> m;
    cout << 1ll * dp[n - m] * C(n, n - m) % MOD << "\n";
  }
  return 0;
}

相關文章