Programming Challenges 习题10.5.5

本文深入解析了一道经典的并查集算法题,探讨了如何根据输入信息将N个人划分成若干等价类,详细解释了朋友和敌人关系的处理策略,以及如何使用并查集进行高效计算。

PC/UVa:111005/10158

War

爸爸的爸爸是爷爷,妈妈的妈妈是姥姥。。。。。。

一共N个人,每组输入代表将xy设置为朋友,或者设置为敌人,或者查询是否为朋友、或者查询是否为敌人。

根据题目中提供的二元关系定义,朋友关系是等价关系:

  • 朋友是自反的,自己是自己的朋友
  • 朋友是对称的,朋友是相互的
  • 朋友是传递的,朋友的朋友是朋友

敌人关系满足对称性和反自反性:

  • 敌人是对称的,敌人是相互的
  • 敌人是反自反的,没有人和自己是敌人

此外还有:

  • 敌人的敌人是朋友
  • 敌人的朋友是敌人
  • 朋友的敌人是敌人

题意是根据输入信息,将N个人划分成若干等价类,这题属于并查集的经典题,不过之前没见过,写了好久。并查集就是通过集合的求并查找操作,最终划分成若干等价类。一般最常用的优化方法就是在查找时进行路径压缩,至于根据深度和集合大小进行优化的合并操作,我看网上也没什么人用。

书上给了一个通过Floyd算法计算人与人之间是否有给定关系的提示,但是放到这道题中根本没法用,总不能每添加一个关系,就去跑一遍Floyd吧?即使只把受影响的部分更新一下,稍微快一些,但是UVa上依然是超时的。

网上关于这道题的并查集代码都是一模一样的,或许只有这么些才能过吧?不过我还是自己写了一份不一样的。

并查集,也叫不相交集类,用来划分等价关系。题目中只有朋友关系是等价关系,使用viFriend来表示等价类的结果,其中第i个元素表示i所属等价类的leader,当两个人的leader相同时,即属于同一等价类,初始时每个人都是自己的leaderviEnemy用来表示每个等价类的敌人,由于朋友的敌人是敌人这条规则,敌人(们)是相对于等价类而言的,所以viEnemy中只有leader对应的元素有意义,表示整个等价类的敌人是哪个等价类,初始时全部为-1,即自己和自己不是敌人。

设置两个人是朋友时,首先要保证两个人所处的等价类不能是敌人关系,然后根据这两个等价类是否已经有了敌人,分为3种情况处理:

  • 都已经有了敌人,将两个朋友等价类合并,也将两个敌人等价类合并(朋友的敌人是敌人),并将被合并的等价类的敌人设置为-1
  • 只有一个有敌人,将没有敌人的等价类合并到有敌人的等价类中
  • 都没有敌人,简单的将两个朋友等价类合并即可

设置两个人是敌人时,首先要保证两个人所处的等价类不能是朋友关系,然后根据这两个等价类(用xLeaderyLeader表示)是否有了敌人(用xEnemyyEnemy表示),分成3种情况处理:

  • 都已经有了敌人,即各自有各自的敌人,将xLeaderyEnemy合并,将yLeaderxEnemy合并(敌人的敌人是朋友)。由于合并导致xLeaderyLeader不再是等价类的代表,所以其敌人应该置为-1,同时设置xEnemyyEnemy的敌人
  • 只有一个有敌人,将没有敌人的等价类和另一个敌人等价类合并
  • 都没有敌人,简单的将两个等价类互相置为敌人
#include <iostream>
#include <vector>

using namespace std;

int findSet(vector<int> &viSet, const int x)
{
	if (viSet[x] == x) return x;
	else{
		viSet[x] = findSet(viSet, viSet[x]);
		return viSet[x];
	}
}

void setFriend(vector<int> &viFriend, vector<int> &viEnemy, const int x, const int y)
{
	int xLeader = findSet(viFriend, x);
	int yLeader = findSet(viFriend, y);
	int xEnemy = viEnemy[xLeader];
	int yEnemy = viEnemy[yLeader];
	if (xLeader == yEnemy || yLeader == xEnemy){
		cout << -1 << endl;
		return;
	}
	if(xEnemy != -1 && yEnemy != -1){
		//两个集合都有敌人,合并
		viFriend[yLeader] = xLeader;
		viFriend[yEnemy] = xEnemy;
		viEnemy[yLeader] = -1;//还原
		viEnemy[yEnemy] = -1;
		//没有这两行则同一集合无法合并
		viEnemy[xLeader] = xEnemy;
		viEnemy[xEnemy] = xLeader;
	}
	else if (xEnemy != -1){
		//一个集合有敌人
		viFriend[yLeader] = xLeader;
	}
	else if (yEnemy != -1){
		viFriend[xLeader] = yLeader;
	}
	else{
		//两个集合都没有敌人
		viFriend[yLeader] = xLeader;
	}
}

void setEnemy(vector<int> &viFriend, vector<int> &viEnemy, const int x, const int y)
{
	int xLeader = findSet(viFriend, x);
	int yLeader = findSet(viFriend, y);
	int xEnemy = viEnemy[xLeader];
	int yEnemy = viEnemy[yLeader];
	if (xLeader == yLeader){
		cout << -1 << endl;
		return;
	}
	if (xEnemy != -1 && yEnemy != -1){
		//两个集合都有敌人,加入到另一个的敌人中
		viFriend[xLeader] = yEnemy;
		viEnemy[xLeader] = -1;
		viEnemy[yEnemy] = xEnemy;
		viFriend[yLeader] = xEnemy;
		viEnemy[yLeader] = -1;
		viEnemy[xEnemy] = yEnemy;
	}
	else if (xEnemy != -1){
		viFriend[yLeader] = xEnemy;
	}
	else if (yEnemy != -1){
		viFriend[xLeader] = yEnemy;
	}
	else{
		viEnemy[xLeader] = yLeader;
		viEnemy[yLeader] = xLeader;
	}
}

int main()
{
	int N = 0;
	cin >> N;
	vector<int> viFriend, viEnemy;
	for (int n = 0; n < N; n++)
	{
		viFriend.push_back(n);
		viEnemy.push_back(-1);
	}
	int c, x, y;
	while (cin >> c >> x >> y){
		if (c == 0 && x == 0 && y == 0) break;
		switch (c)
		{
		case 1:
			setFriend(viFriend, viEnemy, x, y);
			break;
		case 2:
			setEnemy(viFriend, viEnemy, x, y);
			break;
		case 3:
			if (findSet(viFriend, x) == findSet(viFriend, y)){
				cout << 1 << endl;
			}
			else cout << 0 << endl;
			break;
		case 4:
			if (findSet(viFriend, x) == viEnemy[findSet(viFriend, y)] ||
				findSet(viFriend, y) == viEnemy[findSet(viFriend, x)]){
				cout << 1 << endl;
			}
			else cout << 0 << endl;
			break;
		default:
			break;
		}
	}
	return 0;
}
/*
10
1 0 1
1 1 2
2 0 5
3 0 2
3 8 9
4 1 5
4 1 2
4 8 9
1 8 9
1 5 2
3 5 2
0 0 0
*/

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值