P10687 True Liars 个人题解

题目传送门

题目大意:

给你两个神圣种族和邪恶种族的人数以及询问次数,其中神圣种族的人说真话,邪恶种族的人说假话,请你判断那几个是神圣种族的人。

Solution:

题解区已经有很多带边权并查集的做法了,这里我用的是扩展域并查集。

首先我们来看,询问时是有逻辑的,分为三种情况:

  1. \(x\)\(y\) 是神圣种族时,因为当 \(x\) 为邪恶种族时会说假话,则 \(y\) 也是邪恶种族;当 \(x\) 为神圣种族时会说真话,则 \(y\) 也是神圣种族,所以此时 \(x\)\(y\) 同一种族。
  2. \(x\)\(y\) 是邪恶种族时,因为当 \(x\) 为邪恶种族时会说假话,则 \(y\) 是神圣种族;当 \(x\) 为神圣种族时会说真话,则 \(y\) 是邪恶种族,所以此时 \(x\)\(y\) 不是同一种族。
  3. \(x\)\(x\) 是神圣种族时,因为无论 \(x\) 是神圣种族还是邪恶种族,都会说自己是神圣种族的,此时该询问无意义,可以跳过。

然后我们根据结论对并查集设置两个域,第一个域为同族域(\(1\)\(p1+p2\)),第二个域为异族域(\(p1+p2+1\)\(2(p1+p2)\)),我们每读进来一个就把其同族异族关系通过并查集合并,然后得到每个人的同族和异族。然后我们需要确定到底那些是神圣种族哪些是邪恶种族,可以发现:第 \(i\) 个人所在的集合是神圣种族时,与 \(i+p1+p2\) 处于一个集合的都是邪恶种族,即 \(fa_{j}(1\le i\le p1+p2,j\neq i)=fa_{i}\) 的都是神圣种族,\(fa_{j}(p1+p2+1\le i\le 2(p1+p2),j\neq i+p1+p2)=fa_{i+p1+p2}\) 的都是邪恶种族,而当第 \(i\) 个人所在集合是邪恶种族时同理。所以这个集合是什么种族是由其 \(fa\) 时好时坏决定的,我们用一个 \(a\) 数组表示集合的 \(fa\) 是神圣种族时神圣种族人数,用 \(b\) 数组表示集合的 \(fa\) 是邪恶种族时神圣种族人数。通过这个我们可以用一个背包 DP 来解决神圣种族有哪些人。

首先我们定义 \(dp_{i,j}\) 表示满足前 \(i\) 个集合有 \(j\) 个好人的方法数,转移方程就为:
\(dp_{i,j}=dp_{i,j}+\begin{cases}dp_{i-1,j-a_{i}} & a_{i}\le j\\dp_{i-1,j-b_{i}} & b_{i}\le j\end{cases}\) 初始状态为 \(dp_{0,0}=1\) 表示没有人时也算一种方案,最终状态为 \(dp_{cnt,p1}=1\),其中 \(cnt\) 为划分的集合数,如果最终状态不等于 \(1\),那么有多种方案,此时无解;否则我们就可以回溯找 \(dp_{i,j}\) 在何时发生转移的,并标记下此时的神圣种族最后枚举一遍输出就行(此时保证字典序输出)

代码:

#include<bits/stdc++.h>
using namespace std;
const int N=1500;
inline int read(){
	int x=0,f=1;
	char c=getchar();
	while(c<'0' || c>'9'){
		if(c=='-')
			f=-1;
		c=getchar();
	}
	while(c>='0' && c<='9'){
		x=x*10+c-'0';
		c=getchar();
	}
	return x*f;
}
int n,p1,p2,cnt,fa[N],dp[N][N],h[N],s[N],st[N],a[N],b[N];
//cnt表示集合数量,fa[i]表示i的祖先,dp[i][j]表示前i个集合有j个神圣种族的方法数
//h[i]是在按秩合并是判断大小所用的,s[i]表示i所在集合的大小,st[i]表示i所在的集合
//a[i]表示fa[i]是神圣种族时神圣种族人数,b[i]表示fa[i]是邪恶种族时神圣种族人数
inline int sfind(int x){
	if(x==fa[x])
		return x;
	return fa[x]=sfind(fa[x]);
}
inline void merge(int x,int y){//并查集->按秩合并 
	int fx=sfind(x),fy=sfind(y);
	if(fx==fy)
		return;
	if(h[fx]>h[fy])
		s[fx]+=s[fy],fa[fy]=fx;
	else if(h[fx]<h[fy])
		s[fy]+=s[fx],fa[fx]=fy;
	else
		s[fx]+=s[fy],fa[fy]=fx,h[fx]++;
}
int main(){
	while(n=read(),p1=read(),p2=read()){
		if(!n && !p1 && !p2)
			break;
		int m=p1+p2;
		for(int i=1;i<=m;i++){
			fa[i]=i,fa[i+m]=i+m;
			s[i]=h[i]=h[i+m]=1; 
			s[i+m]=st[i]=st[i+m]=0;
		}
		for(int i=1;i<=n;i++){
			int x=read(),y=read();
			string c;
			cin>>c;
			if(c=="no")
				merge(x,y+m),merge(x+m,y);
			else
				merge(x,y),merge(x+m,y+m);
		}
		memset(dp,0,sizeof(dp));
		dp[0][0]=1;
		cnt=0;
		for(int i=1;i<=m;i++){
			int father=sfind(i);
			if(father!=i)
				continue;
			st[father]=st[father+m]=++cnt;
			a[cnt]=s[father];
			b[cnt]=s[father+m];
		}
		for(int i=1;i<=cnt;i++){//枚举集合 
			for(int j=min(a[i],b[i]);j<=p1;j++){//枚举神圣种族的人数 
				if(j>=a[i]) 
					dp[i][j]+=dp[i-1][j-a[i]];
				if(j>=b[i])
					dp[i][j]+=dp[i-1][j-b[i]];
			}
		} 
		if(dp[cnt][p1]!=1){//如果方案数不唯一退出 
			puts("no");
			continue;
		}
		for(int i=cnt;i;i--){//回溯寻找什么时候发生了转移 
			if(dp[i-1][p1-a[i]])
				p1-=a[i],a[i]=-1;
			if(dp[i-1][p1-b[i]])
				p1-=b[i],a[i]=-2;
		}
		for(int i=1;i<=m;i++){//枚举所有人找到神圣种族的人 
			int father=sfind(i);
			if(st[father]){
				if((father>m && a[st[father]]==-2) || (father<=m && a[st[father]]==-1))
					printf("%d\n",i);
			}	
		}
		puts("end");
	}
	return 0;
} 
posted @ 2025-11-23 17:34  See_you_soon  阅读(6)  评论(0)    收藏  举报