True Liars (POJ - 1417)带权并查集+dp路径

107 阅读6分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第15天,点击查看活动详情

题目:

中文题意:一个村庄有两类人,好人坏人, 好人总是说真话, 坏人总是说假话, 给你n个询问和好人、坏人的数量p q, 每个询问 x y yes/no, 表示 x 说 y 是 好/坏人。问是否能够唯一确定哪些是好人, 哪些是坏人, 如果可以输出好人的序号以"end"结尾, 否则输出"no"

分析:我们简单地分析能够发现,当一个人说另一个人是好人的时候有两种情况,第一种情况是第一个人是好人,那么第二个人也是好人,第二种情况就是第一个人是坏人,那么他说的是假话,则第二个人也是坏人,不管怎样,他们总是属于同一类人。同样的,当一个人说另一个人是坏人的时候也会分两种情况,第一种情况是第一个人是好人,那么第二个人就是坏人,如果第一个人是坏人,由于他说的是假话,那么第二个人就是好人,他们总是属于不同类别的人。知道这个结论我们就可以对题目中所给的条件进行分类了,这个显然是用并查集。但是大家有没有发现一个问题?就是无论是上述哪种情况,我们都无法直接确定哪一类人是好人,哪一类人是坏人。但最起码我们可以知道他们属于同一类还是不同类,那我们如何唯一确定哪些是好人呢?举个简单点的例子来说吧,我们现在有两个集合,一个集合中包含4,6(代表这个集合中其中4个人是一类,另外6个人是一类),另一个集合中有7和9,显然每个集合中都会有一类是好人,一类是坏人,现在告诉我们有15个人是好人,我们能知道哪些人是好人吗?显然只能是第一个集合的6人和第二个集合的9人是好人,而假如好人有13个呢?这个时候就会有两种情况,第一种情况是第一个集合中的6人和第二个集合中的7人是好人,第二种情况就是第一个集合中的4人和第二个集合中的9人是好人,所以这个时候就不能确定谁是好人谁是坏人。而这个问题抽象成数学问题就是,假如我们有n个好人,m个集合,我们必须从每个集合中的两个数中挑选一个,看最后能够组成n的情况数是否唯一,而这个问题我们可以类似于背包问题去解决它,其中dp[i][j]表示从前i个集合中挑选j个好人的情况数。

这道题目的大致思路就是这样,下面我来说一下具体的一些细节:

第一个就是我们如何在一个集合中存储两类人,这个就是一个带权并查集的操作了,并查集大家肯定都不陌生,它可以帮我们把有关联的人放一块,每个集合中都会有一个代表元素,我们可以假设与根节点的距离为奇数的节点代表与根节点不同类的人,而与根节点之间的距离为偶数的点来表示与根节点同一类的人,这样我们就可以在一个集合中存储两类人,这部分具体的实现代码如下:

int find(int x)
{
	int tmp=fu[x];
	if(x==tmp) return x;
	fu[x]=find(tmp);
	d[x]=(d[x]+d[tmp])%2;//更新距离,利用其直接父类进行更新 
	return fu[x];
}
void unite(int x,int y,int z)
{
	int f1=find(x),f2=find(y);
	if(f1!=f2)
	{
		fu[f2]=f1;
		d[f2]=(d[x]+d[y]+z)%2;//f2到f1的距离是由x和y的距离以及y和f2的关系以及x和f1的关系决定的 
	}
}

带权并查集的查找过程我就不作赘述了,就是每个节点岛根节点的距离都利用其父节点到根节点的距离来进行更新,我主要说一下两个集合合并的时候应该怎样处理,看这个到吗就可以看出,我显然是要让f1做合并后的集合的代表元素,那么fu[f2]=f1比较容易理解,关键是 d[f2]=(d[x]+d[y]+z)%2这条语句的理解,d[f2]表示待合并的根节点到合并后的集合的根节点之间的距离,f2到f1的距离是由x和y的距离以及y和f2的关系以及x和f1的关系决定的 image.png 如图所示:

如果x到y的距离是奇数(代表x和y不同类),即x到根节点r1的距离与y到根节点r2的距离奇偶性不同,即有(b+z)%2==(a+h)%2,那么h=b+z-a,这样就解决了带权并查集的问题。

