6.9~6.15

容斥原理

集合 \(S\) 的子集 \(A_1\) 有性质 \(p_1\)\(A_2\) 有性质 \(P_2\)\(\dots\)\(A_n\) 有性质 \(P_n\)
那么,集合 \(S\) 中具有性质 \(P_1,P_2,\dots,P_n\) 的集合个数为

\[\Big|\bigcap_{i=1}^nA_i \Big| = \sum_{i=1}^n\left| A_i\right| - \sum_{1 \leq i \leq j \leq n} \left| A_i \cap A_j \right| + \sum_{1 \leq i \leq j \leq k \leq n} \left| A_i \cap A_j \cap A_k \right| - \dots + (-1)^{n+1} \left| \cap_{i=1}^n A_i \right| \]

那么不具有性质 \(P_1,P_2,\dots,P_n\) 的集合个数呢?很明显就是

\[\Big| S \Big| - \Big| \bigcap_{i=1}^n A_i \Big| \]


来个例子加深理解

问题:在100人中,60人喜欢咖啡,40人喜欢茶,20人两者都喜欢。求至少喜欢一种饮料的人数?一种饮料都不喜欢的人数?

解答:
\(A\) 为喜欢咖啡的集合,\(B\) 为喜欢茶的集合。
容斥原理给出:

\[ |A \cup B| = |A| + |B| - |A \cap B| = 60 + 40 - 20 = 80. \]

因此,80人喜欢至少一种饮料。

\[|\complement_S A| = |S| - |A \cup B| = 100 - 80 = 20. \]

因此,20人一种饮料都不喜欢


以下是一道例题(2024CCPC新疆E题):


题目

Bob现在具有一个长为 \(n\) 的序列 \(a_1,a_2,a_3,\cdots,a_n\)

一共有 \(q\) 次询问,每次给定一个数 \(x\)

询问在 \([1,x]\) 中有多少个 正整数 满足它不是任何 \(a_i\) 的倍数。

输出满足这样要求的正整数数量。

输入

第一行一个正整数 \(n\),表示数组的长度。

第二行 \(n\) 个正整数,第 \(i\) 个正整数表示 \(a_i\)

第三行一个正整数 \(q\),表示共有\(q\)个询问。

接下来的 \(q\) 行中,第 \(i\) 行一个正整数 \(x\) 表示当次询问。

其中保证\(n \leq 10,q \leq 10^4, a_i, x \leq10^9\).


子集共有 \(2^n\) 个,用二进制法把每个子集给枚举出来,奇数个子集相加,偶数个子集减去,本题属于"不是、不具有"的情况,因此满足“不具有”的情况就是总集减去子集数

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

int n,q,a[15];

inline ull lcm(int x,int y){
	return x/__gcd(x,y)*y;
}

int main(void){
	ios::sync_with_stdio(false);
	cin.tie(0);cout.tie(0);
	cin >> n;
	for(int i=1;i<=n;i++){
		cin >> a[i];
	}
	cin >> q;
	while(q--){
		ull x, ans = 0; 
		cin >> x;
		for(int i = 1; i < (1 << n); i++){
			ull sum = 0, glcm = 1;
			for(int j=0;j<n;j++){
				if((i>>j) & 1){
					sum++;
					glcm = lcm(glcm,a[j+1]);
				}
			}
			if(sum & 1) ans += x/glcm;
			else ans -= x/glcm;
		}
		cout << x - ans << '\n';
	}
	return 0;
}

CCPC新疆重现赛

B


题目描述

在一款即时通讯软件里,有一种功能叫做群组。

现在,群组中有\(n\)个成员。每个成员都是一个模仿者,会模仿指定对象的头像。允许模仿自己。

模仿的具体含义是:每经过一个时刻,若成员A模仿对象为成员B,那么成员A就会更换为成员B的头像。

现在给定所有成员的模仿对象。初始时,每个成员都有本质不同的原始头像。

你的任务是计算经过\(10^{100}\)个时刻之后,群组里还存在几种本质不同的头像。

输入描述

第一行一个整数\(n\)。表示群组里一共有几个成员。编号\(1 \sim n\)

