【算法专题】容斥原理

【算法专题】容斥原理

这类题主要是从反方向(所求=全集-补集)去思考,枚举补集的子集,复杂度一般为 \(O(2^n)\)
适用 \(n=20\) 左右。

E. Devu and Flowers

https://codeforces.com/contest/451/problem/E
前置知识:隔板法
然后正难则反,把至多取 \(a_i\) 个转化为 至少取 \(a_i+1\) 的反问题,就能套用隔板法的公式了。
答案即为:

#include <bits/stdc++.h>
#define ll long long

using namespace std;
const int N = 25, mod = 1e9 + 7;
ll c[N], n, m, sum, fm = 1;

ll qmi(ll a, ll k, ll p){
    ll res = 1;
    while(k){
        if(k & 1)    res = (ll)res * a % p;
        a = (ll)a * a % p;
        k >>= 1;
    }
    return res;
}

ll C (ll a, ll b) { //a! / (b!(a-b)!)
    if (a < b)  return 0;
    ll fz = 1;
    for (ll i = a; i > a - b; i--)     (fz *= i % mod) %= mod;
    return (fz % mod * fm) % mod;
}

int main () {
    cin >> n >> m;
    for (int i = 0; i < n; i++)    cin >> c[i];
    for (int i = 1; i < n; i++)     fm = (i * fm) % mod;
    fm = qmi (fm, mod - 2, mod);
    for (int i = 0; i < 1ll << n; i++) { //二进制枚举
        ll a = m + n - 1, b = n - 1, sign = 1;
        for (int j = 0; j < n; j++) {
            if (i >> j & 1) {
                sign *= -1;
                a -= c[j] + 1;
            }
        }
        sum = (sum + C (a, b) * sign + mod) % mod;        
    }
    cout << (sum + mod) % mod;
}

215. 破译密码

https://www.acwing.com/problem/content/217/
前置知识:莫比乌斯函数数论分块

题解:https://www.acwing.com/solution/content/17858/

建议顺便复习一下这题:https://www.luogu.com.cn/problem/P2261

#include <bits/stdc++.h>
#define ll long long

using namespace std;
const int N = 50005;
ll a, b, d, mobius[N], prime[N], sum[N];
bool vis[N];

void init (int x) { //线性筛法求mobius函数
    mobius[1] = 1;
    int cnt = 0;
    for (int i = 2; i <= x; i++) {
        if (!vis[i])    prime[++cnt] = i, mobius[i] = -1;
        for (int j = 1; i * prime[j] <= x; j++) {
            int t = i * prime[j];
            vis[t] = true;
            if (i % prime[j] == 0) {
                mobius[t] = 0;
                break;
            }
            mobius[t] = mobius[i] * -1;
        }
    }
    for (int i = 1; i <= x; i++)    sum[i] = sum[i-1] + mobius[i];
}

void solve () {
    scanf ("%lld%lld%lld", &a, &b, &d);
    a /= d, b /= d; //即求 x<=a, y<=b, (a,b)=1的个数
    ll l = 1, r, n = min (a, b), ans = 0;
    while (l <= n) {
        r = min (n, min (a / (a / l), b / (b / l)));
        ans += (a / l) * (b / l) * (sum[r] - sum[l-1]);
        l = r + 1;
    }
    printf ("%lld\n", ans);
}

int main () {
    init (N - 1);
    int t;
    scanf ("%d", &t);
    while (t--) solve ();
}

F - Tree and Constraints

https://atcoder.jp/contests/abc152/tasks/abc152_f
蛮难的感觉。
看这里:https://www.luogu.com.cn/blog/subtlemaple/solution-at-abc152-f

#include <bits/stdc++.h>
#define ll long long

using namespace std;
const int N = 55;
int h[N], e[N*2], ne[N*2], idx;
ll n, m, d[N], st[N]; //di为限制条件, st[i]当前边状态

void add (int a, int b) {
    e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}

void dfs (int u, int fa) {
    for (int i = h[u]; ~i; i = ne[i]) {
        int j = e[i];
        if (j == fa)    continue;
        d[j] = d[u] | (1ll << (j - 1)); //根节点到 x 经过的边的状压数值
        dfs (j, u);  
    }
}

ll count (ll S) { //f(S) = 2^{n-1-|S|}
    ll cnt = __builtin_popcountll (S), T = 0; //T为并集
    for (int i = 0; i < m; i++) {
        if (S & (1ll << i)) {
            T |= st[i];
        }
    }
    ll sum = (1ll << (n - 1 - __builtin_popcountll (T)));
    if (cnt & 1)   sum *= -1ll; //容斥系数
    return sum;
}

int main () {
    memset (h, -1, sizeof h);
    cin >> n;
    for (int i = 1; i < n; i++) {
        int a, b;
        cin >> a >> b;
        add (a, b), add (b, a);
    }
    cin >> m;
    dfs (1, -1);

    for (int i = 0; i < m; i++) {
        int a, b;
        cin >> a >> b;
        st[i] = d[a] ^ d[b]; //经典的树上异或性质:x到y的路径上的边的状压数值
    }
    ll ans = 0;
    //0的状态是全集
    for (ll i = 0; i < (1ll << m); i++)    ans += count (i);
    cout << ans << endl;
}
posted @ 2023-04-01 17:03  Sakana~  阅读(60)  评论(0编辑  收藏  举报