您的位置:首页 > 其它

【专题总结】容斥原理(持续更新)

2016-07-10 22:53 686 查看

从动机的角度出发。在用“做减法”的思想解决计数类问题时,可能会遇到“多减去符合条件的数目”,试图加回来的时候又会遇到“多加上不符合条件的数目”的情况,这时候也许需要用容斥原理来设计计数算法。

从实现的角度出发。在对事件集合的“并事件”计数遇到困难时,可通过容斥原理转化成对事件子集的“交事件”的计数。即“大并化小交”)



UVAOJ 11806

大意

将 k 个棋子放在 n×m 的棋盘上。我们将连续的紧贴棋盘边界的放置棋子位置称为边缘。问上下左右四个边缘上都至少放置一个棋子的棋子放置方案共有多少种。

思路

根据“做减法”的思想,我们能够起草一种貌似正确的算法:将无任何限制的摆放棋子的方法数 sum 减去没有棋子放在上边缘的方法数 sumup 减去没有棋子放在下边缘的方法数 sumdown…… 以此类推,再减去 sumleft 和 sumright 就能得出结果 ans 。但是这样做会导致重复减掉一些东西,我们要将这些东西( sumleftAndUp,sumrightAndDown…… )加回来。我们可以用容斥原理来解决这个问题。实现起来是将边缘是否有棋子用 0 和 1 来表示,于是上下左右四个边缘是否有棋子的情况一共需要 16 个状态来表示。对每个状态计算有多少个边缘被编码为 1 ,根据“奇加偶减”的容斥原理计算公式便可以知道该状态的摆放方案数是应该加到答案中还是应该被从答案中减去。(每种状态的摆放方案数可以用组合数来计算。)

代码

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

const int mod = 1e6 + 7, maxk = 500;
int t, n, m, k, r, c, b, ans, C[maxk+5][maxk+5];

// 预处理出组合数
void makeComb(int n) {
memset(C, 0, sizeof(C));
for(int i = 0; i <= n; i++) {
C[i][0] = C[i][i] = 1;
for(int j = 1; j < i ; j++) {
C[i][j] = (C[i-1][j] + C[i-1][j-1]) % mod;
}
}
}

int main() {
makeComb(maxk);
scanf("%d", &t);
for(int kase = 1; kase <= t; kase++) {
scanf("%d%d%d", &n, &m, &k);
ans = 0;
// 枚举压缩后的状态
for(int s = 0; s < 16; s++) {
r = n;
c = m;
b = 0;
if(s & 1) {
r--;
b++;
}
if(s & 2) {
r--;
b++;
}
if(s & 4) {
c--;
b++;
}
if(s & 8) {
c--;
b++;
}
// 奇加偶减
if(b & 1) {
ans = (ans - C[r*c][k] + mod) % mod;
}
else {
ans = (ans + C[r*c][k]) % mod;
}
}
printf("Case %d: %d\n", kase, ans);
}
return 0;
}


HDU 5120

大意

求平面直角坐标系上的两个圆环的相交部分的面积。

思路

一个圆环由一个大圆和一个小圆构成。将两个相交圆环在纸上画出来后,两个圆环相交部分的面积实际上等于两个大圆的相交面积减去两个“大圆与小圆”的相交面积,再加回两个小圆的相交面积就是最后的结果。这是容斥原理的一种特殊情况,因此可以直接得出简短的公式并将数值代入进行计算。

代码

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

const double eps = 1e-10, pi = 4 * atan(1.0);

int dcmp(double x) {
if(fabs(x) < eps) return 0;
return x < 0 ? -1 : 1;
}

struct Point {
double x, y;
Point() {}
Point(double x, double y): x(x), y(y) {}
double distance(Point p) {
return hypot(x - p.x, y - p.y);
}
};

struct Circle {
double r;
Point o;
Circle() {}
Circle(double r, Point o): r(r), o(o) {}
double area(Circle c) {
double d = o.distance(c.o);
if(dcmp(d - r - c.r) >= 0) {
return 0;
}
if(dcmp(d - fabs(r - c.r)) <= 0) {
double R = dcmp(r - c.r) < 0 ? r : c.r;
return pi * R * R;
}
double x = (r * r + d * d - c.r * c.r) / (2 * d);
double a1 = acos(x / r);
double a2 = acos((d - x) / c.r);
double s1 = a1 * r * r + a2 * c.r * c.r;
double s2 = r * d * sin(a1);
return s1 - s2;
}
};

