True Liars POJ - 1417 (并查集+01背包)

在一艘小船上漂流了几天之后,Akira Crusoe Maeda 被困在一个雾气弥漫的小岛上。尽管他已经筋疲力尽,但他仍然想起了他童年时从族长那里听到的雾岛的传说,那个传说中的岛屿。在传说中,有两个部落居住在岛上,一个是神族,另一个是恶魔,一旦神圣部落的成员祝福你,你的未来会变得光明而充满希望,灵魂最终会升入天堂,相反,一旦恶魔部落的成员诅咒你,你的未来会变得凄凉无望,灵魂最终会沉入地狱。
为了防止最坏的情况发生,Akira 要区分恶魔与神族。但是他该怎么做呢?这些人看起来完全相同,他不能仅仅通过他们的外表来区分彼此。然而,他仍然有最后的办法。神圣部落的成员都是诚实的,也就是说,他们总是说实话。而恶魔部落的成员都是骗子,也就是说,他们总是说谎。
Akira 问这些人中的某些人是否神圣。这些人非常了解彼此并且总是根据他们的特质“忠实地”回应他(他们总是说实话或总是撒谎)。他不敢问任何其他别的的问题,因为传说中一个恶魔会在他不喜欢这个问题时永远诅咒提问的人。他还有另一条重要的线索:传说提到了两个部落的人口。因为生活在这个岛上的每个人都是不朽的,所以这个数字是值得信赖的。至少在这几千年里岛上的人口都没有改变过。
你是一个优秀的计算机程序员,请你帮助 Akira 编写一个程序,根据他的问题答案对居民进行分类,帮助他脱离困境。

输入

输入格式如下:

n p1 p2
xl yl a1
x2 y2 a2
...
xi yi ai
...
xn yn an

第一行有三个非负整数 n, p1, 和 p2. n 是 Akira 问的问题数. p1 和 p2 是在传说中记载的神族和恶魔族的人口数量。下面的每 n 行包括两个整数xi, yi和一个单词 ai。 xi 和 yi 是每个人的编号,号码范围从 1 到 p1 + p2。 ai 可能是 yes 或者 no。如果居民 xi 想让你认为 yi 是神族,他会回答 yes。注意:xi 和 yi 可能相同。所以”你是神族吗?“是一个有效的问题。还要注意,Akira 十分紧张,所以相同的问题可能重复出现。
数据范围:
n < 1000
p1,p2 < 300
出现 0 0 0 的一行标示数据结束。 数据保证成立,没有矛盾数据。
 

输出

对于每个数据集,如果它包含足够的信息来对所有居民进行分类,则按升序打印所有神族居民的编号,一行一个。 在输出数字后再输出end。 如果给定数据集不包括足以识别所有神族的信息,输出no。

样例输入

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

题目分析:有一点十分重要的是题目告诉我们神说真话,恶魔说假话,所以我们可以先假设:

(1)YES情况: A说B是神,如果A是神,那么A说真话所以B也是神,A是恶魔,A说假话,B也                                是恶魔

                            B是神,A说B是神,A说的是真话吗,则A是神,B是恶魔,A说B是神,A说假                                话,A是恶魔

(2)NO情况: A说B是恶魔,如果A是恶魔,A说假话,那么B是神,反之如果B是神,A说的                                  是假话,A是恶魔

                           假设A是神,那么A说真话,B是恶魔,如果B是恶魔,A说的是真话,A是神

从上面的分析中我们可以得出如果是YES的情况,那么2者一定是同一类,如果是NO的情况,那2者一定是不同类,一说的归类我们就很容易想到并查集,那问题来了,如果是同一类我们就把2者合并,那如果是不同类呢?不做任何处理显然是不对的,所以我们就需要用一个rel数组来记录当前节点与根节点的关系,rel[x]==0说明x与根节点是同一类,rel[x]==1说明x与根节点不同类,我们这里是用异或来处理子节点和根节点的关系(之前好像做过类似的用异或来处理关系的题目,不过本人太菜了又给忘了。。。。,最后还是看的别的大佬的博客),做完了这一步后现在我们得到了一堆一堆的集合,现在的任务是从这些集合中选出特定的集合来凑出神,然后想到了01背包问题,不过这里和01背包有一点些许的不同就是01背包是当前物品有2种选择选或者是不选,而我们这里是有3种,选同类,选不同类,不选,这里我们设dp[i][j]表示神有i人恶魔有j人的情况的种类,初始是dp[0][0]==12者都是0人的时候就1中情况喽,做完之后如果dp[神总人数][恶魔总人数]!=1可以直接输出no,如果不等于1,这里就有坑点了,之前我们用01背包算的dp数组中的情况可能会有被覆盖的情况,所以就需要记录一下路径,具体的就看代码吧,

