题目:http://poj.org/problem?id=1417
另一个地方:https://www.acwing.com/problem/content/description/261/
题意:
一个岛上存在着两种居民,一种是天神,一种是恶魔。
天神永远都不会说假话,而恶魔永远都不会说真话。
岛上的每一个成员都有一个整数编号(类似于身份证号,用以区分每个成员)。
现在你拥有n次提问的机会,但是问题的内容只能是向其中一个居民询问另一个居民是否是天神,请你根据收集的回答判断各个居民的身份。
输入:
输入包含多组测试用例。
每组测试用例的第一行包含三个非负整数n,p1,p2 其中n是你可以提问的总次数,p1是天神的总数量,p2是恶魔的总数量。
接下来n行每行包含两个整数xi,yi以及一个字符串ai,其中xi,yi是岛上居民的编号,你将向编号为xi的居民询问编号为yi的居民是否是天神,
ai是他的回答,如果ai为“yes”,表示他回答你“是”,如果ai为“no”,表示他回答你“不是”。
xi,yi可能相同,表示你问的是那个人自己是否为天神。
当输入为占据一行的“0 0 0”时,表示输入终止。
输出:
对于每组测试用例,如果询问得到的信息足以使你判断每个居民的身份,则将所有天神的编号输出,每个编号占一行,在输出结束后,在另起一行输出“end”,表示该用例输出结束。
如果得到的信息不足以判断每个居民的身份,则输出“no”,输出同样占一行。
咋一看有2-sat问题内味儿,但是细看就不一样,对于每个居民来说要么是天使要么是恶魔,可能性只有一种,不可能既可以是天使也可以是恶魔,这就和2-sat问题不一样了
对于此题,假如居民回答的是yes,那么这两个居民一定同为天使或者同为恶魔,假如居民回答为no,那么这两个居民一定一个为天使一个为恶魔
想到并查集,使用relat[i]表示 i 与其根节点的关系,relat[i]=0 表示他们是一样的,relat[1]表示他们是不一样的
到这里还不够,因为仅仅通过节点之间是否一样还没办法判断是否有一种唯一的方案可以满足居民的回答
对于某一棵由并查集形成的树,给予其根节点两个属性: sam_siz: 这棵树中和根节点一样的节点有多少个 , dif_siz:这棵树中和根节点不一样的节点有多少个
那么最后天使的数量就是所有并查集形成的树中天使的个数。
但是对于每一棵树,其根节点可以是天使也可以是恶魔,这两种选择需要选择一种。
因此使用dp[i][j]表示前i棵树中,天使数量为j的方案数
只有到最后dp[n][p1]==1 ,才表示有唯一方案满足居民的回答
最后一个问题,如何输出天使居民的编号。
我们可以在dp过程中记录路径,对于每个dp[i][j]记录其来源,以及 这个来源的并查集树的根节点是选择了天使还是选择恶魔,那么回溯的时候就可以追溯来源并且追溯到每个来源的根节点是选择了天使还是恶魔,进而知道那棵并查树有哪些节点选择了天使
代码:
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<cmath>
#include<vector>
#include<queue>
#include<map>
#define ll long long
#define ull unsigned long long
using namespace std;
const int INF = 0x3f3f3f3f;
const int maxn = 1007;
// fat:并查集的根节点 relat 关系数组 ,same_siz[i]:以i为根的并查集树和
//i相同的节点有多少 ,dif_siz[i]:和same_siz相对
int fat[maxn], relat[maxn], same_siz[maxn], dif_siz[maxn];
int n, p1, p2;
// dp[第几棵并查集树][天使数] = 方案数
int dp[1005][305];
//pre :回溯数组 flag:回溯时的那颗并查集树根节点是天使还是恶魔
//need[i]: 节点i是天使还是恶魔
int pre[1005][305], flag[1005][305], need[1005];
//V:里面放着并查集树的根
vector<int> V;
void init() {
memset(dp, 0, sizeof(dp));
for (int i = 1; i <= p1+p2; i++) {
fat[i] = i;
relat[i] = 0;
same_siz[i] = dif_siz[i] = 0;
}
V.clear();
V.push_back(0); //加了这句是为了后面背包的时候i从1开始
}
int trace(int x) {
if (fat[x] == x) {
return x;
}
else {
int ret = trace(fat[x]);
relat[x] ^= relat[fat[x]];
return fat[x] = ret;
}
}
//relat : 0:same 1:different
bool combine(int a, int b, int opt) {
int f_a = trace(a), f_b = trace(b);
if (f_a == f_b) {
if (opt != relat[a] ^ relat[b])
return 0;
}
else {
fat[f_a] = f_b;
relat[f_a] = opt ^ relat[a] ^ relat[b];
}
return 1;
}
int main() {
char s[5];
int inp1, inp2, ok;
while (cin >> n >> p1 >> p2,n||p1||p2) {
init();
ok = 1;
for (int i = 1; i <= n; i++) {
scanf("%d %d %s", &inp1, &inp2, s);
ok = 1;
if (inp1 == inp2) {
if (s[0] == 'n') {
ok = 0;
}
}
else {
if (combine(inp1, inp2, s[0] == 'n' ? 1 : 0) == 0)
ok = 0;
}
}
if (ok == 0) {
cout << "no" << endl;
continue;
}
//下面是记录并查集根节点的同时统计sam_siz and dif_siz
for (int i = 1; i <= p1+p2; i++) {
trace(i);
if (fat[i] == i) {
V.push_back(i);
}
if (relat[i] == 0) {
same_siz[fat[i]]++;
}
else
dif_siz[fat[i]]++;
}
// 背包
dp[0][0] = 1;
for (int i = 1; i < V.size(); i++) {
for (int j = same_siz[V[i]]; j <= p1; j++) {
if (dp[i - 1][j - same_siz[V[i]]]) {
dp[i][j] += dp[i - 1][j - same_siz[V[i]]];
pre[i][j] = j-same_siz[V[i]];//same
flag[i][j] = 0;//same
}
}
for (int j = dif_siz[V[i]]; j <= p1; j++) {
if (dp[i - 1][j - dif_siz[V[i]]]) {
dp[i][j] += dp[i - 1][j - dif_siz[V[i]]];
pre[i][j] = j - dif_siz[V[i]];
flag[i][j] = 1;//different
}
}
}
if (dp[V.size() - 1][p1] == 1) {
int now = V.size() - 1, siz = p1;
//先统计好need数组减少时间复杂度
while (now) {
need[V[now]] = flag[now][siz];
siz = pre[now][siz];
now--;
}
//通过need数组来对天使居民输出
for (int i = 1; i <= p1+p2; i++) {
if (relat[i] == need[fat[i]])
cout << i << endl;
}
cout << "end" << endl;
}
else
cout << "no" << endl;
}
return 0;
}