做题随笔: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\)。
分析
对每条信息,我们先考虑:
- 如果 \(x\) 是好人,他说 \(\texttt{yes}\) 则 \(y\) 也为好人,否则 \(y\) 为坏人;
- 如果 \(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\),则可以确定,再逆向转移一遍,标记答案,输出即可。
实现
- 维护一个 \(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;
}
闲话
如果觉得有用,点个赞吧!