解题思路:
1.首先这道题涉及关系问题,考虑使用并查集维护关系.对于a和b,若a说b是诚实人,那么a与b的身份必然相同,如果a是诚实人,那么b也是诚实人;如果a是骗子,那么b也是骗子.反过来,如果a说b是骗子,说明a与b的身份不同.这样我们可以用01边权的并查集维护这些人的关系了.最后我们可以获得一个并查集(森林)
2.如果诚实人和骗子的数量一样多,显然我们是无法得知哪个是骗子,哪个是诚实人的.
3.对于每棵树,我们可以枚举这棵树中一个人的身份,如果最后得到的合法的方案只有一种,那么有解,反之无解.我们采用01背包实现这个过程.(注意,由于这道题要求我们回溯答案,所以不要对背包降维)
代码实现如下:
#include <iostream>
#include <algorithm>
#include <queue>
#include <cstring>
#include <cstdio>
#include <cmath>
#include <map>
#include <vector>
#define LOCAL
#define INF 0x3f3f3f3f3f3f3f3f
#define IOS ios::sync_with_stdio(false), cin.tie(0), cout.tie(0);
#define PII pair<int, int>
#define int long long
#define ll long long
#define debug(a) cout << #a << "=" << a << endl;
#define all(x) (x).begin(), (x).end()
#define sf(x) scanf("%lld", &(x))
using namespace std;
const int N = 1050;
struct rec {
int same, diff;
}item[N];
int n, p, q;
int fa[N], d[N];
int f[N][N];
vector<int> son[N], group_0[N], group_1[N];
void init() {
memset(f, 0, sizeof f);
for (int i = 1; i <= 1000; ++i) {
fa[i] = i, d[i] = 0;
son[i].clear();
group_0[i].clear();
group_1[i].clear();
}
}
int find(int x) {
if (x == fa[x])
return x;
int root = find(fa[x]);
d[x] ^= d[fa[x]];
return fa[x] = root;
}
void merge(int x, int y, int z) {
int fx = find(x), fy = find(y);
if (fx == fy)
return;
fa[fx] = fy;
d[fx] = d[x] ^ d[y] ^ z;
}
signed main() {
#ifdef LOCAL
freopen("input.in", "r", stdin);
freopen("output.out", "w", stdout);
#endif
IOS;
while (cin >> n >> p >> q, n || p || q) {
init();
for (int i = 1; i <= n; ++i) {
int x, y;
string s;
cin >> x >> y >> s;
merge(x, y, s == "yes" ? 0 : 1);
}
if (p == q) {
cout << "no\n";
continue;
}
//将并查集森林转化成邻接矩阵方便后续操作
for (int i = 1; i <= p + q; ++i)
son[find(i)].push_back(i);
//处理并查集森林,主要处理每个根节点有多少个子节点与其相同,和与其相异,并查集中的相对关系至关重要
int cnt = 0;
for (int i = 1; i <= p + q; ++i) {
int t = son[i].size();
if (t == 0)
continue;
++cnt;
int sum = 0;
for (int j = 0; j < son[i].size(); ++j)
if (d[son[i][j]] == 1)
group_1[cnt].push_back(son[i][j]);
else {
group_0[cnt].push_back(son[i][j]);
++sum;
}
item[cnt].same = sum;
item[cnt].diff = t - sum;
}
//f[i][j]表示前i个树中,有j个骗子
f[0][0] = 1;
for (int i = 1; i <= cnt; ++i)
for (int j = 0; j <= p; ++j) {
if (j >= item[i].same)
f[i][j] += f[i - 1][j - item[i].same];
if (j >= item[i].diff)
f[i][j] += f[i - 1][j - item[i].diff];
}
if (f[cnt][p] != 1) {
cout << "no\n";
continue;
}
int val = p;
vector<int> ans;
for (int i = cnt; i >= 1; --i) {
if (f[i - 1][val - item[i].same] == 1) {
for (int j = 0; j < (int)group_0[i].size(); ++j)
ans.push_back(group_0[i][j]);
val -= item[i].same;
}else {
for (int j = 0; j < (int)group_1[i].size(); ++j)
ans.push_back(group_1[i][j]);
val -= item[i].diff;
}
}
sort(all(ans));
for (int i = 0; i < (int)ans.size(); ++i)
cout << ans[i] << "\n";
cout << "end\n";
}
}
总结:
一.带权并查集该如何解决问题?
1.想办法把关系转化成路径长度
2.通过向量偏移合并两个集合
几大关系转化路径:
1.循环两元关系,非黑即白,利用1和0表示,在路径合并的时候使用异或运算
2.循环多元关系,1->2->3->...->n->1,用0~n-1表示,把每对关系用0~n-1表示,在路径合并的时候相加取模于n,其实循环二元关系也可以这么操作
3.非循环普通传递关系,直接路径合并即可,无需取模
二.在我们要对并查集森林中的每颗树进行处理时,我们可以转化成邻接矩阵.
三.背包回溯的实现.