题目传送门
题目大意:
给你两个神圣种族和邪恶种族的人数以及询问次数,其中神圣种族的人说真话,邪恶种族的人说假话,请你判断那几个是神圣种族的人。
Solution:
题解区已经有很多带边权并查集的做法了,这里我用的是扩展域并查集。
首先我们来看,询问时是有逻辑的,分为三种情况:
- 当 \(x\) 说 \(y\) 是神圣种族时,因为当 \(x\) 为邪恶种族时会说假话,则 \(y\) 也是邪恶种族;当 \(x\) 为神圣种族时会说真话,则 \(y\) 也是神圣种族,所以此时 \(x\) 与 \(y\) 同一种族。
- 当 \(x\) 说 \(y\) 是邪恶种族时,因为当 \(x\) 为邪恶种族时会说假话,则 \(y\) 是神圣种族;当 \(x\) 为神圣种族时会说真话,则 \(y\) 是邪恶种族,所以此时 \(x\) 与 \(y\) 不是同一种族。
- 当 \(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;elses[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);elsemerge(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;
}