图论做题记录(二)(2025.4.2)

P9257 [PA 2022] Mędrcy

一道非常有意思的逻辑 + 图论题,写了我一天才写出来。

其实这道题目应该是《迷人的逻辑题》中“花心丈夫”这道题目的加强版,如果做了原题可能会好想一点。

我们先考虑一个贤者在什么情况下会离开。那一定是目前的情况无法用这个贤者已知的信息推出,那么一定是有这个贤者不知道的咒语,于是该贤者就会离开。

现在我们从小数据开始推:

  • 第一天,什么咒语都不知道的贤者知道了居然有人知道咒语,自己真是太菜了,于是就退了。那么第一天的退出规则就是掌握的咒语为空集的人退出。

  • 第二天,由于每个贤者都知道掌握了自己知道的咒语的其他贤者是谁,那么就会判断其他每个贤者是否会退出。当一个贤者 \(a\) 掌握的咒语的其它掌握者都不包括贤者 \(b\),那么 \(a\) 就会认为 \(b\) 会退出,如果 \(b\) 没有退出,那么 \(a\) 就知道 \(b\) 还掌握了他不知道的咒语,于是 \(a\) 就会退出。对于 \(b\) 也是一样的,于是 \(a\)\(b\) 都会退出。那么第二天的退出规则就是两个掌握咒语的交集为空的贤者会退出。

  • 第三天,对于一个贤者 \(a\),如果它掌握的咒语的其它掌握者中存在贤者 \(b\) 和贤者 \(c\) 掌握的咒语的交集为空,那么 \(a\) 就会认为 \(b\)\(c\) 会退出。如果 \(b\)\(c\) 没有退出,那么 \(a\) 就知道 \(b\)\(c\) 还共同掌握了他不知道的咒语,于是 \(a\) 就会退出。对于 \(b\)\(c\) 也是一样,于是 \(a, b, c\) 都会退出。那么第三天的退出规则就是三个掌握咒语的交集为空的贤者会退出。

  • 推广到第 \(n\) 天,对于一个贤者 \(a\),如果它掌握的咒语的其它掌握者中存在贤者 \(b_1, b_2, \dots, b_{n - 1}\) 掌握的咒语的交集为空,那么 \(a\) 就会认为\(b_1, b_2, \dots, b_{n - 1}\) 会退出。如果 \(b_1, b_2, \dots, b_{n - 1}\) 没有退出,那么 \(a\) 就知道 \(b_1, b_2, \dots, b_{n - 1}\) 还共同掌握了他不知道的咒语,于是 \(a\) 就会退出。对于 \(b_1, b_2, \dots, b_{n - 1}\) 也是一样,于是 \(a, b_1, b_2, \dots, b_{n - 1}\) 都会退出。那么第 \(n\) 天的退出规则就是 \(n\) 个掌握咒语的交集为空的贤者会退出。

由于题目要找到第一次有贤者退出的日期,那么我们就需要找到最小的 \(n\) 使得存在 \(n\) 个贤者,它们掌握的咒语的交集为空集。由于可能会存在多组这样的 \(n\) 个贤者,我们需要求出它们的并集

转化成数学语言,就是找到一组 \(b_1, b_2, \dots, b_n\)\(b_1 \cap b_2 \cap \dots \cap b_n = \emptyset\),对两边取反,根据德-摩根定律,可以得到 \(c_1 \cup c_2 \cup \dots \cup c_n = U\)\(c_i\)\(b_i\) 的补集),于是我们要找到 \(n\) 个贤者,他们不知道的咒语的并集为全集。如果把贤者看成点,不掌握的咒语看成边,那么这道题就变成了一般图最小点覆盖,也就是图中每一条边,都至少有一个端点被选中。

这是一个典型的 NPC 问题,暴力的时间复杂度是 \(O(n2^n)\) 但是我们依然可以降低复杂度。

首先,流浪者观察 \(k\) 天就走了,于是大于 \(k\) 的点覆盖都不行,于是复杂度先降低成 \(O(n2^k)\)

考虑一个显然的贪心,如果一个点 \(u\) 不选,那么与 \(u\) 相连的所有边的另一个端点必须选,那么选 \(u\) 一定不劣,于是我们按照度数从大到小选点,选了的点就删掉(不是真删,把 \(u\) 连接了的点的度数减 \(1\) 即可)。

但是这个做法会被一条链或一个环将常数卡成斐波那契数的复杂度,这是因为删了一个点后,总边数要么减少 \(1\),要么减少 \(2\),于是这种情况直接特判:

  • 如果找到了一个长度为 \(len\) 的环,那么就需要选中 \(\left\lceil \displaystyle\frac{len}{2}\right\rceil\) 个点才能点覆盖。因为要取并集,因此每个点都可能被选;

  • 如果找到了长度为 \(len\) 的链,需要分以下情况讨论:

    • 长度为偶数,此时选链上从左往右第 \(2, 4, \dots, \displaystyle\frac{len}{2}\) 的点即可覆盖整条链;

    • 长度为偶数,那么就需要选中 \(\left\lfloor \displaystyle\frac{len}{2}\right\rfloor\) 个点才能点覆盖。因为要取并集,因此每个点都可能被选;

