[PA 2022] Mędrcy

发布时间 2023-10-28 17:01:44作者: 小篪篪

题面:[PA 2022] Mędrcy
看到这道题没有题解, 所以过来水了一篇。

从题目上来看,这是一道经典的智力游戏问题,这类问题的核心其实就一点,为什么他会得到自己想要的信息。本题中想要知道的信息是是否存在自己不知道的咒语。假设有一个人小 A 知道所有的咒语,那么因为所有人都绝顶聪明,小 A 会知道每个人会在什么时候走掉。那反过来,如果小 A 发现了与自己所知道的信息不符的情况(本来不应该走掉的走掉了,本来该走掉的没走掉) ,那他就知道肯定有自己不知道的咒语,也就会在这一天的晚上走掉。

跟据刚才的分析,我们现在只需要知道什么情况下会产生冲突即可。如果一个人没有加入任何群,那么他会在第一天之前就走掉。如果两个人加入的群没有交集,那么他们会认为对方在第一天之前就该走掉,所以如果第二天见面了,他们会在当晚走掉。如果三个人,两两之间存在一个群,但没有一个同时包含三人的群,那么他们会认为剩下两个人应该是上一种情况,所以三人在第二天还能互相见到时,就会在当天晚上走掉。

以此类推,我们可以发现,如果存在 \(a\) 个人,每 \(a - 1\) 个人知道一个咒语,且不存在 \(a\) 个人同时知道的咒语,那么我们的答案将小于等于 \(a\) ,更确切地说,答案是所有满足条件的 \(a\) 中最小的。由于上面的推理证明了此结论的必要性(如果存在这样的 \(a\) 个人那么依照上面的方式逐层分析,可以得到这 \(a\) 个人必定同时在第 \(a\) 天的前一个晚上走掉),所以我们需要证明本结论的充分性,即没有比这样的 \(a\) 更小的数成为答案。

证明:假设 \(b\) 为答案,那么就有 \(b\) 时间会产生第一个离开,所以 \(b\) 时刻的离开是因为对于某个在 \(b\) 天前一晚走的人来说在 \(b - 1\) 天出现了本来该在前一天晚上走却留了下来的人。那么我们可以递归出一个子问题,即去除 \(x\) 和所有不含 \(x\) 的咒语之后,应该得到一个答案为 \(b - 1\) 的问题。这样逐层递归(也可能不经过任何递归),必定会有一个时刻问题的答案为 \(1\) ,此时有存在一个人没加入任何咒语,即答案满足上面给出的结论中的答案 \(a\) 。我们考虑归纳,如果 \(b - 1\) 层满足上面的结论,那么在 \(b\) 层,我们需要加入一个不含 \(x\) 的咒语使得答案为 \(b\) 。如果这个咒语不包含所有 \(b - 1\) 层判断应该走掉的点,那么根据上面答案的必要性(多余的咒语并不影响 \(b - 1\) 答案的必要性,只会影响答案的充分性,因为我少考虑咒语一定只会使答案变大),\(b\) 层的答案必定小于等于 \(b - 1\) ,这与条件答案为 \(b\) 不相符。故我们得到了像上面所说的情况,并且证明了情况的充分性。

那么,我们的问题就转化成了求最小的合法情况,但这明显是不好求的,我们需要再次转化。我们尝试减少限制,不要求每 \(a - 1\) 个人知道一个咒语,因为这看上去并不是必须的,事实证明,去掉此限制后的最优解依然是答案。我们尝试对此进行证明:如果我们以新条件去获取答案, 设此时选出来的人员集合为 \(S\) ,若本集合在原条件下不合法,则必定存在一个人 \(x\) ,不存在不包含它而包含其它 \(S\) 中的人的咒语,此时去掉 \(x\) 符合新条件且是一个更优解。

当然经过优化后的算法还是太劣了,且没有用到每个咒语包含 \(n - 2\) 人的限制,所以考虑将选中问题转化成不被选中问题。我们记 \(C_i\) 为不包含第 \(i\) 个人的魔咒集合,那么如果某个人员集合 \(S\) 合法,当且仅当 \(\cup_{i}C_i\) 为所有魔咒组成的集合,否则,不在集合里的魔咒会包含 \(S\) 中的所有人,是 \(S\) 集合不合法。

再次转化后,我们的问题变得看上去更加好求,但依旧没有用到 \(n - 2\) 的人数限制。对于这种一个点对应两个量的问题,我们可以想到连接这两个量,建出图来。思考一下我们的转化,点表示人,边表示魔咒,再思考我们的需求,人数最小,魔咒全选,那么可以轻松地将问题转化成最小点覆盖问题。

转化成最小点覆盖问题以后,我们发现貌似进入了一个死胡同。因为最小点覆盖问题是一个经典的 NPC 问题,这使得问题看上去不太可做。由于原问题和最小点覆盖问题的转化完全等价,所以我们完全不能寻求转化上的突破,只能转而向另一个方向上寻求帮助,也就是答案的大小。很显然,这道题的作者也解决不了这一 NP 问题,所以他用了一个非常巧妙的办法,在答案上动手脚。注意到答案如果大于 \(k\) 就直接输出 -1 即可,这就给了我们对于暴力算法剪枝的可能。

