并查集优化(UVA11354 Bond)

代码和思路参考:并查集2个优化——按秩合并和路径压缩
并查集讲解(按秩合并与路径压缩),模板与典型例题并查集(按秩合并) 和《算法竞赛进阶指南》


并查集优化
一、路径压缩

当我们只关心每个集合的根是什么,而不关心它的具体形态时,就可以将每一个节点都指向根节点
get均摊复杂度: O ( l o g N ) O(log N) O(logN)

1.递归

(短小精悍好写但是可能会栈溢出)

int get(int x)
{
	return fa[x] == x ? x : fa[x] = get(fa[x]);
}
2.非递归
int get(int x)
{
	if(fa[x] == x) return x;
	int r = x;
	while(fa[r] != r)
	{
		r = fa[r];
	}
	//找到根节点
	int k = x, f;
	while(k != r)
	{
		f = fa[k];
		fa[k] = r;
		k = f;
	}
	//把经过的节点都指向根
	return r;
}
/*
int get(int x)//循环实现的find和路径压缩 
{
	while(x != fa[x]) x = fa[x] = fa[fa[x]];//表示这句话很强... 
    return x;
}
*/
二、按秩合并

一般有两种定义:

  1. 集合的大小
  2. 未进行路径压缩时(即原本给出的树)树的深度

按秩合并,就是把集合的“秩”记录在根节点上,在合并时将“秩”较小的节点作为“秩”较大的节点的子节点

当“秩”定义为集合大小时,“按秩合并”也称为“启发式合并”,是数据结构中一种重要思想,不止局限于并查集
启发式合并原则:把“小的结构”合并到“大的结构中”,并只增加“小的结构”的查询代价

rank[x]记录x这棵树的树高(就是到树根最长的链的长度)
树高最高为 log2(N)
如果令fa[x] = y,那么y的树高最小为rank[x] + 1
所以rank[y] = max(rank[y], rank[x] + 1)

get均摊复杂度: O ( l o g N ) O(log N) O(logN)(因为树高范围)

1.思路如上,直观代码

void merge(int x, int y)
{
	x = get(x), y = get(y);
	if(x == y) return;
	if(rank[x] <= rank[y])
	{
		fa[x] = y;
		rank[y] = max(rank[x] + 1, rank[y]);
	}
	else
	{
		fa[y] = x;
		rank[x] = max(rank[x], rank[y] + 1);
	}
}

2.更简洁常用,其实和上面的一样

void merge(int x, int y)
{
	x = get(x), y = get(y);
	if(x == y) return;
	if(rank[x] < rank[y])
		fa[x] = y;
	else
	{
		fa[y] = x;
		if(rank[x] == rank[y]) rank[x]++;
	}
}

例题:UVA11354 Bond

题目大意:有一张n个点m条边的无向图, 每条边有一个危险度,有q个询问, 每次给出两个点s、t,找一条路径, 使得路径上的最大危险度最小

思路:Kruskal + 按秩合并

题目并不保证为一棵树,但保证s到t存在路径,容易想到先建最小生成树,在这棵树上的路径的最大危险度一定是最小的
但是直接遍历的话每一次复杂度为 O ( N ) O(N) O(N),n和q又大,过不了
于是想到在Kruskal的并查集上加优化
但常用的路径压缩又不行,那样会破坏树的结构,这时按秩合并就派上用场了


代码&解读
#include <iostream>
#include <cstdio>
#include <algorithm>

const int N = 50005, M = 100005;
int fa[N], rank[N], pre[N], vis[N];

struct Edge
{
	int u, v, w;
	bool operator <(const Edge &x) const
	{
		return w < x.w;
	}
}e[M];

int n, m;

int max(int a, int b)
{
	return a > b ? a : b;
}

int get(int x)
{
	return fa[x] == x ? x : get(fa[x]);
}

void merge(int x, int y, int z)
{
	x = get(x), y = get(y);
	if(x == y) return;
	if(rank[x] < rank[y])
	{
		fa[x] = y;
		pre[x] = z;
	}
	else
	{
		fa[y] = x;
		pre[y] = z;
		if(rank[x] == rank[y]) rank[x]++;
	}
}

int query(int x, int y)
{
	int ans = 0, k;
	k = x;
	//从x走到根
	while(1)
	{
		vis[k] = ans;
		if(fa[k] == k) break;
		ans = max(ans, pre[k]);
		k = fa[k];
	}
	ans = 0;
	k = y;
	//从y走到根
	while(1)
	{
		if(vis[k] >= 0)
		{
			ans = max(ans, vis[k]);
			break;
		}
		if(fa[k] == k) break;
		ans = max(ans, pre[k]);
		k = fa[k];
	}
	k = x;
	//x到根
	while(1)
	{
		vis[k] = -1;
		if(fa[k] == k) break;
		k = fa[k];
	}
	return ans;
}

void init()
{
	std::sort(e, e+m);
	for(int i = 1; i <= n; ++i)
	{
		fa[i] = i;
		rank[i] = 0;
		vis[i] = -1;//vis初值要<0 
	}
	for(int i = 0; i < m; ++i)
	{
		merge(e[i].u, e[i].v, e[i].w);
	}
}

int main(){
	int t = 0;
	while(scanf("%d%d", &n, &m) == 2)
	{
		if(t) printf("\n");
		else t++;
		for(int i = 0; i < m; ++i)
		{
			scanf("%d%d%d", &e[i].u, &e[i].v, &e[i].w);
		}
		init();
		int q, s, t;
		scanf("%d", &q);
		for(int i = 0; i < q; ++i)
		{
			scanf("%d%d", &s, &t);
			printf("%d\n", query(s, t));
		}
	}
	return 0;
}
变量:

pre[x]表示最小生成树上由x的父节点走向x的那条边的边权
vis[x]记录在某一次询问中树上从s到x的路径的最大危险度,同时可以判断x有没有被走过

merge()

读入边的数据后先按危险度从小到大排序,这样保证先插入的路径是更优的
也就是说如果现在x和y在同一集合中,那么连接x和y所在集合的边就不必加入

query()

注意前两个while()只有第一个有vis值的改变

x到y,可分为三种情况

  1. lca(x, y) = y
    这种情况只需x向上走,就能遇到y
    那么第二个while()中,有vis[y] >= 0,则ans = vis[y]后立即退出

  2. lca(x, y) = x
    这种情况y向上遇到x,发现vis[x] = 0ans不变,直接退出

  3. lca(x, y) != x && lca(x, y) != y(也就是x,y不在一条链上)
    在第二个while()中,走到lca(x, y),有vis[lca(x, y)] >= 0,于是ans = max(vis[lca(x, y)](x到lca(x, y)的最大危险度), ans(y到lca(x, y)的最大危险度))

那么第三个while()是干什么的呢
在第一个while()中,从x到根经过的节点的vis值都改变了,但这只适用于起点为x的情况,所以要再走一遍将vis改回来

不得不说算法文化博大精深
为什么要一直走到根
为什么第二个while()不改变vis
细思极恐啊我是不明白为什么会想到这样写QAQ

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值