[PA 2022] Mędrcy
题面:[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');
}
}
}