剩下的就是路径输出问题了,我们知道,每次必然要从一个集合中选出一类作为好人,我们在dp的过程中就可以记录路径,这样最后我们直接在倒序遍历过程中存入答案即可。不要忘记对答案进行排序。

下面是代码:

#include<cstdio>
#include<iostream>
#include<cstring>
#include<vector>
#include<algorithm>
using namespace std;
const int N=1003;
int fu[N],d[N];//d[i]=0/1,表示第i个节点与其根节点(是/不是)一类人 
int cnt[N][2];//cnt[i][0/1]表示与第i个节点属于(同一类/不同类)的个数 
vector<int> s[N][2];//s[i][0/1]存储与第i个节点属于同一类/不同类的点 
int dp[N][N];//dp[i][j]记录前i个集合中有j个好人的方案数 
int pre[N][N];//记录背包中选取的物品 
bool vis[N];//记录是否出现在集合中 
int find(int x)
{
	int tmp=fu[x];
	if(x==tmp) return x;
	fu[x]=find(tmp);
	d[x]=(d[x]+d[tmp])%2;//更新距离,利用其直接父类进行更新 
	return fu[x];
}
void unite(int x,int y,int z)
{
	int f1=find(x),f2=find(y);
	if(f1!=f2)
	{
		fu[f2]=f1;
		d[f2]=(d[y]+z-d[x]+2)%2;//f2到f1的距离是由x和y的距离以及y和f2的关系以及x和f1的关系决定的 
	}
}
int main()
{
	int n,p,q;
	while(scanf("%d%d%d",&n,&p,&q)&&(n!=0||p!=0||q!=0))
	{
		for(int i=1;i<=p+q;i++)//初始化 
		{
			fu[i]=i;
			d[i]=cnt[i][0]=cnt[i][1]=0;
			vis[i]=false;
			s[i][0].clear();
			s[i][1].clear();
		}
		int x,y;
		char o[5];
		for(int i=1;i<=n;i++)
		{
			scanf("%d%d%s",&x,&y,o);
			if(o[0]=='y') unite(x,y,0);//yes代表x和y是一类人 
			else unite(x,y,1);//no代表x和y不是一类人
		}
        int count=0;//记录集合的个数(每个集合中有两个小集合,一个是好人,一个是坏人) 
		for(int i=1;i<=p+q;i++)
		{
			if(!vis[i])
			{
				int t=find(i);
				for(int j=i;j<=p+q;j++)
				{
					if(find(j)==t)
					{
						vis[j]=true;
						s[count+1][d[j]].push_back(j);
						cnt[count+1][d[j]]++;
					}
				}
				count++;
			}
		}
		memset(dp,0,sizeof(dp));
        dp[0][0]=1;
        for(int i=1;i<=count;i++)
		{
			for(int j=p;j>=0;j--)
			{
				if(j>=cnt[i][0]&&dp[i-1][j-cnt[i][0]])//必须要保证dp[i-1][j]存在
				{
					dp[i][j]+=dp[i-1][j-cnt[i][0]];
					pre[i][j]=cnt[i][0];
				}
				if(j>=cnt[i][1]&&dp[i-1][j-cnt[i][1]])
				{
					dp[i][j]+=dp[i-1][j-cnt[i][1]];
					pre[i][j]=cnt[i][1];
				}
			}
		}
		vector<int> ans;//记录选取的好人集合 
		ans.clear();//多组输入,不要忘记清空答案数组
		if(dp[count][p]!=1)
			puts("no");
		else
		{
			while(count)
			{
				if(cnt[count][0]==pre[count][p])//必须从每个集合中选出一个集合作为好人 
				{
					p-=cnt[count][0];
					for(int i=0;i<cnt[count][0];i++)
						ans.push_back(s[count][0][i]);
				}
				else
				{
					p-=cnt[count][1];
					for(int i=0;i<cnt[count][1];i++)
						ans.push_back(s[count][1][i]);
				}
				count--;
			}
			sort(ans.begin(),ans.end());
			for(int i=0;i<ans.size();i++)
				printf("%d\n",ans[i]);
			puts("end");
		}
	}
	return 0;
}