做题随笔:P10687

Solution

题意

原题链接

岛上有 \(p_1\) 个好人和 \(p_2\) 个坏人按 \(1\)\(p_1 + p_2\) 编号,好人说真话,坏人说谎话。现在有 \(n\) 条信息表示 \(x\)\(y\) 是(\(\texttt{yes}\))不是(\(\texttt{no}\))好人,问:是否能确定所有好人。

多测,\(T \le 10\)\(n \le 1000\)\(p_1,p_2 \le 300\)

分析

对每条信息,我们先考虑:

  1. 如果 \(x\) 是好人,他说 \(\texttt{yes}\)\(y\) 也为好人,否则 \(y\) 为坏人;
  2. 如果 \(x\) 是坏人,他说 \(\texttt{yes}\)\(y\) 也为坏人,否则 \(y\) 为好人;

于是得出:若信息为 \(\texttt{yes}\),则 \(x\)\(y\) 定同为一类人,反之。自然想到并查集。因为要维护是否为同一类人,可以使用带权并查集或拓展域并查集,这里本蒟蒻使用的是拓展域并查集。对此不清楚的可以去 P2024 食物链

现在考虑如何具体确定好人。

其实我们只需考虑好人能否是“唯一”的,也即拼出 \(p_1\) 个人的方案是否唯一。可以直接背包 dp:\(dp(i,j)\) 表示前 \(i\) 个集合中选 \(j\) 个好人的方案数。但是这样有一个小问题:“前 \(i\) 个集合中”可能有一些集合是确定不能合并的。比如:有信息 \(1\) \(2\) \(\texttt{no}\) 的情况下,\(1\)\(2\)所在集合不能同时选中,所以我们加一维:\(dp(i,j,c)\) 表示前 \(i\) 个集合中选 \(j\) 个好人,\(c=1\) 表示当前集合选择,反之,的方案数。

状态转移方程:

\(dp(i,j,1) = \begin{cases} 0,j<\operatorname{card}(S_{i-1})\\ dp(i-1,j-\operatorname{card}(S_{i-1}),0),j \ge\operatorname{card}(S_{i-1}) 且 S_i 与 S_{i-1}不可同时选中\\ dp(i-1,j-\operatorname{card}(S_{i-1}),0)+dp(i-1,j-\operatorname{card}(S_{i-1}),1),j \ge\operatorname{card}(S_{i-1}) 且 S_i 与 S_{i-1}可同时选中 \end{cases}\)

\(dp(i,j,0) = dp(i-1,j,0)+dp(i-1,j,1)\)

最后,如果 \(dp(cnt,p_1,1)+dp(cnt,p_1,0)=1\),则可以确定,再逆向转移一遍,标记答案,输出即可。

实现

  1. 维护一个 \(2\) 倍拓展域并查集;
  2. 把分好的集合“离散化”,跑背包 dp。

Code

#include <iostream>
#include <cstdio>
#include <cctype>
#include <algorithm>
#include <vector>
#include <cstring>

using namespace std;

typedef long long ll;

inline ll fr() {
	ll x=0,f=1;char c=getchar();
	while(!isdigit(c)) {
		if(c=='-') f=-1;
		c=getchar();
	}
	while(isdigit(c)) {
		x=(x<<3)+(x<<1)+(c^48);
		c=getchar();
	}
	return x*f;
}

const int maxn=6.6e2;

int n,p1,p2;

int g[maxn],card[maxn],gf[maxn];
//g:离散化后的集合编号
//gf:从离散化后编号到原集合的映射
bool vis[maxn],ans[maxn];
int dp[maxn][maxn][2];

int f[maxn*2];

int getf(int x) {return x==f[x]?f[x]:f[x]=getf(f[x]);}

inline void merge(int x,int y) {f[getf(y)]=getf(x);}

int main() {
	while(1) {
		n=fr();p1=fr();p2=fr();
		if(!n&&!p1&&!p2) break;
		int N=p1+p2,cnt=0;
		memset(vis,0,sizeof(vis));
		memset(ans,0,sizeof(ans));
		memset(card,0,sizeof(card));
		memset(g,0,sizeof(g));
		memset(gf,0,sizeof(gf));
		memset(dp,0,sizeof(dp));//多测不清空,。。。。
		for(register int i = 1; i <= 2*N; i++) f[i]=i;
		for(register int i = 1; i <= n; i++) {
			int x=fr(),y=fr();
			char ans='#';
			while(!(ans=='n'||ans=='y')) ans=getchar();//避免读到神秘的东西
			switch (ans) {
				case 'n':
					merge(x,y+N);
					merge(y,x+N);
					break;
				case 'y':
					merge(x,y);
					merge(x+N,y+N);
					break;
			}
		}
		for(register int i = 1; i <= N; i++) {
			if(!vis[getf(i)]) {
				vis[getf(i)]=1;
				g[getf(i)]=++cnt;
				card[cnt]++;
				gf[cnt]=getf(i);
			}
			else card[g[getf(i)]]++;
		}
		dp[0][0][0]=1;//一定要记得初始化!!!
		for(register int i = 1; i <= cnt; i++) {
			for(register int j = 0; j <= N; j++) {
				dp[i][j][0]=dp[i-1][j][0]+dp[i-1][j][1];
				if(j>=card[i]) {
					dp[i][j][1]=dp[i-1][j-card[i]][0];
					if(getf(gf[i])!=getf(gf[i-1]+N)) {//判断两集合是否可以同时选中
						dp[i][j][1]+=dp[i-1][j-card[i]][1];
					}
				}
			}
		}
		if(dp[cnt][p1][0]+dp[cnt][p1][1]!=1) printf("no\n");
		else {
			int now=p1;
			for(register int i = cnt; i; i--) {//反着跑一遍,标记
				if(dp[i-1][now][0]==0) {
					now-=card[i];ans[i]=1;
				}
			}
			for(register int i = 1; i <= N; i++) {
				if(ans[getf(i)]) printf("%d\n",i);
			}
			printf("end\n");
		}
	}
	return 0;
}

闲话

如果觉得有用,点个赞吧!

posted @ 2025-05-07 16:10  Tenil  阅读(19)  评论(0)    收藏  举报