一本通 3.4.4 并查集

1346:【例4-7】亲戚(relation)

【题目描述】

或许你并不知道,你的某个朋友是你的亲戚。他可能是你的曾祖父的外公的女婿的外甥女的表姐的孙子。如果能得到完整的家谱,判断两个人是否是亲戚应该是可行的,但如果两个人的最近公共祖先与他们相隔好几代,使得家谱十分庞大,那么检验亲戚关系实非人力所能及。在这种情况下,最好的帮手就是计算机。为了将问题简化,你将得到一些亲戚关系的信息,如Marry和Tom是亲戚,Tom和Ben是亲戚,等等。从这些信息中,你可以推出Marry和Ben是亲戚。请写一个程序,对于我们的关于亲戚关系的提问,以最快的速度给出答案。

【题目分析】

一般并查集的执行过程,查find函数带路径压缩,并union函数将左侧归顺右侧,查询功能如果两个点的祖先不是同一个输出No,否则输出Yes
特别的:输入输出数据较大,使用scanf与printf函数

【代码实现】

#include<bits/stdc++.h>
using namespace std;

int fa[20005], x, y;

int get(int a) {
	return fa[a] == a ? a : fa[a] = get(fa[a]);
}
void join(int a, int b) {
	int fa_a = get(a);
	int fa_b = get(b);
	if (fa_a != fa_b) fa[fa_a] = fa_b;
}
int main() {
	int n, m;
	cin >> n >> m;
	for (int i = 1; i <= n; i++) {
		fa[i] = i;
	}
	for (int i = 1; i <= m; i++) {
		scanf("%d%d", &x, &y);
		join(x, y);
	}
	cin >> m;
	for (int i = 1; i <= m; i++) {
		scanf("%d%d", &x, &y);
		if (get(x) == get(y)) printf("Yes\n");
		else printf("No\n");
	}
	return 0;
}

1347:【例4-8】格子游戏

【题目描述】

Alice和Bob玩了一个古老的游戏:首先画一个n × n的点阵(下图n = 3)

接着,他们两个轮流在相邻的点之间画上红边和蓝边:

直到围成一个封闭的圈(面积不必为1)为止,“封圈”的那个人就是赢家。因为棋盘实在是太大了(n ≤ 200),他们的游戏实在是太长了!他们甚至在游戏中都不知道谁赢得了游戏。于是请你写一个程序,帮助他们计算他们是否结束了游戏? 

【题目分析】

并查集找环:将一条边进行合并时,首先判断边的两个端点是否属于同一个集合,如果是,已经存在环,否则不存在环,合并两点的祖先。

特别的:本题中的对象不是单个数值是点数据,需要定义点的结构体进行存储,初始化将二维数组的编号与存储的点对应相等,判断时需要同时满足x、y都相等才说明祖先相同(也可以使用pair存储)。

【代码实现】

#include <bits/stdc++.h>
using namespace std;

int n, m;
struct Point {
	int x;
	int y;
} father_p[205][205], p1, p2;

Point find(Point p) {
	int x = p.x;
	int y = p.y;
	if (father_p[x][y].x == x && father_p[x][y].y == y) return p;
	else {
		return father_p[x][y] = find(father_p[x][y]);
	}
}
void unionn(Point p1, Point p2) {
	Point fa_p1 = find(p1);
	Point fa_p2 = find(p2);
	if (fa_p1.x != fa_p2.x || fa_p1.y != fa_p2.y) {
		int x = p2.x;
		int y = p2.y;
		father_p[x][y] = fa_p1;
	}
}
int main() {
	//input data
	//clock_t s = clock();
	cin >> n >> m;
	//init
	for (int i = 1; i <= n; i++)
		for (int j = 1; j <= n; j++) {
			father_p[i][j].x = i;
			father_p[i][j].y = j;
		}

	for (int i = 1; i <= m; i++) {
		int x, y;
		char c;
		cin >> x >> y >> c;
		if (c == 'D') {
			Point pp;
			pp.x = x;
			pp.y = y;
			p1 = find(pp);
			pp.x = x + 1;
			pp.y = y;
			p2 = find(pp);
		}
		if (c == 'R') {
			Point pp;
			pp.x = x;
			pp.y = y;
			p1 = find(pp);
			pp.x = x;
			pp.y = y + 1;
			p2 = find(pp);
		}

		if (p1.x == p2.x && p1.y == p2.y) {
			cout << i << endl;
			return 0;
		} else {
			unionn(p1, p2);
		}
	}
	cout << "draw" << endl;

	//output time
	//cout <<endl<< clock() - s<<endl;
	return 0;
}