说明一下这个代码是我看着另一位大佬的博客写的,但是我又给找不到了QAQ。。。,求勿喷

#include <iostream>
#include <cstring>
#include <cstdio>
using namespace std;
const int N=610;
int m,n,p1,p2,scc;
int p[N],rel[N];
int num[N][2],root[N],dp[N][N];//num[i][j]记录的是第i个集合情况是j的集合元素的个数,root是记录每一组的根节点 
int pre[N][N];//用来记录路径 
bool ans[N];
int find(int x)
{
	if(p[x]!=x)
	{
		int t=find(p[x]);
		rel[x]=rel[x]^rel[p[x]];
		p[x]=t;
	}
	return p[x];
}
void solve()
{
	int i,j,k,x,y;
	
	memset(dp,0,sizeof dp);
	dp[0][0]=1;
	for(i=1;i<=scc;i++)
	{
		for(j=p1;j>=0;j--)
		for(k=p2;k>=0;k--)
		{
			x=j-num[i][0],y=k-num[i][1];
			if(x>=0&&y>=0&&dp[x][y])
			dp[j][k]+=dp[x][y];
			x=j-num[i][1],y=k-num[i][0];
			if(x>=0&&y>=0&&dp[x][y])
			dp[j][k]+=dp[x][y];
		 } 
	}
	if(dp[p1][p2]!=1)
	cout<<"no"<<endl;
	else
	{
		memset(pre,0,sizeof pre);
		memset(dp,0,sizeof dp);
		dp[0][0]=1;
		for(i=1;i<=scc;i++)
		{
			for(j=p1;j>=0;j--)
			for(k=p2;k>=0;k--)
			{
				if(dp[j][k])
				continue;//这一步很重要,没有这一步会疯狂RE,找了好几遍才发现 
				x=j-num[i][0],y=k-num[i][1];
				if(x>=0&&y>=0&&dp[x][y])
				{
					dp[j][k]+=dp[x][y];
					pre[j][k]=0;
					continue;
				}
				x=j-num[i][1],y=k-num[i][0];
				if(x>=0&&y>=0&&dp[x][y])
				{
					dp[j][k]+=dp[x][y];
					pre[j][k]=1;
					continue;
				}
			}
		}
		x=p1;
		y=p2;
		memset(ans,false,sizeof ans);
		for(i=scc;i>=1;i--)
		{
			for(j=1;j<=n;j++)
			if(p[j]==root[i]&&pre[x][y]==rel[j])
			ans[j]=true;
			if(!pre[x][y])
			x-=num[i][0],y-=num[i][1];
			else
			x-=num[i][1],y-=num[i][0];
		}
		for( i=1;i<=n;i++)
		{
			if(ans[i])
			cout<<i<<endl;
		}
		cout<<"end"<<endl;
	}
//	
	return ;
}
int main()
{
	while(scanf("%d%d%d",&m,&p1,&p2)!=EOF)
	{
		if(m==0&&p1==0&&p2==0)
		break;
		
		int i,j,u,v,w;
		char s[10];
		n=p1+p2;
		memset(num,0,sizeof num);
		for( i=1;i<=n;i++)
		{
			p[i]=i;
			rel[i]=0;
		}
		for( i=1;i<=m;i++)
		{
			scanf("%d%d%s",&u,&v,s);
			w=(s[0]=='n');
			int du=find(u),dv=find(v);
			if(du!=dv)
			{
				p[du]=dv;
				rel[du]=rel[u]^rel[v]^w;
			}
		}
		for( i=1;i<=n;i++)
		p[i]=find(i);
		scc=0;
		for( i=1;i<=n;i++)
		{
			if(p[i]==i)
			{
				scc++;
				root[scc]=i;
				for(j=1;j<=n;j++)
				if(p[j]==i)
				num[scc][rel[j]]++;
			}
		}
		
		 solve();
	}
	return 0;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值