带权并查集。题目先假设同性之间不发生性行为,然后给出很多对昆虫的性行为记录,问你从这些记录中能不能推得假设错误。
有两种方法,第一种方法思路简单,第二种方法类似POJ 1182。
这种题就是根据记录1 ~ i-1
作为已知信息,判断当前记录i
是否与之冲突。
其实判断的只是造成假设错误的充分条件,但这就已经符合题目要求了,也无法做更多判断。
(在第二种方法中思考这句话比较直观)其实,只有当该条记录的两点可以被之前记录的点连起来了(不仅仅是都出现过),才能真正地对这条记录作出判断。
1.
这种方法直接判断每条记录的两点是否同性。用并查集保存一个个集合,每个集合内性别相同。(不同集合之间性别关系未知,无法判断,即默认不冲突)
利用且只有利用情敌关系构建同性集合,情敌包括直接情敌和间接情敌,举一个间接情敌的例子:a
和b
是直接情敌、都连接d
,b
和c
是直接情敌、都连接e
,那么a
和c
就是间接情敌。
sex[]
记录每个点的原配,所以把点x
之后出现的对象都和原配sex[x]
u
一下就完事了。可以保证情敌关系(直接或间接)一定在一个集合内。
#include <iostream>
#include <algorithm>
#include <vector>
#include <cstring>
using namespace std;
const int MAXN = 2001;
int T, N, M;
int pre[MAXN];
int sex[MAXN];
bool flag;
void init()
{
memset(pre, -1, sizeof pre);
memset(sex, 0, sizeof sex);
flag = false;
}
int f(int x)
{
if (pre[x] < 0) return x;
return pre[x] = f(pre[x]);
}
void u(int a, int b)
{
int f1 = f(a);
int f2 = f(b);
if (f1 == f2) return;
if (pre[f1] <= pre[f2])
{
pre[f1] += pre[f2];
pre[f2] = f1;
}
else
{
pre[f2] += pre[f1];
pre[f1] = f2;
}
}
int main()
{
int a, b;
scanf("%d", &T);
for (int i = 1; i <= T; i++)
{
init();
scanf("%d%d", &N, &M);
for (; M--;)
{
scanf("%d%d", &a, &b);
if (flag) continue;
if (f(a) == f(b))
{
flag = true;
continue;
}
if (!sex[a]) sex[a] = b;
else u(sex[a], b);
if (!sex[b]) sex[b] = a;
else u(sex[b], a);
}
printf("Scenario #%d:\n", i);
if (flag) printf("Suspicious bugs found!\n\n");
else printf("No suspicious bugs found!\n\n");
}
return 0;
}
2. 参考POJ 1182。
再写一种,这种方法的思想比较难写出来。我的理解,sign
数组的意义是不确定的,有时指与父节点的一致情况,有时指与根节点的一致情况,当然,相对来说,其实都是指与父节点的一致情况。要搞清这个,关键在于理解find
函数的递归过程,首先符合压缩规则这没问题,
sign[x] = (sign[x] + sign[pre[x]]) & 1;
这一句完成了sign
意义的转化,sign[x]
旧值指与父节点的一致情况(上一次被赋值时是直接指向根节点,然而当时的根节点已不是现在的根节点),sign[x]
新值是指与根节点的一致情况,pre[x]
在现在还是指父节点(马上下一句就变成根节点了),因为父节点在上次递归返回中已被直接指向根节点,所以sign[pre[x]]
的意义及值也得到更新,所以sign[pre[x]]
是指父节点与根节点的一致情况,所以,根据父节点过渡,负负得正,得到当前节点与根节点的一致情况。
所以,find
函数先根据访问点层层上升直到根节点,然后从根节点下降,把沿途经过的每个结点直接指向根节点并赋予其正确的与根节点的一致情况。
接下来我说一说union/merge
函数,之前我想的是,首先合并肯定不会涉及flag
,合并是根1直接指向根2,那么根1及其子树可能都要变sign
,然而,只用更新根1的sign
就行了,因为每次查询根的时候都会把链粉碎并自上而下地赋予sign
,所以只要保证子树的根 根1
的sign
正确就行了。那么,原先的两个查询点n1 n2
已经直接指向根1根2,且n1 n2
一定不一致,根1到根2的一致情况也就可以通过下式确定了。
sign[f1] = (sign[n1] + sign[n2] + 1) & 1;
#include <iostream>
#include <algorithm>
#include <vector>
#include <cstring>
using namespace std;
const int MAXN = 2001;
int T, N, M;
int pre[MAXN];
int sign[MAXN]; // 0表示一致,1表示不一致
bool flag;
void init()
{
memset(pre, -1, sizeof pre);
memset(sign, 0, sizeof sign);
flag = false;
}
int f(int x)
{
if (pre[x] < 0) return x;
int t = f(pre[x]);
sign[x] = (sign[x] + sign[pre[x]]) & 1; // 也可以写成 sign[x] ^ sign[pre[x]]
return pre[x] = t;
}
void u(int n1, int n2)
{
int f1 = f(n1);
int f2 = f(n2);
if (f1 == f2)
{
if (sign[n1] == sign[n2])
flag = true;
return;
}
if (pre[f1] <= pre[f2])
{
pre[f1] += pre[f2];
pre[f2] = f1;
sign[f2] = (sign[n1] + sign[n2] + 1) & 1; // 也可以写成 !(sign[n1] ^ sign[n2])
}
else
{
pre[f2] += pre[f1];
pre[f1] = f2;
sign[f1] = (sign[n1] + sign[n2] + 1) & 1;
}
}
int main()
{
int a, b;
scanf("%d", &T);
for (int i = 1; i <= T; i++)
{
init();
scanf("%d%d", &N, &M);
for (; M--;)
{
scanf("%d%d", &a, &b);
if (flag) continue;
u(a, b);
}
printf("Scenario #%d:\n", i);
if (flag) printf("Suspicious bugs found!\n\n");
else printf("No suspicious bugs found!\n\n");
}
return 0;
}