1385:团伙(group)

【题目描述】

在某城市里住着n个人,任何两个认识的人不是朋友就是敌人,而且满足:

1、我朋友的朋友是我的朋友;

2、我敌人的敌人是我的朋友;

所有是朋友的人组成一个团伙。告诉你关于这n个人的m条信息,即某两个人是朋友,或者某两个人是敌人,请你编写一个程序,计算出这个城市最多可能有多少个团伙?

【题目分析】

扩展域并查集:将点x进行分解,x表示x本身,x+n表示x的敌人,y表示y本身,y+n表示y的敌人,如果x与y时朋友需要将x和y合并,x+n与y+n合并,如果x与y是敌人,需要将x+n与y合并,x与y+n合并。

特别的:定义祖先数组时需要扩展2倍,判断团伙数量只需要查找fa[i]==i的数量

【代码实现】

#include<bits/stdc++.h>
#define N 100001
using namespace std;
int f[N], d[N], n, m;
int find(int x) {
	return f[x] == x ? x : f[x] = find(f[x]);
}
void fun(int x, int y) {
	x = find(x);
	y = find(y);
	if (x != y)
		f[y] = x;
}
int main() {
	cin >> n >> m;
	for (int i = 1; i <= n * 2; i++) {
		f[i] = i;
	}
	for (int i = 1; i <= m; i++) {
		int z, x, y;
		cin >> z >> x >> y;
		if (z == 0)fun(x, y);
		else if (z == 1)fun(x + n, y), fun(x, y + n);
	}
	int sum = 0;
	for (int i = 1; i <= n; i++) {
		int z = find(i);
		if (!d[z]) {
			d[z] = 1;
			sum++;
		}
	}
	cout << sum;
	return 0;
}

1386:打击犯罪(black)

【题目描述】

某个地区有n(n<=1000)个犯罪团伙,当地警方按照他们的危险程度由高到低给他们编号为1-n,他们有些团伙之间有直接联系,但是任意两个团伙都可以通过直接或间接的方式联系,这样这里就形成了一个庞大的犯罪集团,犯罪集团的危险程度由集团内的犯罪团伙数量唯一确定,而与单个犯罪团伙的危险程度无关(该犯罪集团的危险程度为n)。现在当地警方希望花尽量少的时间(即打击掉尽量少的团伙),使得庞大的犯罪集团分离成若干个较小的集团,并且他们中最大的一个的危险程度不超过n/2。为达到最好的效果,他们将按顺序打击掉编号1到k的犯罪团伙,请编程求出k的最小值。

【题目分析】

带权并查集
正向思维:使用邻接矩阵存储连边关系,从1~n枚举去掉1-k的点(k的范围从1-n),查询最大集合点数是否不超过n/2,则为答案,每次枚举都要重置并查集,时间复杂度:O(n^3) 
逆向思考:n ~ 1枚举,每次把K加入图中,也就是删除1 ~ K-1,剩余k ~ n,若最大集合点不超过N/2这个方案可行还可能更小,一旦不满足就是不能再加K,即K 要删掉,就输出答案。时间复杂度:O(n^3)

【代码实现】

#include <bits/stdc++.h>
using namespace std;

int n, m;
int father[1005];
int group_num[1005];
vector<int > group[1005];

int find(int x) {
	if (father[x] != x)
		father[x] = find(father[x]); //路径压缩
	return  father[x];
}
void unionn(int x, int y) {
	int fa_x = find(x);
	int fa_y = find(y);
	if (fa_x != fa_y) {
		father[fa_y] = fa_x;
		int nfx = group_num[fa_x];
		int nfy = group_num[fa_y];
		group_num[fa_x] += nfy;
		group_num[fa_y] += nfx;
	}

}
int main() {
	cin >> n;
	for (int i = 1; i <= n; i++) {
		father[i] = i;
		group_num[i] = 1;
	}
	for (int i = 1; i <= n; i++) {
		int p, x;
		cin >> p;
		group[i].push_back(p);
		for (int j = 1; j <= p; j++) {
			cin >> x;
			group[i].push_back(x);
		}
	}

	for (int i = n; i >= 1; i--) {
		for (int j = 1; j < (int)group[i].size(); j++) {
			if (group[i][j] >= i) {
				unionn(group[i][j], i);
				for (int k = 1; k <= n; k++) {
					if (group_num[k] > (n >> 1)) {
						cout << i;
						return 0;
					}
				}
			}
		}
	}

	return 0;
}