int main() {
int t;
scanf("%d", &t);
double r, R, x1, y1, x2, y2;
for(int kase = 1; kase <= t; kase++) {
scanf("%lf%lf%lf%lf%lf%lf", &r, &R, &x1, &y1, &x2, &y2);
Point o1(x1, y1), o2(x2, y2);
Circle c1(r, o1), C1(R, o1);
Circle c2(r, o2), C2(R, o2);
double s1 = C1.area(C2);
double s2 = C1.area(c2);
double s3 = c1.area(c2);
printf("Case #%d: %.6f\n", kase, s1 - 2 * s2 + s3);
}
return 0;
}


HDU 4135

大意

问区间 [A,B] 中与 n 互质的数有多少个。

思路

由于 A,B 的规模都比较大,因此无法枚举 [A,B] 中的所有数同时检查是否与 n 互质。所以将目光投向“互质”。“互质”是否能为我们带来可用的性质,使得题目的计数变为可能呢?很遗憾,这应该是比较麻烦的。“互质”这个条件太强了。

于是尝试换一个方向思考,能否求出“不互质”的情况,再让总情况减去“不互质”的情况呢?尝试思考“不互质”的本质。因为“整数的结构是由质因数决定的”,因此“不互质”的本质就是“有公因数”。所以,我们将问题转化为“问区间 [A,B] 中与 n 有公因数的数有多少个”。我们还可以进一步转化问题,使得问题更加简化——“问区间 [1,x] 中与 n 有公因数的数有多少个”并将这个问题的答案记作 ans[x] ,那么原问题就等价于求 B−ans[B]−((A−1)−ans[A−1]) 。

现在当务之急是找出 ans[x] 的解法。也就是求解区间 [1,x] 中有与 n 有公因数的数有多少个。遗憾的是,这也是很难求的。但是我们可以求区间 [1,x] 中含有 n 的因子 f 的数有多少个,将其命名为 subAns[x,f] ,其答案就是 xf 。到此,可行的解法终于浮出水面。我们可以用容斥原理将所有的 subAns[x,f] 求出来——其中 f 可以是 n 的因子,也可以是 n 的因子的乘积——根据“奇加偶减”的计算方法将这些“小交集”的计数结果“拼接”成“大并集”的计数结果。这题的答案也就水到渠成地被解出来了。

代码

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

typedef long long ll;
int t, n, m, cnt, full;
ll a, b, pro, ans;
vector <int> f;

ll solve(ll x) {
ll res = 0;
for(int i = 1; i <= full; i++) {
int pro = 1;
int cnt = 0;
for(int j = 0; j < m; j++) {
if((i >> j) & 1) {
pro *= f[j];
cnt++;
}
}
if(cnt & 1) {
res += x / pro;
}
else {
res -= x / pro;
}
}
return x - res;
}

int main() {
scanf("%d", &t);
for(int kase = 1; kase <= t; kase++) {
scanf("%I64d%I64d%d", &a, &b, &n);
f.clear();
for(int i = 2; i * i <= n; i++) {
if(n % i == 0) {
f.push_back(i);
while(n % i == 0) {
n /= i;
}
}
}
if(n != 1) {
f.push_back(n);
}
m = f.size();
full = (1 << m) - 1;
printf("Case #%d: %I64d\n", kase, solve(b) - solve(a - 1));
}
return 0;
}


HDU 5768

大意

给定一个区间 [x,y] 和 n 个数对 (pi,ai) ,问在区间中的能被 7 整除的,并且对所有 i ,被 pi 除不余 ai 的数共有多少个。

思路

本题实际上要我们求对于一个给定的 r ,在区间 (0,r] 中,被 7 整除的数的个数 seven(r) 与“被 7 整除且存在某个 pi ,使其除它余 ai ”的数的个数 notOk(r) 之差(简单地说就是 seven(r)−notOk(r) )。为什么要将原问题向这个方向转化呢?因为 seven(r) 和 notOk(r) 都是可求的。其中 seven(r)=r/7 ,而 notOk(r) 可以用容斥原理算出。

notOk(r)相当于集合“ {x|xmod7=0}⋃{x|xmodp1=a1}⋃...⋃{x|xmodpn=an} ”的元素数量。因将取并集之前的集合的元素数量简单相加会产生重复,因此要用容斥原理解决(大并集元素个数等于若干个小交集元素个数相加减的结果)。

由于 pi 之间是互质的,所以我们可以用中国剩余定理求出容斥原理的过程中,某个状态的“小交集”的元素的个数(方法类似于 seven(r)=r/7 )。

代码

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

typedef long long ll;
const int maxn = 20;
ll T, n, l, r, p[maxn], a[maxn], m[maxn], b[maxn];

// 快速幂算法
ll modMul(ll a, ll b, ll mod) {
ll ans = 0;
for(; b > 0; b >>= 1) {
if (b & 1) {
ans = (ans + a) % mod;
}
a = (a << 1) % mod;
}
return ans;
}