接下来一行\(n\)个整数\(a_i\),描述每个成员\(i\)的模仿对象为\(a_i\)

其中,\(1 \le n \le 10^5, 1 \le a_i \le n\).


题目的 $ 10^{100} $ 意味着正无穷,也就是经过一定次数后,"本质"的种类数会固定下来,不再发生变动。
造了几个样例,并且在纸上画出图论关系图后,就有思路了。
思路是用拓扑排序找环,只要是环上(包括自环)的点,就不会发生退化;而所有的不在环上的点,都会发生退化。

#include <bits/stdc++.h>
using namespace std;
const int maxn = 1e5+5;

int n,a[maxn],s[maxn];

void init(){
	for(int i=1;i<=n;i++) s[i] = i;
}

int finds(int x){
	if(x!=s[x]) s[x] = finds(s[x]);
	return s[x];
}

void unions(int x,int y){
	x = finds(x);
	y = finds(y);
	if(x != y) s[x] = s[y];
}

int main(void){
	ios::sync_with_stdio(false);
	cin.tie(0);cout.tie(0);
	cin >> n;
	init();
	//for(int i=1;i<=n;i++) cout << s[i] << ' ';
	//cout << '\n';
	for(int i=1;i<=n;i++){
		cin >> a[i];
		unions(i,a[i]); 
	}
	for(int i=1;i<=n;i++) cout << s[i] << ' ';
	return 0;
}

C


题目

现在给定你一个字符串。小Y有一个幸运字符,他认为含有这个幸运字符的字符串是好的。

现在给你一个简单的问题。小Y会有\(q\)次询问,每次询问会有必须包含的位置要求,你的任务是在包含这个位置的前提下,选出一段最短的连续子串,使得这个子串中含有小Y所指定的幸运字符。

输入

第一行两个整数\(n, q (1 \le n, q \le 10^5)\),表示字符串长度为\(n\),一共有\(q\)次询问。另有一个字符\(ch\),表示连续子串中需要包含的幸运字符\(ch\)。该字符一定是一个小写英文字母。

第二行一个长度为\(n\)的字符串。字符串中的位置从1开始计数。

接下来\(q\)行,每行一个正整数\(pos\),保证\(1 \le pos \le n\),你的任务是对每次询问的\(pos\)给出最短的连续子串的长度,使得它含有小Y所指定的幸运字符。若不存在满足要求的选取方法输出-1。


先预处理一遍,将每个s[i]初始化为\(-1\),然后依次处理每个s[i]:向左找最近的幸运字符,并记录路径长度;向右找最近的幸运字符,并记录路径长度;取左右长度的最小值存进a[i]里;查询时直接查表即可

#include <bits/stdc++.h>
using namespace std;
const int maxn = 1e5+5;

int n,q,a[maxn];
char ch;
string s;

void init(){
	for(int i=1;i<=n;i++) a[i] = -1;
	for(int i=1;i<=n;i++){
		int l = i, r = i, cnt = 1;
		bool flag = 0;
		while(1){
			if(l == 0) break;
			if(s[l] == ch) {
				flag = 1;
				break;
			}
			l--,cnt++;
		}
		//cout << "cnt: " <<  cnt << '\n';
		if(flag) a[i] = cnt;
		cnt = 1;
		flag = 0;
		while(1){
			if(r == n+1) break;
			if(s[r] == ch){
				flag = 1;
				break;
			}
			cnt++;
			r++;
		}
		if(flag && a[i]==-1) a[i] = cnt; 
		else if(flag) a[i] = min(a[i],cnt);
	}
}

int main(void){
	ios::sync_with_stdio(false);
	cin.tie(0);cout.tie(0);
	cin >> n >> q >> ch;
	cin >> s; s = ' ' + s;
	init();
	for(int i=1;i<=q;i++){
		int t; cin >> t;
		cout << a[t] << '\n';
	}
	//for(int i=1;i<=n;i++) cout << a[i] << ' ';
	return 0;
}

D


题目

Alice现在通过随机数生成了一个长度为\(n\)的序列。

定义一个序列是好的,当且仅当:
- 对于这个序列,任意截取序列中前若干个数字,其和均非负。

例如:序列[1, 2, -3]是好的,序列[5, -3, -3, 9]是不好的。这是因为对于5 - 3 - 3 = -1 < 0。

现在Alice希望把这个序列划分成若干个连续的子序列,使得划分后的每个子序列都是好的。有些情况下不止一种划分方案,Alice希望你给出最多的序列划分方案。

输入

第一行一个正整数\(n\)表示序列的长度\(n\)。其中保证\(1 \le n \le 2 * 10^5\)

接下来一行\(n\)个整数\(a_1, a_2, ..., a_n\),表示这个序列。其中保证\(-10^4 \le a_i \le 10^4\)


主要是贪心,
很明显的是,负数不能在开头,因为这样会使得开头的一段直接为负;结合贪心,要让尽可能多的非负数独立成段。
所以思路就是,从后向前走一遍序列,如果遇到非负数,就独立成段,段数++;如果遇到负数,就出现了一个“含负数段”,从这个负数开始,一直像前走,直到sum为非负时停止。

#include <bits/stdc++.h>
using namespace std;
const int maxn = 2e5+5;

int n,a[maxn],cnt = 0;

int main(void){
	ios::sync_with_stdio(false);
	cin.tie(0);cout.tie(0);
	cin >> n;
	for(int i=n;i>=1;i--) cin >> a[i];
	for(int i=1;i<=n;i++){
		if(a[i] >= 0) cnt++;
		else{
			int sum = a[i];
			while(sum < 0){
				if(++i > n) break;
				sum += a[i];
			}
			if(sum >= 0) cnt++;
		}
	}
	cout << cnt << '\n';
	return 0;
}

F


题目

Cindy最近沉迷某种六个字的游戏。

这个游戏里有一个场景是选择房间,每个房间会有不同的事件。最后一个房间会面对游戏Boss。

Cindy设计了一个比原版游戏简单一些的场景:

现在有一行\(n\)个房间。每个房间里有\(a_i\)份奖励。一些房间是普通房间,获取该房间的奖励对其他房间无任何影响。另一些房间是特殊房间,获取该房间内的奖励会导致编号满足\(i - a_i \le x \le i + a_i\)\(x\)号房间内的奖励清零。当然,对于这两类房间,你都可以不获取奖励,此时对其他房间无任何影响。

你的任务是计算最多能在按顺序从\(1 \sim n\)走完这\(n\)个房间后可以获取的最多奖励数量。

输入

第一行输入一个整数 \(n(1 \leq n \leq 5 \times 10^5)\) 表示一共有几个房间。

第二行输入 \(n\) 个整数 \(a_i(1 \leq a_i \leq 10^9)\) 表示每个房间内的奖励数量。

第三行输入 \(n\) 个整数 \(b_i(0 \leq b_i \leq 1)\) 表示每个房间属性, 1 表示普通房间, 0 表示特殊房间。


六字游戏?不会是崩坏星穹铁道吧(bushi)

戴南米克破管明,简称DP
定义 \(dp[i]\) 为到第 \(i\) 个房间可以获得的最大奖励,从后往前dp
走到第 \(i\) 个房间时,先 \(dp[i] = dp[i+1]\),获取前一节点的最优状态;状态转移方程:
对于普通房间:

\[dp[i] = dp[i+1] + a[i] \]

对于特殊房间:

\[dp[i] = max(dp[i],dp[min(n+1,i+a[i]+1)]+a[i]) \]

之所以取 \(min\) ,是担心 \(i+a[i]+1\) 会越界
全程用ans记录最大值即可

#include <bits/stdc++.h>
#define int long long
using namespace std;
const int maxn = 5e5+5;

int n,a[maxn],dp[maxn],ans = 0;
bool b[maxn];

signed main(){
	ios::sync_with_stdio(false);
	cin.tie(0);cout.tie(0);
	cin >> n;
	for(int i=1;i<=n;i++) cin >> a[i];
	for(int i=1;i<=n;i++) cin >> b[i];
	for(int i=n;i>=1;i--){
		dp[i] = dp[i+1];
		if(b[i]) dp[i] = dp[i+1] + a[i];
		else{
			int edge = min(n+1,i+a[i]+1);
			dp[i] = max(dp[i],dp[edge]+a[i]);
		}
		ans = max(ans,dp[i]);
	}
	cout << ans << '\n';
	return 0;
}

J


题目

在平面上有若干个点,另有若干个圆。

给出所有点和所有圆,对于每个圆,问有多少点位于该圆内。

注:在圆形边缘上的点也算在内。

输入

第一行两个整数\(n, m (1 \le n, m \le 2 * 10^5)\),表示一共有\(n\)个圆,\(m\)个点。

接下来\(n\)行,每行三个正整数\(O_x, O_y, r (1 \le O_x, O_y, r \le 20)\),表示一个圆心在\((O_x, O_y)\)处,半径为\(r\)的圆。

接下来\(m\)行,每行两个正整数\(x, y (1 \le x, y \le 20)\)表示一个点。


第一思路就是暴力
考虑最坏的情况,点的坐标都在20以内,枚举每个点,有 \(20×20=400\) 个坐标点, \(2×10^5\) 个圆,那么就一共要枚举 \(400×2×10^5=8×10^7\) 次,接近 \(10^8\) 了,虽然不太保险,但是我们还是很信任牛客的数据和机器不会针对我们的ovo
提交后成功过了

#include <bits/stdc++.h>
using namespace std;
const int maxn =2e5+5;

class circle{
	public:
		int x,y,r;
}cir[maxn];

int n,m,room[25][25];

int main(void){
	ios::sync_with_stdio(false);
	cin.tie(0);cout.tie(0);
	cin >> n >> m;
	for(int i=1;i<=n;i++){
		int x,y,r; cin >> x >> y >> r;
		cir[i] = {x,y,r};
	}
	for(int i=1;i<=m;i++){
		int x,y; cin >> x >> y;
		room[x][y]++;
	}
	for(int i=1;i<=n;i++){
		int cnt = 0;
		for(int j=1;j<=20;j++){
			for(int k=1;k<=20;k++){
				if(room[j][k] && (cir[i].x-j)*(cir[i].x-j) + (cir[i].y-k)*(cir[i].y-k) <= cir[i].r*cir[i].r){
					cnt += room[j][k];
				}
			}
		}
		cout << cnt << '\n';
	}
	return 0;
}

E


题目

Bob现在具有一个长为 \(n\) 的序列 \(a_1,a_2,a_3,\cdots,a_n\)

一共有 \(q\) 次询问,每次给定一个数 \(x\)

询问在 \([1,x]\) 中有多少个 正整数 满足它不是任何 \(a_i\) 的倍数。

输出满足这样要求的正整数数量。

输入

第一行一个正整数 \(n\),表示数组的长度。

第二行 \(n\) 个正整数,第 \(i\) 个正整数表示 \(a_i\)

第三行一个正整数 \(q\),表示共有\(q\)个询问。

接下来的 \(q\) 行中,第 \(i\) 行一个正整数 \(x\) 表示当次询问。

其中保证\(n \leq 10,q \leq 10^4, a_i, x \leq10^9\).


容斥原理,赛时还不会,赛后学了就把这题开出来了
子集共有 \(2^n\) 个,用二进制法把每个子集给枚举出来,奇数个子集相加,偶数个子集减去,本题属于"不是、不具有"的情况,因此满足“不具有”的情况就是总集减去子集数

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

int n,q,a[15];

inline ull lcm(int x,int y){
	return x/__gcd(x,y)*y;
}

int main(void){
	ios::sync_with_stdio(false);
	cin.tie(0);cout.tie(0);
	cin >> n;
	for(int i=1;i<=n;i++){
		cin >> a[i];
	}
	cin >> q;
	while(q--){
		ull x, ans = 0; 
		cin >> x;
		for(int i = 1; i < (1 << n); i++){
			ull sum = 0, glcm = 1;
			for(int j=0;j<n;j++){
				if((i>>j) & 1){
					sum++;
					glcm = lcm(glcm,a[j+1]);
				}
			}
			if(sum & 1) ans += x/glcm;
			else ans -= x/glcm;
		}
		cout << x - ans << '\n';
	}
	return 0;
}
posted @ 2025-06-14 12:31  HLAIA  阅读(23)  评论(0)    收藏  举报