1387:搭配购买(buy)

【题目描述】

Joe觉得云朵很美,决定去山上的商店买一些云朵。商店里有n朵云,云朵被编号为1,2,…,n,并且每朵云都有一个价值。但是商店老板跟他说,一些云朵要搭配来买才好,所以买一朵云则与这朵云有搭配的云都要买。

但是Joe的钱有限,所以他希望买的价值越多越好。

【题目分析】

带权并查集:使用并查集算出每个云朵集合的最大价值和花费,使用0-1背包进行问题求解

定义fa[]数组存储云朵的祖先,value[]数组存储云朵所在集合的价值,money[]数组存储云朵所在集合的花费.
使用并查集合并云朵集合,得到每个集合的价值和花费,例如将a合并到b中,yuan[fa[b]].value+=yuan[fa[a]].value    yuan[fa[b]].money +=yuan[fa[a]].money
检查所有并查集获得云朵集合的价值value[]和费用money[],个数为cnt,总钱数为w,使用0-1背包求解最大价值dp[w]
for(i=1;i<=cnt;i++)
     for(j=w;j>=dc[i];j--)
          dp[j]=max(dp[j],dp[j-money[i]]+value[i])

【代码实现】

#include <bits/stdc++.h>
using namespace std;

int n, m, w;
const int maxn = 10005;
int money[maxn], value[maxn], dp[maxn];
struct Yun {
	int father;
	int money;
	int value;
} yun[maxn];

int find(int y) {
	if (yun[y].father != y) {
		yun[y].father = find(yun[y].father);
	}
	return yun[y].father;
}

bool cmp(const Yun& a, const Yun& b) {
	return a.value > b.value;
}
int main() {
	//input data
	//clock_t s = clock();
	cin >> n >> m >> w;

	for (int i = 1; i <= n; i++) {
		yun[i].father = i;
		cin >> yun[i].money;
		cin >> yun[i].value;
	}

	for (int i = 1; i <= m; i++) {
		int x, y;
		cin >> x >> y;
		int fa_x = find(x);
		int fa_y = find(y);
		if (fa_x != fa_y) {
			yun[fa_y].father = fa_x;
			yun[fa_y].money += yun[fa_x].money;
			yun[fa_x].money = yun[fa_y].money;
			yun[fa_y].value += yun[fa_x].value;
			yun[fa_x].value = yun[fa_y].value;
		}
	}
	int cnt = 0;
	for (int i = 1; i <= n; i++) {
		if (yun[i].father == i) {
			money[++cnt] = yun[i].money;
			value[cnt]  = yun[i].value;
		}
	}



	for (int i = 1; i <= cnt; i++) {
		for (int j = w; j >= money[i]; j--) {
			dp[j] = max(dp[j], dp[j - money[i]] + value[i]);
		}
	}
//	sort(yun+1, yun + n+1, cmp);
	cout << dp[w];
	return 0;
}

1388:家谱(gen)

【题目描述】

现代的人对于本家族血统越来越感兴趣,现在给出充足的父子关系,请你编写程序找到某个人的最早的祖先。

【题目分析】

问题本质:给定合并顺序的并查集

定义关联容器map数组 fa[]存储字符串,获取每一行字符判断第一个字符如果为#,分割字符串fat,初始化fa[fat]=fat,如果为+,分割字符串son,指定son的父亲节点fa[son]=find(fat),如果为?,分割字符串son,输出查询结果fa[son],如果为$,结束运行

【代码实现】

#include <bits/stdc++.h>
using namespace std;

map<string, string > father;

string find(string s) {
	if (father[s] != s)
		father[s] = find(father[s]);
	return father[s];
}

int main() {
	//input data
	//clock_t s = clock();
	string in;
	string fa;
	string son;
	string que;
	while (cin >> in, in != "$") {
		if (in[0] == '#') {
			fa = in.substr(1);
			if (father[fa] == "") father[fa] = fa;
		}
		if (in[0] == '+') {
			son = in.substr(1);
			father[son] = find(fa);
		}
		if (in[0] == '?') {
			que = in.substr(1);
			cout << que << " " << find(que) << endl;
		}
	}
	//output time
	//cout <<endl<< clock() - s<<endl;
	return 0;
}

1389:亲戚

【题目描述】

若某个家族人员过于庞大,要判断两个是否是亲戚,确实还很不容易,现在给出某个亲戚关系图,求任意给出的某个人所在家族的人数。