// 扩展欧几里得算法
void extGcd(ll a, ll b, ll& x, ll& y) {
if(b == 0) {
x = 1;
y = 0;
return;
}
extGcd(b, a % b, x, y);
ll tmp = x;
x = y;
y = tmp - (a / b) * y;
}

// 中国剩余定理
ll CRT(ll a[], ll m[], ll M, ll n) {
ll ans = 0;
for(int i = 0; i <= n; i++) {
ll x, y, Mi = M / m[i];
extGcd(Mi, m[i], x, y);
x = (x % M + M) % M;
ans = (ans + modMul(modMul(Mi, x, M), a[i], M)) % M;
}
return (ans + M) % M;
}

// 计算(0, r]内有多少满足条件的数
ll count(ll r) {
if(r < 0) {
return 0;
}
ll x, num, ans = r / 7;
// 状态压缩的容斥原理
for(int mask = 1; mask < (1 << n); mask++) {
ll tail = 0, cnt = 0, M = 7;
for(int j = 0; j < n; j++) {
if(mask & (1 << j)) {
m[++tail] = p[j];
b[tail] = a[j];
M *= m[tail];
cnt++;
}
}
x = CRT(b, m, M, tail);
if(r < x) {
continue;
}
num = (r - x) / M + 1;
ans += (cnt % 2 ? -1 : 1) * num;
}
return ans;
}

int main() {
cin >> T;
m[0] = 7;
for(int kase = 1; kase <= T; kase++) {
cin >> n >> l >> r;
for(int i = 0; i < n; i++) {
cin >> p[i] >> a[i];
}
cout << "Case #" << kase << ": ";
cout << count(r) - count(l - 1) << endl;
}
return 0;
}


HDU 5514

大意

有 m 个石子围成一圈,编号为 1−m 。又有 n 只青蛙,每只青蛙的跳跃距离为 ai ,所有青蛙从 1 号石子起跳,路过的石子都打上标记(每个青蛙的标记无区别),问最后被打上标记的石子的标号和是多少。

思路

首先根据“裴蜀定理”,当一只青蛙 i 将能打的所有标记打完后,它标记的相邻石子的间隔一定是 gi=gcd(ai,m) 。设以 1 为首项, gi 为公差的等差数列为 di 。那么答案 ans=∑sum(di)−rep ,其中 rep 表示这些数列的所有重复部分。理论上这里可以用容斥原理计算出 ans 。那么该怎么实现呢?能不能枚举所有 gi 的组合呢?答案是不能的,因为 n 的数据规模有 104 。好在一个 gi 影响的 gj 满足 i<j ,也就是我们可以将 sum(di) 加到答案中,然后枚举 gj ,将 gi 影响的 gj (满足 gjmodgi=0 )打上标记,当计算到 sum(dj) 的时候考虑之前 gi 施加的影响。我们应该注意到,所有 gi 都应该出现在 m 的约数中。因此在 m 的约数中找 gi 影响到的 gj 即可。

在数据量不允许枚举状态的时候,若能在很方便地无后效性地计算方案数同时给影响到的情况打上标记。之后的情况又能根据标记方便地将影响消除,就能够圆满地解决问题。

代码

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

typedef long long ll;
const int maxn = 1e4 + 10, maxFac = 1e3;
int t, n, m, a, g[maxn], painted[maxFac];
vector <int> fac;
ll ans;

ll sum(ll n, ll d) {
return n * (n-1) / 2 * d;
}

int main() {
scanf("%d", &t);
for(int kase = 1; kase <= t; kase++) {
scanf("%d%d", &n, &m);
for(int i = 0; i < n; i++) {
scanf("%d", &a);
g[i] = __gcd(a, m);
}
fac.clear();
for(int i = 1; i * i <= m; i++) {
if(m % i == 0) {
fac.push_back(i);
if(i * i != m) {
fac.push_back(m / i);
}
}
}
sort(fac.begin(), fac.end());
memset(painted, 0, sizeof(painted));
for(int i = 0; i < n; i++) {
for(int j = 0; j < fac.size(); j++) {
if(fac[j] % g[i] == 0) {
painted[j] = 1;
}
}
}
ans = 0;
for(int i = 0; i < fac.size(); i++) {
if(painted[i] != 0) {
ans += painted[i] * sum(m / fac[i], fac[i]);
for(int j = i + 1; j < fac.size(); j++) {
if(fac[j] % fac[i] == 0) {
painted[j] -= painted[i];
}
}
}
}
printf("Case #%d: %I64d\n", kase, ans);
}
return 0;
}
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息