容斥原理笔记

例题

2020 中有多少个数能被 2,32,3 中至少一个数字整除。直接将 20/2+20/320/2+20/3 相加,答案错误。因为 66 会被计算两次。

我们要将多算的给减掉。显然当 xx2,32,3 的公倍数时,会被多算。所以再减去 20/(2×3)20/(2\times 3) 即可。

这即是容斥原理。

简单来说就是“奇加偶减”。(其实这么说也不好)


练习

题面

  • 给出一个长度为 nn 的序列 aa,问在区间 [L,R][L,R] 中有多少个数,至少能被 aa 中的一个数整除。

1lr109,n181\leq l\leq r\leq 10^9,n\leq 18.


根据上文,我们可以容斥。发现如果一个数是集合 xx 的公倍数,那么就会被多(少)计算。

通过 dfs 枚举每一种组合,套用上面的公式即可。


代码

#include<bits/stdc++.h>
using namespace std;
#define int long long
const int N = 19;
int n,k,a[N];

int ans1,ans2;
void dfs(int lvl,int cnt,int mul,int &ans,int n) {
	if(lvl>k||mul>n) return ;
	dfs(lvl+1,cnt,mul,ans,n);
	cnt++,mul=mul*a[lvl]/__gcd(a[lvl],mul);
	if(cnt&1) ans+=n/mul;
	else ans-=n/mul;
	dfs(lvl+1,cnt,mul,ans,n);
}
signed main() {
	int T;
	cin>>T;
	while(T--) {
		ans1 = ans2 = 0;
		int l,r;
		cin>>n>>l>>r;
		for(int i=1;i<=n;i++) {
			cin>>a[i];
		}
		k=n;
		dfs(1,0,1,ans1,l-1);
		dfs(1,0,1,ans2,r);
		cout<<ans2-ans1<<"\n";
	}
	return 0;
}

题面

如果每一次询问都使用多重背包,显然超时。那么不妨弱化问题。假如是没有货币数量的限制呢。

简单的完全背包即可。此时我们要思考 dpsdp_s 的意义。表示的是使用 ss 块钱的方案数。那么其中包含着一些不合法的,如何才能确定呢?

考虑从转移入手。在多重背包中,一次非法的转移是对于一种货币,面值为 cc,有 dd 个,但使用了 d+1d+1 个及以上的货币个数。如何书写的呢?

dps=dpsc×(d+1)\large dp_s=dp_{s-c\times (d+1)}

注意,dpsc×(d+1)\large dp_{s-c\times (d+1)} 已经包括了取 d+2,d+3d+2,d+3\dots 种的情况了。

对于一种非法转移,我们已经了解了,但是如果两种都非法呢?被多减去了。

从状态转移开始讨论。X1=dpsc1×(d1+1)X_1=\large dp_{s-{c_1\times (d_1+1)}}X2=dpsc2×(d2+1)X_2=\large dp_{s-{c_2\times (d_2+1)}} 是非法的。

但是 X3=dpsc1×(d1+1)c2×(d2+1)X_3=\large dp_{s-{c_1\times (d_1+1)}-{c_2\times (d_2+1)}} 也是非法的。而 X1,X2X_1,X_2 显然都包含 X3X_3。所以被多减去了。然后套用开头公式解题。


代码

#include<bits/stdc++.h>
using namespace std;
int c[5],d[5],dp[100005],n;
int main() {
	for(int i=1;i<=4;i++) cin>>c[i];
	cin>>n;
	dp[0]=1;
	for(int i=1;i<=4;i++) {
		for(int j=c[i];j<=100000;j++) {
			dp[j]+=dp[j-c[i]];
		}
	}
	while(n--) {
		int s;
		for(int j=1;j<=4;j++) {
			cin>>d[j];
		}cin>>s;
		int ans=0;
		for(int i=0;i<1<<4;i++) {//全 0 的情况会使 ans = dp_s。所以答案就是总共的方案数减去非法方案数。
			int p=0,cnt=0;
			for(int j=0;j<4;j++) {
				if((i>>j) & 1) cnt++, p+=c[j+1]*(d[j+1]+1);
			}
			if(s-p>=0){
				if(cnt & 1) ans-=dp[s-p];
				else ans+=dp[s-p];
			}
		}
		cout<<ans<<"\n";
	}
	return 0;
}

题面

  • 武林中流传了 mm 种武功,现在有 nn 个习武之人去学习一些武功,经过深思熟虑,每个人都选择了 55 个不同的武功,名字叫 a1a5a_1\dots a_5。 并学习成功,现在由于武林盟主失踪,群龙无首,这 nn 个人发生了叛乱,任意两个人 xxyy ,如果存在一种相同的武功他们俩都会,那么他们俩就是朋友,反之他们就是敌人,问 nn 个人中有多少对敌人。

1m105,1n5×104,1aim1 \leq m \leq 10^5,1 \leq n \leq 5\times 10^4,1\leq a_i \leq m.


我们先要找朋友,并进行计数。再用总数减去朋友,就是敌人。这种思想被称为间接计数。

考虑分类讨论,有些朋友有 11 种相同的武功,有些是 22\dots

现在有大小为 CC 集合 xx,他们都会武功 aa。那么有 C×(C1)/2C\times(C-1)/2 对朋友出现。

那如果有大小为 CC 集合 xx他们都会武功 a,ba,b 呢?

同时会两种武功的人肯定也是同时会一种武功的人,他们是有包含关系的。所以套文章开头公式即可,剩下的省略。对于统计是否同时会 151\dots 5 种武功,可以二进制枚举与哈希来做,具体就是枚举会的集合,并且哈希再遍历。

本文实现的是比较简单暴力的写法,常数特别大。枚举后用 vector 存到 map 里面遍历。

#pragma GCC optimize(3,"Ofast","inline")
#include<bits/stdc++.h>
#define rint register unsigned int
using namespace std;
map<vector<int>,pair<int,int> >mp;
signed main() {
        ios::sync_with_stdio(0); cin.tie(0), cout.tie(0);
        int n; cin>>n;
        for(rint i=1;i<=n;i++) {
                vector<int>a(5,0);
                for(rint j=0;j<5;j++) cin>>a[j];
                sort(a.begin(),a.end());
        vector<int>t(5,0);
                for(rint j=1;j<1<<5;j++) {
                        rint cnt=0;
                        for(rint k=0;k<5;k++) {
                                if((j>>k) & 1) t[k]=a[k],cnt++;
                                else t[k]=0;
                        }
                        sort(t.begin(),t.end());
                        mp[t].first++,mp[t].second = cnt;
                }
        }
        int ans = 0;
        for(auto it:mp) {
                auto j = it; int k = j.second.first;
                if(j.second.second & 1) ans+= (k-1)*k;
                else ans-= (k-1)*k;
        }
        cout<<((n-1)*n-ans)/2;
        return 0;
}
posted @ 2023-12-11 11:16  cjrqwq  阅读(25)  评论(0)    收藏  举报  来源