这篇文章上次修改于 284 天前,可能其部分内容已经发生变化,如有疑问可询问作者。

算法学习笔记(11): 容斥原理的简单应用

容斥原理

容斥原理本身易于理解,实际上是减去重复计算的部分的思想。

类似于两个有交集的集和并集,就是两个集合相加减去交集。

容斥模型概念

  • 全集:各种元素不在属性限制的条件下的种类数
  • 元素:不同的变量,被限制的个体单元
  • 属性:限制条件。
  • 所求的解:所有变量满足对应属性时集合的大小

不定方程非负整数解计数

给你n个变量x,说明了每个x的约束条件,例如x < m, 又说明了所有x的总和值为S,求不同解的个数。即 x1 + x2 + x3 +…. + xn = S, (x1 < m1 , x2 < m2, x3 < m3……xn < mn)

如果没有约束条件:

对于原本的问题解,我们可以使用割板法,也就是排列组合即可,类似于分10个苹果给4个小朋友。

使用组合模型便可获取有f(S)代表 到当总量为S时,有多少种分配方法。

但有约束条件的情况下,不妨将题目抽象为容斥模型:

当总量为S,设定 g(i) 代表 xi 在约束条件下, 其它任意的种类数,可得g(i) = f(S) - f(S - (mi + 1) * xi), 意思就是所有情况减去xi超限的情况,xi超限情况只要直接让S预留mi + 1 个 x1的所有情况就是g(i)

此时我们我们得到的g(i) 就是满足第一个,但其他不满足的情况,如果我们将他们相加,就需要减去两个符合条件其他不符合,然后处理三个,在处理四个。这就是容斥经典模型。

[HAOI2008] 硬币购物

共有 4 种硬币。面值分别为 c1, c2, c3, c4。

某人去商店买东西,去了 n 次,对于每次购买,他带了 di 枚 i 种硬币,想购买 s 的价值的东西。请问每次有多少种付款方法。

思路同上,此时我们如何计算没有限制条件的组数,也就是任意数量能满足的种数:完全背包。

达成之后,就进行容斥即可。

题目思想突破口在于只有四种硬币,如果使用容斥定理,我们就需要2^4 - 1个情况考虑,四种而不是上百上千种硬币大大利好了容斥。

code:

#include<bits/stdc++.h>

using namespace std;
using i64 = long long;
const int maxN = 1e5 + 5;

void solve(){
    vector<i64> value(5);
    i64 n;
    for(int i = 1; i <= 4; i++)
        cin >> value[i];
    cin >> n;

    vector<i64> numOfSum(maxN);
    numOfSum[0] = 1;
    for(int type = 1; type <= 4; type++){
        for(int S = 1; S < maxN; S++){
            if(S - value[type] >= 0)
                numOfSum[S] += numOfSum[S - value[type]];
        }
    }
    vector<i64> maxCoin(5);
    while(n--){
        i64 Sum;
        i64 result = 0;
        cin >> maxCoin[1] >> maxCoin[2] >> maxCoin[3] >> maxCoin[4] >> Sum;
        for(int group = 1; group < 16; group++){
            i64 tmp = Sum, bits = 0;
            for(int j = 1; j <= 4; j++){
                if((group >> (j - 1)) % 2 == 1){
                    tmp -= (maxCoin[j] + 1) * value[j];
                    bits++;
                }
            }
            if (tmp >= 0)
                result += (bits % 2 * 2 - 1) * numOfSum[tmp];
        }
        cout << numOfSum[Sum] - result << endl;
    }
}

int main(){
    ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
    int T = 1;
    while(T--){
        solve();
    }
}

数论

最大公约数问题

例如:有1≤ x, y ≤ N , 设 f(k) 为最大公约数为 k 的数对个数,求f(1) ~ f(N);

找到最大公约数特征的容斥原理模型,什么会是有重叠的。

例如我们可以很轻松的找到以两个数都是 k 的倍数的数对,这个数对的GCD也就肯定是k的倍数。

我们记这样的数对个数为 g(k) ; g(k) 是很容易获取的;同时我们可以发现

f(k) = g(k) - f(2k) - f(3k) - f(4k) ….

因此我们就可以倒过来从后往前计算得到f(k) - f(1)的值

code:

for (long long k = N; k >= 1; k--) {
  f[k] = (N / k) * (N / k); // g(k) = (N / k) * (N / k)
  for (long long i = k + k; i <= N; i += k) f[k] -= f[i];
}

再例如给定序列 a1-n,求非空集和GCD为1 的数量;

设置答案 f(k) 表示GCD为k的非空集和数量。

同样我们可以设g(k) 为 GCD为 K的倍数的非空集和数量,设序列中K的倍数有t个也就是2^t个;

同时,我们也可以发现:

f(k) = g(k) - f(2 * k) - f(3 * k) - f(4 * k) …

同样可以倒过来计算得出。

code:

for(int i = 1; i < 1e6 + 10; i++){
    int sum = 0;
    for(int j = i; j < 1e6 + 10; j += i){
        sum += cnt[j]; // cnt 也就是 序列中数字j的个数
    }
    res[i] = pow2[sum] - 1;
}

for(int i = 1e6 + 10; i > 0; i--){
    for(int j = 2 * i; j < 1e6 + 10; j+= i){
        res[i] = (res[i] - res[j] + MOD) % MOD;
    }
}

这种gcd还会很隐蔽的被具体化成实际的二维图像:例如一个n*n点阵,如果两个点之间的xy轴差值gcd=1就代表两个点之间的线段没有遇到其他点。