容斥原理小结
若没有个数限制, 则可以用插板法解决
有个数限制:
设第 i 个箱子里选了 x_i 朵花
设集合 S_{s} 表示集合 s 中的花的数量 > a_i 的集合
则 |S_{s}|=C_{m+n-1-∑{(a_j+1),j∈s}}^{n-1}
组合数用定义求
点击查看代码
#include <iostream>
#include <stdio.h>
#include <string.h>
#include <algorithm>
#include <assert.h>
using namespace std;
typedef long long LL;
const int N = 25;
const LL mod = 1e9 + 7;
LL n, m;
LL a[N];
LL qpow(LL b, LL p) {
LL res = 1;
while(p) {
if(p & 1) res = res * b % mod;
b = b * b % mod, p >>= 1;
}
return res;
}
LL C(LL a, LL b) {
if(a < b) return 0;
LL up = 1, down = 1;
for(register LL i = a, j = 1; j <= b; i --, j ++) {
up = up * (i % mod) % mod;
down = down * j % mod;
}
return up * qpow(down, mod - 2) % mod;
}
int main() {
scanf("%lld%lld", &n, &m);
for(int i = 0; i < n; i ++)
scanf("%lld", a + i);
LL res = 0;
for(register LL s = 0; s < (1 << n); s ++) {
int cnt = 0;
LL sum = m + n - 1;
for(register int i = 0; i < n; i ++)
if(s >> i & 1) {
sum -= a[i] + 1;
cnt ++;
}
if(cnt & 1) res -= C(sum, n - 1);
else res += C(sum, n - 1);
res = (res % mod + mod) % mod;
}
printf("%lld\n", res);
return 0;
}
BZOJ2169 连边
DP: 设 f[i][j] 表示添加 i 条边后有 j 个度数为奇数的点的方案数
f[i][j] = (f[i-1][j-2] * C2(n-(j-2),2) -> 连接 偶点 和 偶点
+ f[i-1][j] * (n-j) * j -> 连接 奇点 和 偶点
+ f[i-1][j+2] * C2(j+2, 2) -> 连接 奇点 和 奇点
- f[i-2][j] * (C2(n,2)-i+2)) / i -> 减掉 重边
减去一项是减掉重边的, 除以 i 是除掉重复计算的
点击查看代码
#include <stdio.h>
#include <string.h>
#include <algorithm>
using namespace std;
typedef long long LL;
const int mod = 10007, N = 1e3 + 5;
int inv[N]; // 逆元
LL f[N][N];
int n, m, k, d[N]; // d[u] 为 u 的度数
int qpow(int b, int p) { // 快速幂
int res = 1;
while(p) {
if(p & 1) res = res * b % mod;
b = b * b % mod, p >>= 1;
}
return res;
}
inline int C2(int x) { return x * (x - 1) / 2; } // C(n,2)
int main() {
for(int i = 1; i <= 1000; i ++)
inv[i] = qpow(i, mod - 2);
scanf("%d%d%d", &n, &m, &k);
for(int i = 1, a, b; i <= m; i ++) {
scanf("%d%d", &a, &b);
d[a] ++, d[b] ++;
}
int cnt = 0;
for(int i = 1; i <= n; i ++)
if(d[i] % 2) cnt ++;
f[0][cnt] = 1; // 初始条件
for(int i = 1; i <= k; i ++) {
for(int j = 0; j <= n; j ++) {
if(j - 2 >= 0) (f[i][j] += f[i - 1][j - 2] * C2(n - j + 2) % mod) %= mod;
if(j + 2 <= n) (f[i][j] += f[i - 1][j + 2] * C2(j + 2) % mod) %= mod;
(f[i][j] += f[i - 1][j] * (n - j) * j % mod) %= mod;
if(i >= 2) (f[i][j] = f[i][j] - f[i - 2][j] * (C2(n) - i + 2) % mod + mod) %= mod;
(f[i][j] *= inv[i]) %= mod;
}
}
printf("%lld\n", f[k][0]);
return 0;
}
P1450 [HAOI2008] 硬币购物
点击查看代码
#include <iostream>
#include <stdio.h>
#include <string.h>
#include <algorithm>
using namespace std;
const int N = 100005;
int c[5], n;
int d[5], s;
long long f[N];
int main() {
scanf("%d%d%d%d%d", c, c + 1, c + 2, c + 3, &n);
f[0] = 1;
for(int i = 0; i < 4; i ++)
for(int j = c[i]; j <= 100000; j ++)
f[j] += f[j - c[i]];
while(n --) {
scanf("%d%d%d%d%d", d, d + 1, d + 2, d + 3, &s);
long long res = 0;
for(int S = 0; S < 16; S ++) {
int cnt = 0; int tmp = s;
for(int i = 0; i < 4; i ++)
if((S >> i) & 1) {
cnt ++;
tmp -= (long long)c[i] * (d[i] + 1);
}
if(tmp >= 0)
if(cnt & 1) res -= f[tmp];
else res += f[tmp];
}
printf("%lld\n", res);
}
return 0;
}
P2567 [SCOI2010]幸运数字
点击查看代码
// 使用剪枝的搜索解决容斥原理的问题
#include <stdio.h>
#include <algorithm>
using namespace std;
typedef long long LL;
LL number[2048], tot; // 好数字至多 2046 个
bool del[2048]; // 是否被删掉(如果好数字中有这个数的倍数, 则标记)
LL num2[2048], cnt2; // 被删掉后剩下的好数, 只剩下 943 个数(剪枝 1)
LL ans; // 答案
LL L, R;
// LL gcd(LL x, LL y) {
// while(x) {
// y %= x;
// x ^= y ^= x ^= y;
// }
// return y;
// }
#define gcd __gcd
inline int calc(LL l, LL r, LL d) { // 计算 [l, r] 中有多少个 d 的倍数
return r / d - (l - 1) / d;
}
// 使用搜索解决容斥原理问题 在搜索时如果 lcm 大于 R 则剪掉(剪枝 2)
// u 表示搜到第几个 num2, cnt 表示选的数的个数, lcm 表示当前的最小公倍数
void dfs(int u, int cnt, LL lcm) {
if(lcm > R) return; // 剪枝
if(u > cnt2) {
if(cnt) { // 排掉不选的情况
if(cnt & 1) ans += calc(L, R, lcm);
else ans -= calc(L, R, lcm);
}
} else {
dfs(u + 1, cnt, lcm); // 不选这个数
LL tmp = lcm / gcd(lcm, num2[u]);
if(double(tmp) * num2[u] <= R) // 小心爆 LL
dfs(u + 1, cnt + 1, tmp * num2[u]); // 选这个数
}
}
int main() {
for(int n = 1; n <= 10; n ++) {
for(int s = 0; s < (1 << n); s ++) {
LL num = 0;
for(int i = 0; i < n; i ++)
num = (num << 1) + (num << 3) + 6 + ((s >> i & 1) << 1);
number[++ tot] = num;
}
}
sort(number + 1, number + tot + 1);
for(int i = 1; i <= tot; i ++)
for(int j = i + 1; j <= tot; j ++)
if(number[j] % number[i] == 0) del[j] = true;
for(int i = 1; i <= tot; i ++)
if(!del[i]) num2[++ cnt2] = number[i];
reverse(num2 + 1, num2 + cnt2 + 1); // 剪枝 3 : 从大到小排序增加搜索效率
scanf("%lld%lld", &L, &R);
dfs(1, 0, 1);
printf("%lld\n", ans);
return 0;
}
P5505 [JSOI2011] 分特产
待做
[JSOI2015]染色问题
容斥原理+DP 答案=没有限制3的答案-∑某一列不满足性质3的答案, 记号:行表示方案,纵表示食材
第2类: 不满足性质3的只有一种列(必须>k/2)于是求和只需枚举列超过1/2的,令枚举的这列为c
设f[i][j][k]表示前i行,第c列选j个,其余列选k个的方案数(其中s[i]表示第i行的和)
转移: f[i][j][k] = f[i-1][j][k] + a[i][c]f[i-1][j-1][k] + (s[i]-a[i][c])f[i-1][j][k-1]
优化: f[i][t]表示前i行,j-k=t的方案数,统计答案时只找t>0的 ; 初始: f[0][0] = 1
转移: f[i][t] = f[i-1][t] + a[i][c]f[i-1][t-1] + (s[i]-a[i][c])f[i-1][t+1]
第1类: 类似前面的转移;设g[i][j]表示前i行选j个的方案数 ; 初始: g[0][0] = 1
转移: g[i][j] = g[i-1][j] + s[i]*g[i-1][j-1]
点击查看代码
#include <stdio.h>
#include <string.h>
const int N = 105, M = 2e3 + 5, mod = 998244353;
typedef long long LL;
int n, m, a[N][M], s[N];
int f[N][N << 1], g[N][N];
int ans;
int main() {
scanf("%d%d", &n, &m);
for(int i = 1; i <= n; i ++)
for(int j = 1; j <= m; j ++)
scanf("%d", &a[i][j]), (s[i] += a[i][j]) >= mod && (s[i] -= mod);
for(int c = 1; c <= m; c ++) {
memset(f, 0, sizeof(f)), f[0][n] = 1; // 由于t可能是负数,将下标+n
for(int i = 1; i <= n; i ++)
for(int t = n - i; t <= n + i; t ++)
f[i][t] = (f[i-1][t] + (LL)a[i][c] * f[i-1][t-1] % mod + LL(s[i]-a[i][c]+mod) * f[i-1][t+1] % mod) % mod;
for(int i = 1; i <= n; i ++) (ans += f[n][n + i]) >= mod && (ans -= mod);
}
(ans = mod - ans) >= mod && (ans -= mod), g[0][0] = 1;
for(int i = 1; i <= n; i ++) {
g[i][0] = g[i - 1][0];
for(int j = 1; j <= n; j ++)
g[i][j] = (g[i - 1][j] + (LL)s[i] * g[i - 1][j - 1] % mod) % mod;
}
for(int i = 1; i <= n; i ++) (ans += g[n][i]) >= mod && (ans -= mod);
printf("%d\n", ans);
return 0;
}