POJ 1417 True Liars 并查集+dp(背包)

题目:http://poj.org/problem?id=1417

另一个地方:https://www.acwing.com/problem/content/description/261/

题意:

一个岛上存在着两种居民,一种是天神,一种是恶魔。

天神永远都不会说假话,而恶魔永远都不会说真话。

岛上的每一个成员都有一个整数编号(类似于身份证号,用以区分每个成员)。

现在你拥有n次提问的机会,但是问题的内容只能是向其中一个居民询问另一个居民是否是天神,请你根据收集的回答判断各个居民的身份。

输入:

输入包含多组测试用例。

每组测试用例的第一行包含三个非负整数n,p1,p2 其中n是你可以提问的总次数,p1是天神的总数量,p2是恶魔的总数量。

接下来n行每行包含两个整数xi,yi以及一个字符串ai,其中xi,yi是岛上居民的编号,你将向编号为xi的居民询问编号为yi的居民是否是天神,

ai是他的回答,如果ai为“yes”,表示他回答你“是”,如果ai为“no”,表示他回答你“不是”。

xi,yi可能相同,表示你问的是那个人自己是否为天神。

当输入为占据一行的“0 0 0”时,表示输入终止。

输出:

对于每组测试用例,如果询问得到的信息足以使你判断每个居民的身份,则将所有天神的编号输出,每个编号占一行,在输出结束后,在另起一行输出“end”,表示该用例输出结束。

如果得到的信息不足以判断每个居民的身份,则输出“no”,输出同样占一行。

 

 

咋一看有2-sat问题内味儿,但是细看就不一样,对于每个居民来说要么是天使要么是恶魔,可能性只有一种,不可能既可以是天使也可以是恶魔,这就和2-sat问题不一样了

对于此题,假如居民回答的是yes,那么这两个居民一定同为天使或者同为恶魔,假如居民回答为no,那么这两个居民一定一个为天使一个为恶魔

想到并查集,使用relat[i]表示 i 与其根节点的关系,relat[i]=0 表示他们是一样的,relat[1]表示他们是不一样的

到这里还不够,因为仅仅通过节点之间是否一样还没办法判断是否有一种唯一的方案可以满足居民的回答

对于某一棵由并查集形成的树,给予其根节点两个属性: sam_siz: 这棵树中和根节点一样的节点有多少个 , dif_siz:这棵树中和根节点不一样的节点有多少个

那么最后天使的数量就是所有并查集形成的树中天使的个数。

但是对于每一棵树,其根节点可以是天使也可以是恶魔,这两种选择需要选择一种。

因此使用dp[i][j]表示前i棵树中,天使数量为j的方案数

只有到最后dp[n][p1]==1 ,才表示有唯一方案满足居民的回答

 

最后一个问题,如何输出天使居民的编号。

我们可以在dp过程中记录路径,对于每个dp[i][j]记录其来源,以及 这个来源的并查集树的根节点是选择了天使还是选择恶魔,那么回溯的时候就可以追溯来源并且追溯到每个来源的根节点是选择了天使还是恶魔,进而知道那棵并查树有哪些节点选择了天使

 