规定:x和y是亲戚,y和z是亲戚,那么x和z也是亲戚。如果x,y是亲戚,那么x的亲戚都是y的亲戚,y的亲戚也都是x的亲戚。

【题目分析】

带权并查集:集合元素包含家族人数,询问某个人先找到该人的祖先,并输出祖先的人数

定义并查集关联数组fa[],集合个数数组sum[],获取关系字符c,如果c=='M',将a合并给b,sum[fa_b]+=sum[fa_a],如果c=='Q',输出sum[fa_q]

【代码实现】

#include <bits/stdc++.h>
using namespace std;

int father[100005];
int sum[100005];
int n, m;
int find(int x) {
	if (father[x] != x)
		father[x] = find(father[x]);
	return father[x];
}

int main() {
	//input data
	//clock_t s = clock();
	cin >> n >> m;
	for (int i = 1; i <= n; i++) {
		father[i] = i;
		sum[i] = 1;
	}
	
	for (int i = 1; i <= m; i++) {
		char c;
		int a, b;
		scanf("%c",&c);scanf("%c",&c);
		if (c == 'M') {
			scanf("%d%d", &a, &b);
			int fa_a = find(a); 
			int fa_b = find(b);
			if (fa_a != fa_b) {
				father[fa_b] = fa_a;
				sum[fa_a] += sum[fa_b];
			}
		} 
		if(c=='Q') {
			scanf("%d", &a);
			printf("%d\n", sum[find(a)]);
		}
	}

	//output time
	//cout <<endl<< clock() - s<<endl;
	return 0;
}

1390:食物链【NOI2001】

【题目描述】

动物王国中有三类动物A,B,C,这三类动物的食物链构成了有趣的环形。A吃B, B吃C,C吃A。

现有N个动物,以1-N编号。每个动物都是A,B,C中的一种,但是我们并不知道它到底是哪一种。

有人用两种说法对这N个动物所构成的食物链关系进行描述:

第一种说法是"1 X Y",表示X和Y是同类。

第二种说法是"2 X Y",表示X吃Y。

此人对N个动物,用上述两种说法,一句接一句地说出K句话,这K句话有的是真的,有的是假的。当一句话满足下列三条之一时,这句话就是假话,否则就是真话。

1)当前的话与前面的某些真的话冲突,就是假话;

2)当前的话中X或Y比N大,就是假话;

3)当前的话表示X吃X,就是假话。

你的任务是根据给定的N(1≤ N ≤50,000)和K句话(0≤K≤100,000),输出假话的总数。

【题目分析】

扩展域并查集:将x进行关系分解,x表示自身,x+n表示x的食物,x+2*n表示x的敌人,同样y表示自身,y+n表示x的食物,y+2*n表示x的敌人

x与y是同类,如果x能够吃掉y或者y能够吃掉x,该命题都为假,find(x+n)==find(y)||find(y+n)==find(x)
否则合并x与y,合并x+n与y+n,合并x+2*n与y+2*n
x可以吃掉y,如果x与y是同类或者y可以吃掉x,该命题都为假,find(x)==find(y)||find(y+n)==find(x)
否则合并x+n与y,合并x与y+2*n,合并x+2*n与y+n

统计假命题的个数进行答案输出

【代码实现】

#include<bits/stdc++.h>
using namespace std;


int fa[150005], x, y, p;

int get(int a) {
	return fa[a] == a ? a : fa[a] = get(fa[a]);
}
void join(int a, int b) {
	int fa_a = get(a);
	int fa_b = get(b);
	if (fa_a != fa_b) fa[fa_a] = fa_b;
}

int main() {

	int m, n;
	cin >> n >> m;
	for (int i = 1; i <= 3 * n; i++) {
		fa[i] = i;
	}
	int ans = 0;
	for (int i = 1; i <= m; i++) {
		cin >> p >> x >> y;
		if (x > n || y > n) ans++;
		else if (p == 2 && x == y) ans++;
		else {
			if (p == 1) {
				if (get(x + n) == get(y) || get(x) == get(y + n)) ans++;
				else join(x, y), join(x + n, y + n), join(x + 2 * n, y + 2 * n);

			} else {
				if (get(x) == get(y) || get(x) == get(y + n)) ans++;
				else join(x + n, y), join(x, y + 2 * n), join(x + 2 * n, y + n);
			}
		}
	}
	cout << ans << endl;
	return 0;
}

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值