容斥定理整理

前置知识-容斥定理:

AA1, A2...Ak ⋃Ai = A

 

1. 两集合的简单容斥



 

2. 更一般的容斥定理



 

3. 子集容斥-子集反演



 


洛谷P10580

题意

na, gcd (a) = x, lcm(a) = y, 

 

解析

xy

不同质因子相互独立,考虑研究某个质因子,假设是 p1

分解 ai = p1ki1p2ki2p3ki3...

因为gcd (a) = x,我们有 min ki1 = c1

因为lcm(a) = x,我们有 max ki1 = e1

也就是所有序列中所有数,都要满足,,且至少存在一个数的幂是 c1,至少存在一个数的幂是 c2

 

t = y/x,分解 t = p1k1p2k2p3k3...

对于某种质因子,我们等价转化成如下问题:

有 n 个盒子,每个盒子可以放 [0, k] 个球,至少有一个盒子放了 0 个,且至少有一个盒子放了 k 个,求方案数

 

正向算不好算,考虑无 0,无 k

易得,A = U − A1 ∪ A2

 

由容斥定理

 

MYCODE

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

const int mo = 998244353;

ll qpow(ll a, ll x){
    ll ans = 1;
    while(x){
        if(x & 1) ans *= a;
        a *= a;
        x >>= 1;

        a %= mo;
        ans %= mo;
    }
    return ans;
}

int main(){
    int q;
    cin >> q;
    for(int qq=0; qq<q; qq++){
        int x, y, n;
        cin >> x >> y >> n;
        int z = y/x;

        // 分解质因子
        map<int, int> fct;
        for(int i=2; i*i<=z; i++){
            while(z % i == 0){
                z /= i;
                fct[i]++;
            }
        }
        if(z != 1) fct[z]++;

        ll ans = 1;
        for(auto [p, k] : fct){ // 计算每种质因子的答案 , 每种质因子情况独立
            ans *= (qpow(k+1, n) - 2*qpow(k, n) + qpow(k-1, n)) % mo;
            ans = (ans%mo + mo) % mo;
        }
        cout << ans << '\n';
    }
}

 


ZJU-Summer 2023

题意

n ⋅ m, 

(n, m ≤ 5e4)

解析

正向算不好算,考虑存在空行或空列,设为情况集合 S,ans = 2nm − |S|

容斥定义 :

riicii

RiiCii

 

 

发现 i,j 可以分开,简化求和。

由于没有 i + j = 0 这一项,即没有 i = j = 0 的情况,这种情况我们要减去

 



 

O(n2)i

 

回忆一下二项式定理

你可能已经注意到了, 不正是二项式定理的形式么

我们让它更规范一些

所以我们有

 

终于,到现在 |S| 就可以 o(nlog) 计算了

递推预处理一下 ,这样,这道题就完美解决了

 

MYCODE

#include <bits/stdc++.h>
using namespace std;
using ll = long long;
 
const int mo = 1e9 + 7;
 
const int MX = 2e5;
vector<ll> fct(MX+1);
vector<ll> inv_fct(MX+1);
vector<ll> inv(MX+1);
 
void init(){ // 预处理
    fct[0] = inv_fct[0] = inv[1] = 1;
    for(int i=2; i<=MX; i++){
        inv[i] = (mo - mo / i) * inv[mo % i] % mo;
    }
    for(int i=1; i<=MX; i++){
        fct[i] = fct[i-1] * i % mo;
        inv_fct[i] =inv_fct[i-1] * inv[i] % mo; 
    }
}
 
ll comb(ll n, ll k){ // C(n, k) 组合数
    ll ans = 1ll * fct[n] * inv_fct[n-k] % mo * inv_fct[k] % mo;
    return ans;
}
 
ll qpow(ll a, ll x){ // 快速幂
    ll ans = 1;
    while(x){
        if(x & 1) ans *= a;
        a *= a;
        x >>= 1;
        
        a %= mo;
        ans %= mo;
    }
    return ans;
}
 
void solve(){
    int n, m;
    cin >> n >> m;
    ll ans = 0;
    for(int i=0; i<=n; i++){
        ans += (i&1? -1:1) * comb(n, i) * qpow((qpow(2, n-i) - 1), m) % mo;
        ans = (ans + mo) % mo;
    }
    cout << ans << '\n';
}
 
int main(){
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    init();

    int t = 1; cin >> t;
    while(t--) solve();

    return 0;
}

 


ABC423-F

题意

y ∈ [1, Y],nA1Anai ∣ yy使m

( m ≤ n ≤ 20, Y ≤ 1e18, ai ≤ 1e18 )

解析

子集容斥(子集莫比乌斯反演):

事件集合 S

g(S) S (S)

f(S) S (S)

 

我们有 g(S) = ∑S ⊆ T ⊆ Uf(T)

由子集反演公式:

 

我们需要计算 满足|S|=m 的所有子集 S 的答案,即



 

又到了精彩的推式子环节

 

对于每个子集去枚举超集,时间复杂度 o(3n),需要优化

可以交换 S, T 求和顺序

 

m ≤ |T| O(2n)

g(T),O(2nlog)

O(2nlog + n)

 

MYCODE

#include <bits/stdc++.h>
using namespace std;
using ll = long long;
using ull = unsigned long long;
using i128 = __int128;

const ll inf = 2e18;

// fct[20] 刚好不会爆 ll 
// MX 再大些可以用 n^2 递推求
const int MX = 20;
vector<ll> fct(MX + 1);
void init(){ 
    fct[0] = 1;
    for(int i=1; i<=MX; i++){
        fct[i] = i * fct[i-1];
    }
}

void solve(){
    ll n, m, Y;
    cin >> n >> m >> Y;
    vector<ll> a(n);
    for(int i=0; i<n; i++) cin >> a[i];

    // 得到最低位 1
    auto get = [&](ll x){
        int p = __lg(x & -x);
        return p;
    };
    
    auto comb = [&](ll n, ll k){
        return fct[n] / fct[n-m] / fct[m];
    };

    vector<ll> pre(1 << n, 1), g(1 << n); // 预处理 lcm(S), g(S)
    int full = (1 << n) - 1;
    for(int S = 1; S <= full; S++){
        int lo = get(S);
        i128 l = i128(pre[S ^ (1 << lo)]) * a[lo] / gcd(pre[S ^ (1 << lo)], a[lo]);

        if(l > Y){ // g(S) = 0;
            pre[S] = inf;
            continue;
        }

        pre[S] = l;
        g[S] = Y / pre[S];
    }

    // ull 技巧,因为答案不会爆 ll,但过程计算可能爆,可以直接用 ull,自带取模
    ull ans = 0;
    for(int S = 1; S <= full; S++){
        int k = __builtin_popcount(S);
        if(k < m) continue;
        ans += 1ull * comb(k, m) * g[S] * ((k-m)&1? -1:1);
    }
    cout << ans << '\n';
}

int main(){
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    init();

    int t = 1; //cin >> t;
    while(t--) solve();

    return 0;
}