2023-12-08 21:34阅读: 19评论: 0推荐: 0

容斥学习笔记

日志

  • 2023/12/6 初步施工。
  • 2023/12/8 增加了一些例题、内容。修改了一些错误/模糊的表述。
  • 2023/12/10 增加了一些例题。
  • 还未完结。

容斥原理

我们有:

|Si|=i|Si|i<j|SiSj|+i<j<k|SiSjSk|

即:

|Si|=TU((1)|T|1|iTSi|)

还有:

|Si|=|U||Si|

即满足所有条件 Pi 的方案数等于所有的方案数减去不满足其中任意一条的方案数,并且不满足其中任意一条的方案数可以用容斥简单的计算,也就是说不满足某些条件的交集的大小可以方便的求出时,可以考虑容斥计算。

容斥原理求交集大小

|Si|=|U||Si|

一般情况下直接求 |Si| 的复杂度很高,而求 Si 和之间的交集大小很快时我们考虑用容斥原理计算。

这里以一些道例题为例。

P1450 [HAOI2008] 硬币购物

题意:共有 4 种硬币。面值分别为 c1,c2,c3,c4。某人去商店买东西,去了 n 次,对于每次购买,他带了 dii 种硬币,想购买 s 的价值的东西。请问每次有多少种付款方法。

题目要求的是满足 ciui=s 的方案数,且 uidi,设 uidi 表示第 i 条限制,Si 表示满足第 i 条限制的方案集合,则答案为:

|Si|=|U||Si|

其中 Si 表示不满足i 条限制的方案集合,也就是 ui>di,可以容斥计算,显然 SiSjSk 的情形可以用组合数统计方案数,于是我们轻松的完成了问题。

// 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] 矩阵填数

显然答案为所有子矩阵的最大值小于等于 vi 的方案数减去存在一个子矩阵的最大值小于 vi 的方案数。

存在一个子矩阵的最大值小于 vi 的方案数显然要用容斥来计算,直接 O(2n) 枚举有哪些子矩阵的最大值小于等于 vi,否则子矩阵的最大值小于 vi,然后我们可以 O(nHW) 统计方案数,然后对横纵坐标离散化一下,可以优化到 O(n3+n2logn) 的复杂度统计方案数(log 是快速幂的复杂度)。

总时间复杂度为 O(2n(n3+n2logn))

// 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,jn,求 gcd(i,j)=k(i,j) 的对数。

fi 表示 gcd=i 的对数,则 fi 等于 igcd 的对数 ni2 减去 gcdi 的大于 2 的倍数的对数 fjijji),求出 1nfi 的时间复杂度为 O(nlogn)

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];
}

集合反演

f(S)=TSg(T)g(S)=TS(1)|S||T|f(T)

这里仍然以几道例题为例。

P3349 [ZJOI2016] 小星星

题目要求的是树上节点到图中节点的映射方案数,且映射两两不同。设 F(u,rt,S) 表示节点 u 代表图中节点 rt,整个子树代表图中的节点集合 S 的方案数,直接暴力枚举的复杂度为 O(n34n),优化子集枚举后的复杂度为 O(n33n),显然无法通过。

这个算法的瓶颈是枚举子集的 3n,考虑将 3n 优化到 2n,我们发现这个 dp 有一个限制:每个点映射必须互不相同,这个要求使得我们必须枚举子集,考虑弱化或消去这个要求,设 G(S,i,rt) 表示子树 u 内的映射在集合 S 中,且 i 的映射为 rt、不需考虑映射两两不同的方案数,可以在 O(n32n) 的复杂度求出。

g(S) 表示 rtG(S,1,rt),即映射在集合 S 内不考虑映射两两不同的方案数,设 f(S) 表示映射恰好为集合 S 内不考虑映射两两不同的方案数,有:

g(S)=TSf(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] 黑暗前的幻想乡

题意简述:有 n1 个建筑公司,每个建筑公司可以修建一部分公路,求每个建筑公司恰好修建一条公路,且修建后图联通的方案数。n17

f(S) 表示恰好由集合 S 中的建筑公司修建完成的方案数(不考虑恰好修建一条公路这个条件,下同),g(S) 表示由集合 S 中的建筑公司修建完成的方案数(建筑公司可以不修建任何公路)。

显然 f(U) 即为答案(U 表示全集),而 g(S)=TSf(T),可以子集反演得到 f(S)。所以我们只需快速求出 g(S) 即可。显然 g(S) 等于对 S 中的建筑公司可以修建的公路的生成树个数,大力矩阵树定理统计即可,时间复杂度 O(2nn3)

// 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;
}

二项式反演

g(n)=i=0n(ni)f(i)f(n)=i=0n(ni)(1)nig(i)

gi=j=in(ji)fjfi=j=in(ji)(1)jigj

P4859 已经没有什么好害怕的了

给两个数列 a, b, 要求 ai,bi 两两匹配,使得 ai>bi 的个数减去 ai<bi 的个数等于 k,求总方案数。

先将 a,b 排序,定义 fi,j 为前 ia[i] 中有 j 个满足 ai>bi 、其他的还未匹配的方案数。

fi,j=fi1,j+fi1,j1(ri(j1)),其中 rib 中小于 ai 的个数。

h(i)=fn,i(ni)! 表示有 i 个是满足 ai>bi 的,其他的任意匹配的方案数,g(i) 表示恰好 i 个匹配是满足 ai>bi 的,其他均不满足 ai>bi 的方案数,显然 f(i)=j=in(ji)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] 染色

fi 表示恰好有 i 个颜色的出现次数为 S 的方案数。因为题目要求恰好有 i 个颜色,所以直接做显然没有前途,考虑容斥,将恰好这一要求弱化,设 gi 表示至少有 i 个元素的出现次数为 S 的方案数,有

gi=(mi)(niS)(iS)!(S!)i=(mi)n!(S!)i(niS)!

考虑二项式反演:

fi=j=in(ji)(1)jigj

把式子拆开,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和篮球

fi 表示恰好有 i 个讨论蔡徐坤的方案数,gii 个讨论蔡徐坤,其他的人放任自流的方案数,显然 gi=jmin{A,B,C,D,n}(ji)fj,直接二项式反演即可。

有:

gi=(n3ii)a=0Aib=0Bic=0Cid=0Di[a+b+c+d=n3i](n3i)!a!b!c!d!

直接 ntt 计算即可,时间复杂度 O(n2logn)

// 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;
}

后记

参考文献:

本文作者:fzrcy

本文链接:https://www.cnblogs.com/fzrcy/p/17880681.html

版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。

posted @   fzrcy  阅读(19)  评论(0编辑  收藏  举报
点击右上角即可分享
微信分享提示
💬
评论
📌
收藏
💗
关注
👍
推荐
🚀
回顶
收起
  1. 1 404 not found REOL
404 not found - REOL
00:00 / 00:00
An audio error has occurred.