True Liars POJ1417(背包 + 并查集 + 路径回溯)

 解题思路:

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.非循环普通传递关系,直接路径合并即可,无需取模

二.在我们要对并查集森林中的每颗树进行处理时,我们可以转化成邻接矩阵.

三.背包回溯的实现.

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值