容斥学习笔记
日志
- 2023/12/6 初步施工。
- 2023/12/8 增加了一些例题、内容。修改了一些错误/模糊的表述。
- 2023/12/10 增加了一些例题。
- 还未完结。
容斥原理
我们有:
即:
还有:
即满足所有条件 \(P_i\) 的方案数等于所有的方案数减去不满足其中任意一条的方案数,并且不满足其中任意一条的方案数可以用容斥简单的计算,也就是说不满足某些条件的交集的大小可以方便的求出时,可以考虑容斥计算。
容斥原理求交集大小
一般情况下直接求 \(|\bigcap S_i|\) 的复杂度很高,而求 \(\overline{S_i}\) 和之间的交集大小很快时我们考虑用容斥原理计算。
这里以一些道例题为例。
P1450 [HAOI2008] 硬币购物
题意:共有 \(4\) 种硬币。面值分别为 \(c_1,c_2,c_3,c_4\)。某人去商店买东西,去了 \(n\) 次,对于每次购买,他带了 \(d_i\) 枚 \(i\) 种硬币,想购买 \(s\) 的价值的东西。请问每次有多少种付款方法。
题目要求的是满足 \(\sum c_iu_i=s\) 的方案数,且 \(u_i\le d_i\),设 \(u_i\le d_i\) 表示第 \(i\) 条限制,\(S_i\) 表示满足第 \(i\) 条限制的方案集合,则答案为:
其中 \(\overline{S_i}\) 表示不满足第 \(i\) 条限制的方案集合,也就是 \(u_i>d_i\),可以容斥计算,显然 \(\overline{S_i}\cap \overline{S_j}\cap \overline{S_k}\dots\) 的情形可以用组合数统计方案数,于是我们轻松的完成了问题。
// qwq
#include <bits/stdc++.h>
#define rep(I,A,B) for(int I=(A);I<=(B);I++)
using namespace std;
typedef long long ll;
constexpr int N=2e5+9;
int c[6],d[6],T,n;
ll f[N];
int main(){
f[0]=1;
for(int i=1;i<=4;i++){
cin>>c[i];
for(int j=c[i];j<N;j++)
f[j]+=f[j-c[i]];
}
cin>>T;
while(T--){
for(int i=1;i<=4;i++)
cin>>d[i];
cin>>n;
ll ans=0;
for(int S=0;S<(1<<4);S++){
ll m=n,e=0;
rep(i,1,4)if((S>>(i-1))&1)
m-=(ll)(d[i]+1)*c[i],++e;
if(m<0)continue;
if(e&1)ans-=f[m];
else ans+=f[m];
}
cout<<ans<<'\n';
}
return 0;
}
P3813 [FJOI2017] 矩阵填数
显然答案为所有子矩阵的最大值小于等于 \(v_i\) 的方案数减去存在一个子矩阵的最大值小于 \(v_i\) 的方案数。
存在一个子矩阵的最大值小于 \(v_i\) 的方案数显然要用容斥来计算,直接 \(O(2^n)\) 枚举有哪些子矩阵的最大值小于等于 \(v_i\),否则子矩阵的最大值小于 \(v_i\),然后我们可以 \(O(nHW)\) 统计方案数,然后对横纵坐标离散化一下,可以优化到 \(O(n^3+n^2\log n)\) 的复杂度统计方案数(\(\log\) 是快速幂的复杂度)。
总时间复杂度为 \(O(2^n(n^3+n^2\log n))\)。
// qwq
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
constexpr ll mo=1e9+7;
ll ksm(ll x,ll y){
ll cur=1;
for(;y;y>>=1,x=x*x%mo)
if(y&1)cur=cur*x%mo;
return cur;
}
ll W,H,n,a[30][30],m,X[30],Y[30],cx,cy;
struct Add{ll x,y,l,r,v;}mat[30];
void solve(){
cin>>H>>W>>m>>n;
for(int i=1;i<=n;i++)
cin>>mat[i].x>>mat[i].l>>
mat[i].y>>mat[i].r>>mat[i].v;
cx=cy=0;
X[++cx]=1,Y[++cy]=1;
X[++cx]=H+1,Y[++cy]=W+1;
for(int i=1;i<=n;i++)
X[++cx]=mat[i].x,X[++cx]=mat[i].y+1,
Y[++cy]=mat[i].l,Y[++cy]=mat[i].r+1;
sort(X+1,X+cx+1),sort(Y+1,Y+cy+1);
cx=unique(X+1,X+cx+1)-X-1;
cy=unique(Y+1,Y+cy+1)-Y-1;
cx--,cy--;
ll ans=0;
for(int S=0;S<(1<<n);S++){
for(int i=1;i<=cx;i++)
for(int j=1;j<=cy;j++)
a[i][j]=m;
ll tmp=1,ci=0;
for(int s=1;s<=n;s++){
ll t=mat[s].v-((S>>(s-1))&1);ci+=((S>>(s-1))&1);
int l=lower_bound(X+1,X+cx+1,mat[s].x)-X,r=lower_bound(X+1,X+cx+1,mat[s].y+1)-X-1;
int x=lower_bound(Y+1,Y+cy+1,mat[s].l)-Y,y=lower_bound(Y+1,Y+cy+1,mat[s].r+1)-Y-1;
for(int i=l;i<=r;i++)for(int j=x;j<=y;j++)a[i][j]=min(a[i][j],t);
}
for(int i=1;i<=cx;i++)for(int j=1;j<=cy;j++){
ll l1=X[i+1]-X[i],l2=Y[j+1]-Y[j];
tmp=(tmp*ksm(a[i][j],l1*l2))%mo;
}
ans=(ans+((ci&1)?mo-1ll:1ll)*tmp%mo)%mo;
// cout<<tmp<<'\n';
}
cout<<ans<<'\n';
return;
}
int main(){
ll t; cin>>t;
while(t--)solve();
return 0;
}
求最大公约数为 k 的数对个数
设 \(i,j\le n\),求 \(\gcd(i,j)=k\) 的 \((i,j)\) 的对数。
设 \(f_i\) 表示 \(\gcd=i\) 的对数,则 \(f_i\) 等于 \(i\mid \gcd\) 的对数 \(\lfloor\frac{n}{i}\rfloor^2\) 减去 \(\gcd\) 为 \(i\) 的大于 \(2\) 的倍数的对数 \(f_j\)(\(i\mid j\),\(j\not= i\)),求出 \(1\) 至 \(n\) 的 \(f_i\) 的时间复杂度为 \(O(n\log n)\)。
for(int i=1;i<=n;i++){
f[i]=(n/i)*(n/i);
for(int j=i<<1;j<=n;j+=i)
f[i]-=f[j];
}
集合反演
这里仍然以几道例题为例。
P3349 [ZJOI2016] 小星星
题目要求的是树上节点到图中节点的映射方案数,且映射两两不同。设 \(F(u,rt,S)\) 表示节点 \(u\) 代表图中节点 \(rt\),整个子树代表图中的节点集合 \(S\) 的方案数,直接暴力枚举的复杂度为 \(O(n^34^n)\),优化子集枚举后的复杂度为 \(O(n^33^n)\),显然无法通过。
这个算法的瓶颈是枚举子集的 \(3^n\),考虑将 \(3^n\) 优化到 \(2^n\),我们发现这个 dp 有一个限制:每个点映射必须互不相同,这个要求使得我们必须枚举子集,考虑弱化或消去这个要求,设 \(G(S,i,rt)\) 表示子树 \(u\) 内的映射在集合 \(S\) 中,且 \(i\) 的映射为 \(rt\)、不需考虑映射两两不同的方案数,可以在 \(O(n^32^n)\) 的复杂度求出。
设 \(g(S)\) 表示 \(\sum_{rt}G(S,1,rt)\),即映射在集合 \(S\) 内不考虑映射两两不同的方案数,设 \(f(S)\) 表示映射恰好为集合 \(S\) 内不考虑映射两两不同的方案数,有:
直接子集反演可以得到 \(f(S)\),其中 \(f(U)\) 即为答案(\(U\) 为全集),因为映射为全集,所以每个点的映射必须两两不同。
// qwq
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
constexpr int N=20;
int lnk[N][N],n,m,p[N],pc;
ll f[N][N];
vector<int>e[N];
void dp(int u,int fa){
for(int i=1;i<=pc;i++)f[u][i]=1;
for(int v:e[u])if(v^fa){
dp(v,u);
for(int i=1;i<=pc;i++)if(f[u][i]){
ll tmp=0;
for(int j=1;j<=pc;j++)
if(lnk[p[i]][p[j]])
tmp+=f[v][j];
f[u][i]=f[u][i]*tmp;
}
}
}
ll sol(int S){
pc=0;
for(int i=1;i<=n;i++)
if((S>>(i-1))&1)
p[++pc]=i;
ll tmp=0; dp(1,0);
for(int i=1;i<=pc;i++)
tmp+=f[1][i];
return tmp*(((n-pc)&1)?-1:1);
}
int main(){
cin>>n>>m;
for(int i=1,x,y;i<=m;i++)
cin>>x>>y,lnk[x][y]=lnk[y][x]=1;
for(int i=1,x,y;i<n;i++)
cin>>x>>y,e[x].push_back(y),
e[y].push_back(x);
ll ans=0;
for(int i=1;i<(1<<n);i++)
ans+=sol(i);
cout<<ans<<'\n';
return 0;
}
P4336 [SHOI2016] 黑暗前的幻想乡
题意简述:有 \(n-1\) 个建筑公司,每个建筑公司可以修建一部分公路,求每个建筑公司恰好修建一条公路,且修建后图联通的方案数。\(n\le 17\)。
设 \(f(S)\) 表示恰好由集合 \(S\) 中的建筑公司修建完成的方案数(不考虑恰好修建一条公路这个条件,下同),\(g(S)\) 表示由集合 \(S\) 中的建筑公司修建完成的方案数(建筑公司可以不修建任何公路)。
显然 \(f(U)\) 即为答案(\(U\) 表示全集),而 \(g(S)=\sum_{T\in S}f(T)\),可以子集反演得到 \(f(S)\)。所以我们只需快速求出 \(g(S)\) 即可。显然 \(g(S)\) 等于对 \(S\) 中的建筑公司可以修建的公路的生成树个数,大力矩阵树定理统计即可,时间复杂度 \(O(2^nn^3)\)。
// qwq
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef vector<int> arr;
typedef vector<arr> Arr;
constexpr int N=67,mo=1e9+7;
inline int Det(Arr a,int n){
int ans=1,rev=0;
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
a[i][j]=(a[i][j]+mo)%mo;
for(int i=2;i<=n;i++){
for(int j=i+1;j<=n&&!a[i][i];j++)
if(a[j][i]){a[i].swap(a[j]),rev^=1;}
if(!a[i][i])return 0;
for(int j=i+1;j<=n;j++){
if(a[j][i]>a[i][i])a[i].swap(a[j]),rev^=1;
while(a[j][i]){
int d=a[i][i]/a[j][i];
for(int k=i;k<=n;k++)
a[i][k]=(a[i][k]-(ll)a[j][k]*d%mo+mo)%mo;
a[i].swap(a[j]),rev^=1;
}
}
ans=(ll)ans*a[i][i]%mo;
}
rev&&(ans=(mo-ans)%mo);
return ans;
}
int n;
vector<pair<int,int>>S[N];
int main(){
cin>>n;
for(int i=1,m;i<n;i++){
cin>>m;
for(int j=1,x,y;j<=m;j++)
cin>>x>>y,S[i].emplace_back(x,y);
}
int ans=0;
for(int i=1;i<(1<<n-1);i++){
Arr a(n+1,arr(n+1,0)); int t=0;
for(int j=0;j<n-1;j++)if(i>>j&1){
for(auto k:S[j+1])
a[k.first][k.second]--,a[k.second][k.first]--,
a[k.first][k.first]++,a[k.second][k.second]++;
++t;
}
t=n-1-t;
if(t&1)ans=(ans+mo-Det(a,n))%mo;
else ans=(ans+Det(a,n))%mo;
}
cout<<ans;
return 0;
}
二项式反演
P4859 已经没有什么好害怕的了
给两个数列 \(a\), \(b\), 要求 \(a_i,b_i\) 两两匹配,使得 \(a_i>b_i\) 的个数减去 \(a_i<b_i\) 的个数等于 \(k\),求总方案数。
先将 \(a,b\) 排序,定义 \(f_{i,j}\) 为前 \(i\) 个 \(a[i]\) 中有 \(j\) 个满足 \(a_i>b_i\) 、其他的还未匹配的方案数。
有 \(f_{i,j} = f_{i-1,j} + f_{i-1,j-1}(r_i-(j-1))\),其中 \(r_i\) 为 \(b\) 中小于 \(a_i\) 的个数。
设 \(h(i)=f_{n,i}(n-i)!\) 表示有 \(i\) 个是满足 \(a_i>b_i\) 的,其他的任意匹配的方案数,\(g(i)\) 表示恰好 \(i\) 个匹配是满足 \(a_i>b_i\) 的,其他均不满足 \(a_i>b_i\) 的方案数,显然 \(f(i)=\sum_{j=i}^n\binom{j}{i}g(j)\),二项式反演即可。
// qwq
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
constexpr int mo=1e9+9,N=2009;
int a[N],b[N],C[N][N],m,n,f[N][N],fac[N];
int main(){
cin>>n>>m,fac[0]=fac[1]=1;
if((n+m)&1){puts("0");return 0;}
m=n+m>>1;
if(m>n){puts("0");return 0;}
for(int i=1;i<=n;i++)cin>>a[i];
for(int i=1;i<=n;i++)cin>>b[i];
for(int i=1;i<=n;i++)fac[i]=(ll)fac[i-1]*i%mo;
sort(a+1,a+n+1),sort(b+1,b+n+1);
f[0][0]=1;
for(int i=1;i<=n;i++){
int num=lower_bound(b+1,b+n+1,a[i])-b-1;
for(int j=0;j<=n;j++)
f[i][j]=(f[i-1][j]+(j>0?(ll)(num-j+1)*f[i-1][j-1]%mo:0))%mo;
}
for(int i=0;i<=n;i++)
for(int j=C[i][0]=1;j<=i;j++)
C[i][j]=(C[i-1][j]+C[i-1][j-1])%mo;
int ans=0;
for(int i=m,j=1;i<=n;i++,j=mo-j)
(ans+=(ll)C[i][m]*j%mo*f[n][i]%mo*fac[n-i]%mo)%=mo;
cout<<ans;
return 0;
}
P4491 [HAOI2018] 染色
设 \(f_i\) 表示恰好有 \(i\) 个颜色的出现次数为 \(S\) 的方案数。因为题目要求恰好有 \(i\) 个颜色,所以直接做显然没有前途,考虑容斥,将恰好这一要求弱化,设 \(g_i\) 表示至少有 \(i\) 个元素的出现次数为 \(S\) 的方案数,有
考虑二项式反演:
把式子拆开,ntt 计算即可。
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
const int N = 2e7;
const ll mod = 1004535809;
const ll gen = 3;
ll rev[N], w[N];
ll my_pow(ll x, ll y){
ll res = 1;
for(;y;y>>=1,x=x*x%mod)
if(y&1)res=res*x%mod;
return res;
}
ll inv(ll x){return my_pow(x, mod-2);}
void NTT(ll *a, int Len, bool type){
for(int i = 0; i < Len; i++){
rev[i] = (rev[i>>1]>>1) + (i&1?Len>>1:0);
if(rev[i]>i)swap(a[rev[i]],a[i]);
}
for(int d=1; d<Len; d<<=1){
ll W = my_pow(gen, (mod-1)/(d*2));
if(type) W = inv(W);
w[0] = 1;
for(int i = 1; i < d; i++) w[i] = w[i-1] * W % mod;
for(int fir = 0; fir < Len; fir += d<<1){
int sec = fir + d;
for(int i = 0; i < d; i++){
ll a0 = a[fir+i], a1 = a[sec+i] * w[i] % mod;
a[fir+i] = (a0 + a1) % mod;
a[sec+i] = (a0 - a1 + mod) % mod;
}
}
}
if(type){
ll invlen = inv(Len);
for(int i=0; i<Len; i++)
a[i] = a[i] * invlen % mod;
}
}
ll fac[N], ifac[N], a[N], b[N], c[N], n, m, s, mm, ww[N];
void init_fac(ll tt){
fac[0] = ifac[0] = 1;
for(ll i = 1; i <= tt; i++)
fac[i] = 1ll * fac[i-1] * i % mod;
ifac[tt] = inv(fac[tt]);
for(ll i = tt-1; i >= 1; i--)
ifac[i] = 1ll * ifac[i+1] * (i+1) % mod;
}
ll fu1(int x){return (x&1)?-1:1;}
int main(){
cin >> n >> m >> s;
for(int i = 0; i <= m; i++) cin >> ww[i];
init_fac(max(n, m));
mm = min(m, n/s);
for(ll i = 0; i <= mm; i++){
a[i] = (fu1(i) * ifac[i] + mod) % mod;
b[i] = 1ll * fac[m] * ifac[m-i] % mod * my_pow(m-i, n-s*i) % mod *
fac[n] % mod * my_pow(ifac[s], i) % mod * ifac[n-s*i] % mod;
// printf("%lld %lld\n", a[i], b[i]);
}
ll Len = 1;
while(Len <= (mm*2)) Len<<=1;
reverse(b, b+mm+1);
NTT(a, Len, 0);
NTT(b, Len, 0);
for(ll i = 0; i < Len; i++)
a[i] = a[i] * b[i] % mod;
NTT(a, Len, 1);
reverse(a, a+mm+1);
ll ans = 0;
for(ll i = 0; i <= mm; i++)
ans = (ans + ifac[i] * a[i] % mod * ww[i] % mod) % mod;
cout << (ans + mod) % mod << endl;
return 0;
}
P5339 [TJOI2019] 唱、跳、rap和篮球
设 \(f_i\) 表示恰好有 \(i\) 个讨论蔡徐坤的方案数,\(g_i\) 有 \(i\) 个讨论蔡徐坤,其他的人放任自流的方案数,显然 \(g_i=\sum_{j}^{\min\{A,B,C,D,n\}}\tbinom{j}{i}f_j\),直接二项式反演即可。
有:
直接 \(ntt\) 计算即可,时间复杂度 \(O(n^2\log n)\)。
// qwq
#include <bits/stdc++.h>
#define inl inline
using namespace std;
using ll=long long;
constexpr ll mo=998244353,Gen=3,N=1e5+1;
inl ll sub(ll x,ll y){return x-=y,x<0?x+mo:x;}
inl ll suf(ll x,ll y){return x+=y,x>=mo?x-mo:x;}
#define inv(X) ksm((X),mo-2)
inl ll ksm(ll x,ll y){
ll cur=1;
for(;y;y>>=1,x=x*x%mo)
if(y&1)cur=cur*x%mo;
return cur;
}
ll w[N]; int rev[N];
inl void ntt(ll* a,int len,bool ok){
for(int i=0;i<len;i++)
if(rev[i]>i)swap(a[rev[i]],a[i]);
for(int d=1;d<len;d<<=1){
ll W=ksm(Gen,(mo-1)/(d<<1));if(ok)W=inv(W);
w[0]=1;for(int i=1;i<d;i++)w[i]=w[i-1]*W%mo;
for(int fi=0;fi<len;fi+=d<<1){
int se=fi+d;
for(int i=0;i<d;i++){
ll a0=a[fi+i],a1=a[se+i]*w[i]%mo;
a[fi+i]=suf(a0,a1);a[se+i]=sub(a0,a1);
}
}
}
if(ok){
ll iv=inv(len);
for(int i=0;i<len;i++)
a[i]=a[i]*iv%mo;
}
}
ll F[N],G[N];
void mul(ll* a,ll* b,ll n,ll m,ll* c){
int len=1;while(len<=(n+m+2))len<<=1;
for(int i=0;i<len;i++)
rev[i]=(rev[i>>1]>>1)|(i&1?len>>1:0);
for(int i=0;i<len;i++)F[i]=G[i]=0;
for(int i=0;i<=n;i++)F[i]=a[i];
for(int i=0;i<=m;i++)G[i]=b[i];
ntt(F,len,0), ntt(G,len,0);
for(int i=0;i<len;i++)F[i]=F[i]*G[i]%mo;
ntt(F,len,1);
for(int i=0;i<=n+m;i++)c[i]=F[i];
}
ll fac[N],ifac[N];
void init(int n){
fac[0]=ifac[0]=1;
for(int i=1;i<=n;i++)fac[i]=fac[i-1]*i%mo;
ifac[n]=inv(fac[n]);
for(int i=n-1;i;i--)ifac[i]=ifac[i+1]*(i+1)%mo;
}
ll f[4][N];
ll calc(ll A,ll B,ll C,ll D,ll n){
if(A+B+C+D<n)return 0;
for(int i=0;i<=A;i++)f[0][i]=ifac[i];
for(int i=0;i<=B;i++)f[1][i]=ifac[i];
for(int i=0;i<=C;i++)f[2][i]=ifac[i];
for(int i=0;i<=D;i++)f[3][i]=ifac[i];
mul(f[0],f[1],A,B,f[0]);
mul(f[0],f[2],A+B,C,f[0]);
mul(f[0],f[3],A+B+C,D,f[0]);
return f[0][n]*fac[n]%mo;
}
ll C(ll n,ll m){
if(n<0||m<0||n<m)return 0;
return fac[n]*ifac[m]%mo*ifac[n-m]%mo;
}
int main(){
ll n,a,b,c,d,mn,ans=0,nw=1;
cin>>n>>a>>b>>c>>d;
init(max({a,b,c,d,n}));
mn=min({a,b,c,d,n/4});
for(int i=0;i<=mn;i++,nw=sub(mo,nw))
ans=suf(ans,nw*C(n-3*i,i)%mo*calc(a-i,b-i,c-i,d-i,n-4*i)%mo);
cout<<ans<<'\n';
return 0;
}
后记
参考文献: