// 侧边栏目录 // https://blog-static.cnblogs.com/files/douzujun/marvin.nav.my1502.css //目录导航 //生成目录索引列表

P1543 [POI 2004] SZP

题目背景

一班班花 \(\color{red}{\texttt{y}}\color{black}{\texttt{hb}}\) 十分可爱。

题目描述

班花 \(\color{red}{\texttt{y}}\color{black}{\texttt{hb}}\) 作为某日的值日班长,在自习课上管理着 \(n\) 名同学。除了她以外每一名同学都监视着另一名同学。现在班花 \(\color{red}{\texttt{y}}\color{black}{\texttt{hb}}\) 需要选择尽量多的同学去搬卷子和答题卡,且使得对于这些同学中的每一名同学,至少有一位监视她的同学没有被选中。问班花 \(\color{red}{\texttt{y}}\color{black}{\texttt{hb}}\) 最多可以选择多少同学。

由于班花 \(\color{red}{\texttt{y}}\color{black}{\texttt{hb}}\) 太可爱了,所以没有人监视她,也可以认为她的学号是 \(0\)

如果一个人没有被监视,那么她就不能被选择。

输入格式

第一行只有一个整数,\(n\) 代表同学的数量。同学的学号从 \(1\)\(n\) 编号。

接下来 \(n\) 行每行一个整数 \(a_k\) 表示同学 \(k\) 将要监视同学 \(a_k\)\(1 \le k \le n\)\(1 \le a_k \le n\)\(a_k \ne k\)

输出格式

一个数,最多能有多少同学参加入这个任务。

输入输出样例 #1

输入 #1

6
2
3
1
3
6
5

输出 #1

3

说明/提示

对于 \(100\%\) 的数据,\(1\le k,a_k\le n\le 10^6\)

基本思路

这题理应是一个基环树 \(DP\) 的,奈何内存给太小只有 \(32MB\)

那么就换一种解法吧,先提取题目信息,如果一个人没有人留在教室监视的话就不能去搬书,那么可以转化为一个点如果没有入度的话就不可以选,还有每个点只有一条出边。

我第一反应原本是二分图或者网络流啥的,但这个数据范围即便对 \(Dinic\) 也有点极限,而且集合内部存在连边,网络流建模也没法建。

\(DP\) 肯定是没指望了,知道也写不出来,那就尝试贪心吧。

一开始就没有入度的点是肯定不可以选的,但它指向的儿子一定是可以选的,因为它的父亲不可能选。那么就得到一种贪心思想,就是一个点如果不选的话,那么一定选上它的儿子。

我找了半天找不到反例,那就感性理解一下,如果我们不选一个点,又不选它的儿子节点,那它的孙子节点选上是否会有更大贡献?如果都是在环上那肯定好说,所以我们就讨论在子树上的,好像除了把选的序列向下推了一点,那只会更差,并不会更优。

啊,注意一下,这里的 \(queue\) 并不代表我们选哪个或者不选哪个,而是储存哪些点已经被处理过了。

那么贡献一下第一个版本的代码吧

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 1e6+10;
int n, a[N], degree[N], tot, res, ans;
int color[N];
queue<int>q;

int main() {
	ios::sync_with_stdio(false);
	cin >> n;
	for (int i = 1; i <= n; i++) {
		cin >> a[i];
		degree[a[i]]++;
	}
	tot = n;
	for (int i = 1; i <= n; i++) {
		if (degree[i] == 0) {
			q.push(i);
			color[i] = -1;
			tot--;
		}
	}
	while (!q.empty()) {
		int u = q.front();
		q.pop();
		degree[a[u]]--;
		if (color[u] == -1) {
			if (color[a[u]] == 0)
				tot--;
			color[a[u]] = 1;
			q.push(a[u]);
		} else if (degree[a[u]] == 0 && color[a[u]] == 0) {
			q.push(a[u]);
			tot--;
			color[a[u]] = -1;
		}
	}
	res = 1;
	while (tot) {//总数没清零,存在(裸)环
		for (int i = res; i <= n; i++) {
			if (color[i] == 0) {
				q.push(i);//随便在环中取出一个断点
				color[i] = -1;
				res = i;//小小循环优化
				tot--;
				break;
			}
		}
		while (!q.empty()) {//实际是从上面那一段Ctrl+C下来的
			int u = q.front();
			q.pop();
			degree[a[u]]--;
			if (color[u] == -1) {//这个节点不选
				if (color[a[u]] == 0)
					tot--;
				color[a[u]] = 1;
				q.push(a[u]);
			} else if (degree[a[u]] == 0 && color[a[u]] == 0) {
				q.push(a[u]);
				tot--;
				color[a[u]] = -1;
			}
		}
	}
	for (int i = 1; i <= n; i++)
		if (color[i] == 1)
			ans++;
	cout << ans;
//	8
//	2 4 4 7 2 5 5 7
	return 0;
}

这玩意只有 \(70pts\),是寻找独立的环那段出错了,我也不知道为什么,结论是越复杂越容易错。

那么改一下是第二个代码

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 1e6+10;
int n, a[N], degree[N], tot, res, ans;
int color[N];
queue<int>q;

int main() {
	ios::sync_with_stdio(false);
	cin >> n;
	for (int i = 1; i <= n; i++) {
		cin >> a[i];
		degree[a[i]]++;
	}
	tot = n;
	for (int i = 1; i <= n; i++) {
		if (degree[i] == 0) {
			q.push(i);
			color[i] = -1;
		}
	}
	while (!q.empty()) {
		int u = q.front();
		q.pop();
		degree[a[u]]--;
		if (color[u] == -1) {
			color[a[u]] = 1;
			q.push(a[u]);
		} else if (degree[a[u]] == 0 && color[a[u]] == 0) {
			q.push(a[u]);
			color[a[u]] = -1;
		}
	}
	for (int i = 1; i <= n; i++) {
		if (color[i] == 0) {
			assert(degree[i]);
			color[i] = -1;
			color[a[i]] = 1;
			for (int j = a[i]; j != i; j = a[j]) {
				if (color[a[j]] == 0) {//for循环整个取出
					assert(color[j] != 0);
					if (color[j] == 1)
						color[a[j]] = -1;
					else
						color[a[j]] = 1;
				}
			}
		}
	}
	for (int i = 1; i <= n; i++)
		if (color[i] == 1)
			ans++;
	cout << ans;
	return 0;
}

这次改用 \(for\) 来直接提环,貌似更加可靠了一点,但只有 \(90pts\),怎么回事呢?

看这里:

if (color[u] == -1) {
	color[a[u]] = 1;
	q.push(a[u]);
} else if (degree[a[u]] == 0 && color[a[u]] == 0) {
	q.push(a[u]);
	color[a[u]] = -1;
}

注意 \(color\) 数组是覆盖的方式修改的

这肯定出点啥毛病。

若有多个节点指向当前节点,而这些节点都没被选,是不是说有一个节点会被重复入队?

那么 \(AC\) 代码出炉了:

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 1e6+10;
int n, a[N], degree[N], ans;
int color[N];
queue<int>q;

int main() {
//	freopen("gen4.in", "r", stdin);
	ios::sync_with_stdio(false);
	cin >> n;
	for (int i = 1; i <= n; i++) {
		cin >> a[i];
		degree[a[i]]++;//统计入度
	}
	for (int i = 1; i <= n; i++) {
		if (degree[i] == 0) {
			q.push(i);
			color[i] = -1;//肯定不能选
		}
	}
	while (!q.empty()) {
		int u = q.front();
		q.pop();
		degree[a[u]]--;
		if (color[u] == -1) {
			if (color[a[u]] == 0)
				q.push(a[u]);
			color[a[u]] = 1;
		} else if (degree[a[u]] == 0 && color[a[u]] == 0) {
			q.push(a[u]);
			color[a[u]] = -1;
		}
	}
	for (int i = 1; i <= n; i++) {
		if (color[i] == 0) {
			color[i] = -1;
			int j = i;
			while (color[a[j]] == 0) {//更稳定的方法
				if (color[j] == 1)
					color[a[j]] = -1;
				else
					color[a[j]] = 1;
				j = a[j];
			}
		}
	}
	for (int i = 1; i <= n; i++)
		if (color[i] == 1)
			ans++;
	cout << ans;
//	8
//	2 4 4 7 2 5 5 7
	return 0;
}
posted @ 2025-08-20 17:08  SSL_LMZ  阅读(4)  评论(0)    收藏  举报