容斥定理整理

前置知识-容斥定理:

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
#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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
#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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
#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;
}