考虑我们要求出最小点覆盖和其所有方案的一个算法:寻找一个度数大于 \(2\) 或者为 \(0\) 的点,如果是度数为 \(0\) 的点,直接删除再递归下去,否则,递归两种情况,选择自己和选择所有相邻点。我们对两种情况分别递归,选择自己就把自己和相邻点边删除,选择相邻点就把自己和相邻点以及这些点的相邻边删除。最后我们会省下一些环和链(注意,没有树和基环树),直接判断即可。这里有一个需要注意的点,就是我们并不会寻找到一个度数为 \(1\) 的点后贪心地去选择它的临点,因为这样答案大小是对的,但方案却不对。

最后, 我们在答案天数走掉的人就是所有最小覆盖的方案数选择的点的并。

code:

#include <cstdio>
#include <algorithm>
#include <set> 
using namespace std;

#define MAXN 1000

struct node {
	int ed;
	node *next;
}*s[MAXN + 5];
int k;
int n, m;
bool Flag[MAXN + 5], vis[MAXN + 5], Ans[MAXN + 5];
int ans;
set<pair<int, int> > se;

void push (int u, int v) {
	node *p = new node;
	
	p->ed = v;
	p->next = s[u];
	s[u] = p;
}
void dfs (int sum) {
	if (sum > k) {
		return ;
	}
	
	int loc = 0;
	bool flag = 0;
	
	for (int i = 1; i <= n; i ++) {
		if (vis[i]) {
			continue;
		}
		
		int v = 0;
		
		for (node *j = s[i]; j; j = j->next) {
			if (vis[j->ed]) {
				continue;
			}
			v ++;
		}
		if (v >= 3 || v == 0) {
			loc = i;
			flag = (v == 0);
			
			break;
		}
	}
	if (!loc) {
		vector<int> S;
		
		for (int i = 1; i <= n; i ++) {
			if (vis[i]) {
				continue;
			}
			
			int d = 0;
			
			for (node *j = s[i]; j; j = j->next) {
				if (vis[j->ed]) {
					continue;
				}
				d ++;
			}
			if (d == 1) {
				int v = 0;
				int j = i;
				vector<int> sn;
				
				while (1) {
					vis[j] = 1;
					sn.push_back (j);
					v ++;
					
					bool flag = 0;
					
					for (node *k = s[j]; k; k = k->next) {
						if (vis[k->ed]) {
							continue;
						}
						j = k->ed;
						flag = 1;
						
						break;
					}
					if (!flag) {
						break;
					}
				}
				sum += (v >> 1);
				for (auto j : sn) {
					S.push_back (j);
				}
				if (v & 1) {
					for (int j = 1; j < sn.size(); j += 2) {
						Flag[sn[j]] = 1;
					}
				}
				else {
					for (auto j : sn) {
						Flag[j] = 1;
					}
				}
			}
		}
		for (int i = 1; i <= n; i ++) {
			if (vis[i]) {
				continue;
			}
			
			int v = 0;
			int j = i;
			
			while (1) {
				S.push_back (j);
				vis[j] = 1;
				Flag[j] = 1;
				v ++;
				
				bool flag = 0;
				
				for (node *k = s[j]; k; k = k->next) {
					if (vis[k->ed]) {
						continue;
					}
					j = k->ed;
					flag = 1;
					
					break;
				}
				if (!flag) {
					break;
				}
			}
			sum += ((v + 1) >> 1);
		}
		if (sum <= k) {
			if (sum < ans) {
				for (int i = 1; i <= n; i ++) {
					Ans[i] = Flag[i];
				}
				ans = sum;
			}
			else if (sum == ans) {
				for (int i = 1; i <= n; i ++) {
					Ans[i] |= Flag[i];
				}
			}
		}
		for (auto i : S) {
			vis[i] = 0;
			Flag[i] = 0;
		}
		
		return ;
	}
	if (flag) {
		vis[loc] = 1;
		dfs (sum);
		vis[loc] = 0;
	}
	else {
		vector<int> S;
		
		for (node *i = s[loc]; i; i = i->next) {
			if (!vis[i->ed]) {
				S.push_back (i->ed);
			}
		}
		vis[loc] = 1;
		Flag[loc] = 1;
		dfs (sum + 1);
		Flag[loc] = 0;
		for (auto i : S) {
			vis[i] = 1;
			Flag[i] = 1;
		}
		dfs (sum + S.size());
		for (auto i : S) {
			vis[i] = 0;
			Flag[i] = 0;
		}
		vis[loc] = 0;
	}
}

int main () {
//	freopen ("med1a.in", "r", stdin);
//	freopen ("group.out", "w", stdout);
	
	int t;
	
	scanf ("%d", &t);
	while (t --) {
		scanf ("%d %d %d", &n, &m, &k);
		for (int i = 1; i <= n; i ++) {
			s[i] = 0;
		}
		for (int i = 1; i <= n; i ++) {
			vis[i] = 0;
			Flag[i] = 0;
			Ans[i] = 0;
		}
		se.clear();
		for (int i = 1; i <= m; i ++) {
			int u, v;
			
			scanf ("%d %d", &u, &v);
			if (u > v) {
				swap (u, v);
			}
			if (se.find(make_pair (u, v)) != se.end()) {
				continue;
			}
			se.insert (make_pair (u, v));
			push (u, v);
			push (v, u); 
		}
		ans = k + 1;
		dfs (0);
		if (ans > k) {
			printf ("-1\n");
		} 
		else {
			printf ("%d ", ans);
			
			int sum = 0;
			
			for (int i = 1; i <= n; i ++) {
				if (Ans[i]) {
					sum ++;
				}
			}
			printf ("%d\n", sum);
			for (int i = 1; i <= n; i ++) {
				if (Ans[i]) {
					printf ("%d ", i);
				}
			}
			putchar ('\n');
		}
	}
}