代码:

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<cmath>
#include<vector>
#include<queue>
#include<map>
#define ll long long 
#define ull unsigned long long
using namespace std;
const int INF = 0x3f3f3f3f;
const int maxn = 1007;
// fat:并查集的根节点  relat 关系数组  ,same_siz[i]:以i为根的并查集树和
//i相同的节点有多少 ,dif_siz[i]:和same_siz相对
int fat[maxn], relat[maxn], same_siz[maxn], dif_siz[maxn];
int n, p1, p2;
// dp[第几棵并查集树][天使数] = 方案数
int dp[1005][305]; 
//pre :回溯数组  flag:回溯时的那颗并查集树根节点是天使还是恶魔 
//need[i]: 节点i是天使还是恶魔
int pre[1005][305], flag[1005][305], need[1005];
//V:里面放着并查集树的根
vector<int> V;
void init() {
	memset(dp, 0, sizeof(dp));
	for (int i = 1; i <= p1+p2; i++) {
		fat[i] = i;
		relat[i] = 0;
		same_siz[i] = dif_siz[i] = 0;
	}
	V.clear();
	V.push_back(0); //加了这句是为了后面背包的时候i从1开始
}
int trace(int x) {
	if (fat[x] == x) {
		return x;
	}
	else {
		int ret = trace(fat[x]);
		relat[x] ^= relat[fat[x]];
		return fat[x] = ret;
	}
}
//relat :  0:same   1:different
bool combine(int a, int b, int opt) {
	int f_a = trace(a), f_b = trace(b);
	if (f_a == f_b) {
		if (opt != relat[a] ^ relat[b])
			return 0;
	}
	else {
		fat[f_a] = f_b;
		relat[f_a] = opt ^ relat[a] ^ relat[b];
	}
	return 1;
}
int main() {
	char s[5];
	int inp1, inp2, ok;
	while (cin >> n >> p1 >> p2,n||p1||p2) {
		init();
		ok = 1;
		for (int i = 1; i <= n; i++) {
			scanf("%d %d %s", &inp1, &inp2, s);
			ok = 1;
			if (inp1 == inp2) {
				if (s[0] == 'n') {
					ok = 0;
				}
			}
			else {
				if (combine(inp1, inp2, s[0] == 'n' ? 1 : 0) == 0)
					ok = 0;
			}
		}
		if (ok == 0) {
			cout << "no" << endl;
			continue;
		}
		//下面是记录并查集根节点的同时统计sam_siz and dif_siz
		for (int i = 1; i <= p1+p2; i++) {
			trace(i);
			if (fat[i] == i) {
				V.push_back(i);
			}
			if (relat[i] == 0) {
				same_siz[fat[i]]++;
			}
			else
				dif_siz[fat[i]]++;
		}

		//  背包
		dp[0][0] = 1;
		for (int i = 1; i < V.size(); i++) {
			for (int j = same_siz[V[i]]; j <= p1; j++) {
				if (dp[i - 1][j - same_siz[V[i]]]) {
					dp[i][j] += dp[i - 1][j - same_siz[V[i]]];
					pre[i][j] = j-same_siz[V[i]];//same
					flag[i][j] = 0;//same
				}
			}
			for (int j = dif_siz[V[i]]; j <= p1; j++) {
				if (dp[i - 1][j - dif_siz[V[i]]]) {
					dp[i][j] += dp[i - 1][j - dif_siz[V[i]]];
					pre[i][j] = j - dif_siz[V[i]];
					flag[i][j] = 1;//different
				}
			}
		}

		if (dp[V.size() - 1][p1] == 1) {
			int now = V.size() - 1, siz = p1;
			//先统计好need数组减少时间复杂度
			while (now) {
				need[V[now]] = flag[now][siz];
				siz = pre[now][siz];
				now--;
			}
			//通过need数组来对天使居民输出
			for (int i = 1; i <= p1+p2; i++) {
				if (relat[i] == need[fat[i]])
					cout << i << endl;
			}
			cout << "end" << endl;
		}
		else
			cout << "no" << endl;

	}
	return 0;
}

 

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
题目描述 给出一个$n\times m$的矩阵,每个位置上有一个非负整数,代表这个位置的海拔高度。一开始时,有一个人站在其中一个位置上。这个人可以向上、下、左、右四个方向移动,但是只能移动到海拔高度比当前位置低或者相等的位置上。一次移动只能移动一个单位长度。定义一个位置为“山顶”,当且仅当从这个位置开始移动,可以一直走到海拔高度比它低的位置上。请问,这个矩阵中最多有多少个“山顶”? 输入格式 第一行两个整数,分别表示$n$和$m$。 接下来$n$行,每行$m$个整数,表示整个矩阵。 输出格式 输出一个整数,表示最多有多少个“山顶”。 样例输入 4 4 3 2 1 4 2 3 4 3 5 6 7 8 4 5 6 7 样例输出 5 算法1 (递归dp) $O(nm)$ 对于这道题,我们可以使用递归DP来解决,用$f(i,j)$表示以$(i,j)$为起点的路径最大长度,那么最后的答案就是所有$f(i,j)$中的最大值。 状态转移方程如下: $$ f(i,j)=\max f(x,y)+1(x,y)是(i,j)的下一个满足条件的位置 $$ 注意:这里的状态转移方程中的$x,y$是在枚举四个方向时得到的下一个位置,即: - 向上:$(i-1,j)$ - 向下:$(i+1,j)$ - 向左:$(i,j-1)$ - 向右:$(i,j+1)$ 实现过程中需要注意以下几点: - 每个点都需要搜一遍,因此需要用双重for循环来枚举每个起点; - 对于已经搜索过的点,需要用一个数组$vis$来记录,防止重复搜索; - 在进行状态转移时,需要判断移动后的点是否满足条件。 时间复杂度 状态数为$O(nm)$,每个状态转移的时间复杂度为$O(1)$,因此总时间复杂度为$O(nm)$。 参考文献 C++ 代码 算法2 (动态规划) $O(nm)$ 动态规划的思路与递归DP类似,只不过转移方程和实现方式有所不同。 状态转移方程如下: $$ f(i,j)=\max f(x,y)+1(x,y)是(i,j)的下一个满足条件的位置 $$ 注意:这里的状态转移方程中的$x,y$是在枚举四个方向时得到的下一个位置,即: - 向上:$(i-1,j)$ - 向下:$(i+1,j)$ - 向左:$(i,j-1)$ - 向右:$(i,j+1)$ 实现过程中需要注意以下几点: - 每个点都需要搜一遍,因此需要用双重for循环来枚举每个起点; - 对于已经搜索过的点,需要用一个数组$vis$来记录,防止重复搜索; - 在进行状态转移时,需要判断移动后的点是否满足条件。 时间复杂度 状态数为$O(nm)$,每个状态转移的时间复杂度为$O(1)$,因此总时间复杂度为$O(nm)$。 参考文献 C++ 代码

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值