「模板」圓方樹
定義
集合
兩個集合的交集:集合 \(A\) 與 \(B\) 的交集可以表示為:
\[A \cap B=\{x:x \in A \land x \in B\} \]兩個集合的並集:集合 \(A\) 與 \(B\) 的並集可以表示為:
\[A \cup B = \{x:x \in A \lor x \in B \} \]兩個集合的差集:集合 \(A\) 與 \(B\) 的差集可以表示為:
\[A - B = \{x:x \in A \land x \not\in B \} \]一個集合的大小:集合 \(A\) 的大小可以表示為:
\[|A| \]容斥原理
定義:
\[|\bigcup_{i=1}^nA_i|=\sum_{j=1}^n(-1)^{j-1}\sum_{a_k\not=a_ {k+1}}\bigcap_{l=1}^mA_{a_i}\](算式來自 oi-wiki)
其實這個算式並沒有太多意義,重點還是要發現一道題是否要用容斥原理以及怎麼用
例題
P1450 [HAOI2008] 硬幣購物
思路:
這道題的重點在於如何轉換為容斥原理,我們把每一種使得最終和為 \(s\) 的方案(沒有數量限制)看作一個元素,則假設有所有方案的集合為 \(S\),而方案中第 \(i\) 種硬幣數量超出 \(d_i\) 的所有方案的集合為 \(A_i\),則我們需要求的答案其實就是:
\[|S|-|\bigcup_{i=1}^4A_i| \]那麼該如何的去求出 \(|S|\) 與 \(|\bigcup_{i=1}^4A_i|\)
首先我們設 \(dp_i\) 表示硬幣數量不受限制,最終和為 \(i\) 的方法數,這很明顯是一個完全揹包,由於題目中 \(s \le 10^5\) ,所以直接預處理即可,這樣 \(|S|\) 就是 \(dp_s\)。
void init() {
dp[0] = 1;
for (int i = 1; i <= 4; i++) {
for (int j = c[i]; j <= 100000; j++)
dp[j] += dp[j - c[i]];
}
}
考慮如何求 \(|A_i|\),我們可以先取 \(d_i+1\) 個 \(i\) 種硬幣,那麼還剩下 \(s-(d_i+1) \times c_i\)
同理,如果 \(i\) 與 \(j\) 都超出了限制,你們方法數也應該為 \(dp_{s-(d_i + 1) \times c_i - (d_j+1) \times c_j}\) ,三個或四個的以此類推。
這很明顯是一個容斥,於是直接根據容斥原理算,只用列舉每次是哪幾類超出限制即可,複雜度 \(O(2^4n)=O(16n)=O(n)\)。
如果這題物品種類有 \(m\) 個,複雜度就是 \(O(n2^m)\)。
code
#include <iostream>
#include <cstdio>
#include <vector>
#include <cstring>
#include <algorithm>
using namespace std;
int c[5] = {0}, n;
int d[5] = {0}, s;
long long dp[100005] = {0};
bool f[5] = {false};
long long cal() {
int cnt = 0;
long long sum = 0;
for (int i = 1; i <= 4; i++)
if (f[i])
cnt++, sum += (d[i] + 1) * c[i];
if (s < sum)
return 0;
return (cnt % 2 == 1 ? 1ll : (cnt == 0 ? 0 : -1ll)) * dp[s - sum];
}
long long dfs(int k) {
if (k > 4)
return cal();
long long ans = 0;
f[k] = true;
ans += dfs(k + 1);
f[k] = false;
ans += dfs(k + 1);
return ans;
}
void solve() {
for (int i = 1; i <= 4; i++)
cin >> d[i];
cin >> s;
memset(f, false, sizeof f);
cout << dp[s] - dfs(1) << endl;
}
void init() {
dp[0] = 1;
for (int i = 1; i <= 4; i++)
for (int j = c[i]; j <= 100000; j++)
dp[j] += dp[j - c[i]];
}
int main() {
for (int i = 1; i <= 4; i++)
cin >> c[i];
cin >> n;
init();
while (n--)
solve();
return 0;
}
P5505 [JSOI2011]分特產
題目連結:P5505 [JSOI2011]分特產
思路:
這道題的重點以人為單位來計數。
首先說一下可重組合,即把 \(n\) 分成 \(m\) 個非負整數集合,它們的和為 \(n\) 的方法數,我們用小學奧數的擋板法即可得到答案是:\(C_{n+(m-1)}^{m-1}\)。
我們設 \(T_{i,k}\) 表示把第 \(i\) 種特產,數量為 \(a_i\),分給 \(k\) 個人的方法數(不一定每個人都要分到)。這個問題和上面其實是同一個問題,所以 \(T_{i,k}=C_{a_i+(k-1)}^{k-1}\)。
設 \(N_k\) 為把所有特產分給 \(k\) 個人的方法數(依然有人可能沒拿到),因為每個特產都要被分發,且第 \(i\) 種特產分給 \(n\) 個人的方法數是 \(T_{i,n}\),所以這是一個乘法原理,即:
\[N_k=\prod_{i=1}^mT_{i,k} \]設集合 \(A_i\) 為第 \(i\) 名同學沒有被分到特產的所有方案的集合,\(S\) 為所有人分所有特產(有人可以沒分到)的方案的集合,因為我們要保證每個人都被分到,所以我們要求的就是:\(|S|-|\bigcup\limits_{i=1}^nA_i|\)。
很明顯 \(|S| = N_n\),考慮如何求 \(|\bigcup\limits_{i=1}^nA_i|\)。我們先考慮如果某一個同學沒拿到特產的方法數(別的也不一定都拿到)應該為 \(N_{n-1}\),因為有 \(n\) 個人,所以總共是: \(C_n^1 \times N_{n-1}\)。在考慮有兩個人沒拿到特產的方法數,總共應該是 \(C_n^2 \times N_{n-2}\),而上面這兩者是有交集的,於是根據容斥原理,我們可以以此類推,得到:
\[|\bigcup\limits_{i=1}^nA_i|=\sum_{i=1}^n(-1)^{i+1}C_n^i\times N_{n-i} \]所以答案其實就是:
\[\sum_{i=0}^n(-1)^{i}C_n^i\times N_{n-i} \]然後就快樂的 AC 了~~
code
#include <iostream>
#include <cstdio>
#include <vector>
#include <cstring>
#include <algorithm>
using namespace std;
const int mod = 1e9 + 7;
const int MAXN = 1000;
int fpow(int a, int b, int p) {
if (b == 0)
return 1;
int ans = fpow(a, b / 2, p);
ans = (1ll * ans * ans) % p;
if (b % 2 == 1)
ans = (1ll * ans * a) % p;
return ans;
}
int fac[2 * MAXN + 5] = {0}, inv[2 * MAXN + 5] = {0};
void init() {
fac[0] = 1;
for (int i = 1; i <= 2000; i++)
fac[i] = (1ll * fac[i - 1] * i) % mod;
inv[2000] = fpow(fac[2000], mod - 2, mod);
for (int i = 1999; i >= 0; i--)
inv[i] = (1ll * inv[i + 1] * (i + 1)) % mod;
}
int cmb(int n, int m) {//從n個數中選m個
return (1ll * (1ll * fac[n] * inv[m]) % mod * inv[n - m]) % mod;
}
int rep(int n, int m) {//把n個物體分給m個人
return cmb(n + (m - 1), m - 1);
}
int n, m;
int a[MAXN + 5] = {0};
int N[MAXN + 5] = {0};
int main() {
init();
cin >> n >> m;
for (int i = 1; i <= m; i++)
cin >> a[i];
for (int i = 1; i <= n; i++) {
N[i] = 1;
for (int j = 1; j <= m; j++)
N[i] = (1ll * rep(a[j], i) * N[i]) % mod;
}
int ans = N[n];
for (int i = 1; i <= n; i++)
if (i % 2 == 1)
ans = (ans + mod - (1ll * cmb(n, i) * N[n - i]) % mod) % mod;
else
ans = (ans + mod + (1ll * cmb(n, i) * N[n - i]) % mod) % mod;
cout << ans << endl;
return 0;
}
P6076 [JSOI2015]染色問題
題目連結:P6076 [JSOI2015]染色問題
思路:
設 \(T_i\) 為有 \(i\) 種顏色確定不用,剩下的顏色隨意的方法數,則根據容斥原理,我們要求的就是:
\[\sum_{i=0}^n(-1)^i\times C_n^i \times T_i \]考慮如何求 \(T_k\)。我們記 \(N_i\) 為有 \(i\) 行確定不塗色,其他行隨意的,但是每一列都有顏色的方法數,則根據容斥原理,很明顯:
\[T_k = \sum_{i=0}^n(-1)^i\times C_n^i\times N_i \]考慮如何求 \(N_i\)(別煩,這時最後一個了)。我們考慮把每一列拆開來,因為每一列有 \(n-i\) 個格子需要染色,每個格子有 \(c-k+1\) 種染色方法(不染色也算一種),所以對於一列來說,共有 \((c-k+1)^{n-i}\) 種染色方式,但是不能全部不染色,所以還要減去一,即 \((c-k+1)^{n-i}-1\)。因為總共有 \(m\) 列,並且每一列是相互獨立的,於是就知道了:
\[N_i = [(c-k+1)^{n-i}-1]^m \]然後就可以求出 \(T_k\),最後求出答案了。
在具體實現中,其實並不需要去真的開三個陣列。這題除了求組合數時階乘以及逆元需要開個陣列,其他都沒必要。
程式碼很簡單,但其實思維難度還是很高的。
code
#include <iostream>
#include <cstdio>
#include <vector>
#include <cstring>
#include <algorithm>
using namespace std;
const int mod = 1e9 + 7;
const int MAXN = 1000;
int fpow(int a, int b, int p) {
if (b == 0)
return 1;
int ans = fpow(a, b / 2, p);
ans = (1ll * ans * ans) % p;
if (b % 2 == 1)
ans = (1ll * ans * a) % p;
return ans;
}
int fac[2 * MAXN + 5] = {0}, inv[2 * MAXN + 5] = {0};
void init() {
fac[0] = 1;
for (int i = 1; i <= 2000; i++)
fac[i] = (1ll * fac[i - 1] * i) % mod;
inv[2000] = fpow(fac[2000], mod - 2, mod);
for (int i = 1999; i >= 0; i--)
inv[i] = (1ll * inv[i + 1] * (i + 1)) % mod;
}
int cmb(int n, int m) {
return (1ll * (1ll * fac[n] * inv[m]) % mod * inv[n - m]) % mod;
}
int n, m, c;
int cal(int k) {
int ans = 0;
for (int i = 0; i <= n; i++)
if (i % 2 == 0)
ans = (ans + mod + (1ll * cmb(n, i) * fpow(fpow(c - k + 1, n - i, mod) - 1, m, mod) % mod)) % mod;
else
ans = (ans + mod - (1ll * cmb(n, i) * fpow(fpow(c - k + 1, n - i, mod) - 1, m, mod) % mod)) % mod;
return ans;
}
int main() {
init();
cin >> n >> m >> c;
int ans = 0;
for (int i = 0; i <= c; i++)
if (i % 2 == 0)
ans = (ans + mod + (1ll * cmb(c, i) * cal(i)) % mod) % mod;
else
ans = (ans + mod - (1ll * cmb(c, i) * cal(i)) % mod) % mod;
cout << ans << endl;
return 0;
}