真正的骗子并查集+dp+回溯

一个岛上存在着两种居民,一种是天神,一种是恶魔。

天神永远都不会说假话,而恶魔永远都不会说真话。

岛上的每一个成员都有一个整数编号(类似于身份证号,用以区分每个成员)。

现在你拥有 nn 次提问的机会,但是问题的内容只能是向其中一个居民询问另一个居民是否是天神,请你根据收集的回答判断各个居民的身份。

输入格式

输入包含多组测试用例。

每组测试用例的第一行包含三个非负整数 n,p1,p2n,p1,p2,其中 nn 是你可以提问的总次数,p1p1 是天神的总数量,p2p2 是恶魔的总数量。

接下来 nn 行每行包含两个整数 xi,yixi,yi 以及一个字符串 aiai,其中 xi,yixi,yi 是岛上居民的编号,你将向编号为 xixi 的居民询问编号为 yiyi 的居民是否是天神,

aiai 是他的回答,如果 aiai 为 yes,表示他回答你“是”,如果 aiai 为 no,表示他回答你“不是”。

xi,yixi,yi 可能相同,表示你问的是那个人自己是否为天神。

当输入为占据一行的 0 0 0 时,表示输入终止。

输出格式

对于每组测试用例,如果询问得到的信息足以使你判断每个居民的身份,则将所有天神的编号输出,每个编号占一行,在输出结束后,在另起一行输出 end,表示该用例输出结束。

如果得到的信息不足以判断每个居民的身份,则输出 no,输出同样占一行。

数据范围

1≤xi,yi≤p1+p21≤xi,yi≤p1+p2,
0≤n<1000,0≤p1,p2<3000≤n<1000,0≤p1,p2<300

输入样例:
2 1 1
1 2 no
2 1 no
3 2 1
1 1 yes
2 2 yes
3 3 yes
2 2 1
1 2 yes
2 3 no
5 4 3
1 2 yes
1 3 no
4 5 yes
5 6 yes
6 7 no
0 0 0
输出样例:
no
no
1
2
end
3
4
5
6
end

分析

对于询问只有四种情况
天神 天神 yes
天神 恶魔 no
恶魔 天神 no
恶魔 恶魔 yes
即两人同类是yes,异类为no。用带权并查集来维护所有人的关系,每个集合根节点记为0,子节点与父节点同类值为0,子节点与父节点异类则值为1。对于以下的关系有1、3、6为同类,2、4、5为另一类。
带权并查集存储关系,f[i]=j表示i的父节点为j, d[i] = 1表示与父节点不同类, d[i] = 0表示与父节点同类
在这里插入图片描述

路径压缩:对于节点6压缩后直接连到节点1,更新f[6] = 1, d[6] = d[6] ^ d[4] = 0
在这里插入图片描述

合并:x和y同类op=0,x和y异类op=1
f[fy] = fx ; d[fy] = d[y] ^ d[x] ^ op
在这里插入图片描述

对于数据4
5 4 3
1 2 yes
1 3 no
4 5 yes
5 6 yes
6 7 no
集合1 1 2为同类,3为另一类
集合2 4 5 6为同类,7为另一类
每一个集合都有两种情况,对于集合1, 1 2可能为天神3为恶魔,或者1 2为恶魔3为天神。
动态规划统计出(p, q)这种情况的个数,如果为1则可以确定每个人的身份。

回溯确定每个集合根节点的身份,即可确定每个人的身份
例:dp(4,3)=1,最后一次选的是集合2,上一状态只能是dp(1,2)和dp(3,1),dp(3,1)=0而dp(1,2)=1说明集合2中4 5 6是天神、7是恶魔

参考代码

#include <iostream>
#include <cstring>
using namespace std;

const int N = 310;

int n, p, q, x, y;
int f[2*N], d[2*N], dp[N][N], a[2*N][2], val[2*N];
string str;
bool op;

int find(int x){//并查集带路径压缩
    if(x!=f[x]){
        int u = find(f[x]);
        d[x] ^= d[f[x]];
        f[x] = u;
    }
    return f[x];
}

void print(int x, int y, int top){//回溯
    if(x==0 && y==0) return;
    while(top && !a[top][0] && !a[top][1]) top--;
    int i = a[top][0], j = a[top][1];
    if(x-j>=0 && y-i>=0 && dp[x-j][y-i]==1) {
        val[top] = 0;
        print(x-j, y-i, top-1);
    }
    else if(x-i>=0 && y-j>=0 && dp[x-i][y-j]==1){
        val[top] = 1;
        print(x-i, y-j, top-1);
    }
}

int main(){
    while(cin >> n >> p >> q){
        if(n==0 && p==0 && q==0) break;
        for(int i=0; i<=p+q; i++){
            f[i] = i;
            d[i] = 0;
        } 
        while(n--){//读入数据,并合并集合
            cin >> x >> y >> str;
            if(str=="yes") op = 0;
            else op = 1;
            int fx = find(x), fy = find(y);
            if(fx!=fy){
                f[fy] = fx;
                d[fy] = d[x]^d[y]^op;
            }
      
        }
        memset(val, 0, sizeof val);
        memset(dp, 0, sizeof dp);
        memset(a, 0, sizeof a);
        for(int i=1; i<=p+q; i++){//统计每个集合天神和恶魔的人数
            x = find(i);
            a[x][d[i]]++;
        }
        int cnt = 0;
        dp[0][0] = 1;
        for(int i=1; i<=p+q; i++){//计算dp(i, j),i个天生、j个恶魔的可能个数
            if(a[i][0] || a[i][1]){
                cnt += a[i][0] + a[i][1];
                for(int j=p, k; j>=0; j--){
                    k = cnt - j;
                    if(k<0) continue;
                    if(j-a[i][0]>=0 && k-a[i][1]>=0){
                        dp[j][k] += dp[j-a[i][0]][k-a[i][1]];

                    }
                    if(j-a[i][1]>=0 && k-a[i][0]>=0)
                        dp[j][k] += dp[j-a[i][1]][k-a[i][0]];   
                }    
            }
        }
        if(dp[p][q]==1) {//dp(p, q)只有1种情况,即可确定每个人身份
            int cnt = 0;
            for(int i=1; i<=p+q; i++) cnt += d[i];
            if(cnt == p) op = 1;
            else op = 0;
            print(p, q, p+q);
            for(int i=1; i<=p+q; i++){
                int u = find(i);
                if(val[u]^d[i]) 
                    cout << i << endl;
            }
            cout << "end" << endl;
        }
        else cout << "no" << endl;
    }
    return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

木灬U6770

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值