其他情况直接贪心做起,此时选一个点至少减少了 \(3\) 条边,不选这个点减少了一个点,由于 \(n\)\(m\) 是同阶的,于是常数就降低成了 \(T(n) = T(n - 1) + T(n - 3), T(1) = 1\),此时 \(T(30)\) 只有 \(50000\) 多,足以通过此题。

点击查看代码
#include <bits/stdc++.h>
#define pii pair <int, int>
#define mk make_pair
using namespace std;
const int N = 1e3 + 9, M = 3e3 + 9, INF = 1e9 + 7;
struct Edge{
	int v, nex;
} e[M << 1];
int head[N], ecnt;
void addEdge(int u, int v){
	e[++ecnt] = Edge{v, head[u]};
	head[u] = ecnt;
}
int deg[N], T, n, m, k, ans;
bool vis[N], flag[N], flg[N], b[N], now[N];
map <pii, bool> mp;
void init(){
	memset(head, 0, sizeof(head));
	memset(deg, 0, sizeof(deg));
	memset(vis, 0, sizeof(vis));
	memset(flag, 0, sizeof(flag));
	memset(flg, 0, sizeof(flg));
	mp.clear();
	ecnt = 0;
	ans = 1e9 + 7;
}
vector <int> vec;
void dfs2(int u, int pre){
	b[u] = true;
	vec.push_back(u);
	for(int i = head[u]; i; i = e[i].nex){
		int v = e[i].v;
		if(vis[v] || v == pre)
			continue;
		if(!b[v])
			dfs2(v, u);
	}	
}
void del(int u, int x){
	for(int i = head[u]; i; i = e[i].nex){
		int v = e[i].v;
		deg[v] -= x;
	}	
}
void dfs(int cnt){
	if(cnt > ans || cnt > k)
		return;
	int maxn = 0;
	for(int i = 1; i <= n; i++)
		if(!vis[i] && deg[i] > deg[maxn])
			maxn = i;
	if(!maxn){
		if(cnt < ans){
			ans = cnt;
			memset(flg, 0, sizeof(flg));
		}
		for(int i = 1; i <= n; i++)
			flg[i] |= flag[i];
	} else if(deg[maxn] <= 2){
		memset(b, 0, sizeof(b));
		int tmp = 0;
		for(int i = 1; i <= n; i++)
			now[i] = flag[i];
		for(int i = 1; i <= n; i++){
			if(!vis[i] && !b[i] && deg[i] == 1){
				vec.clear();
				dfs2(i, 0);
				if(vec.size() % 2 == 1)
					for(int j = 1; j < (int)vec.size(); j += 2)
						flag[vec[j]] = 1;	
				else
					for(auto j : vec)
						flag[j] = 1;
				tmp += floor(1.0 * vec.size() / 2);	
			}
		}
		for(int i = 1; i <= n; i++)
			if(!vis[i] && !b[i] && deg[i] == 2){
				vec.clear();
				dfs2(i, 0);
				for(auto j : vec)
					flag[j] = 1;
				tmp += ceil(1.0 * vec.size() / 2);
			}
		if(cnt + tmp < ans){
			ans = cnt + tmp;
			memset(flg, 0, sizeof(flg));
		}
		if(cnt + tmp <= ans)
			for(int i = 1; i <= n; i++)
				flg[i] |= flag[i];
		for(int i = 1; i <= n; i++)
			flag[i] = now[i];
	} else {
		vis[maxn] = 1;
		del(maxn, 1);
		flag[maxn] = 1;
		dfs(cnt + 1);
		flag[maxn] = 0;
		vector <int> st;
		for(int i = head[maxn]; i; i = e[i].nex){
			int v = e[i].v;
			if(!vis[v]){
				st.push_back(v); 
				del(v, 1);
				flag[v] = 1;
				vis[v] = 1;
			}
		}
		dfs(cnt + st.size());
		for(auto v : st){
			vis[v] = 0;
			flag[v] = 0;
			del(v, -1);
		}
		del(maxn, -1);
		vis[maxn] = 0;
	}
}
int main(){
//	freopen("med1a.in", "r", stdin);
//	freopen("med1a.out", "w", stdout);
	scanf("%d", &T);
	while(T--){
		init();
		scanf("%d%d%d", &n, &m, &k);
		for(int i = 1; i <= m; i++){
			int u, v;
			scanf("%d%d", &u, &v);
			if(u > v)
				swap(u, v);
			if(mp[mk(u, v)])
				continue;
			mp[mk(u, v)] = true;
			addEdge(u, v);
			addEdge(v, u);
			deg[u]++;
			deg[v]++;
		}
		dfs(0);
		if(ans > k)
			printf("-1\n");
		else {
			int cnt = 0;
			printf("%d ", ans);
			for(int i = 1; i <= n; i++)
				if(flg[i])
					cnt++;
			printf("%d\n", cnt);
			bool space = false;
			for(int i = 1; i <= n; i++)
				if(flg[i] && space)
					printf(" %d", i);
				else if(flg[i]){
					printf("%d", i);
					space = true;
				}
			printf("\n");
		}	
	}
	return 0;
}
posted @ 2025-04-02 21:36  Orange_new  阅读(30)  评论(0)    收藏  举报