并查集
并查集的两个基本操作:
\(1.\) 查询一个元素属于哪个集合 \(O(logn)\)
\(2.\) 把两个集合合并成一个集合 \(O(logn)\)
实现方法: 建立树形结构存储,树上的节点都是一个元素
查询时只需要沿着路径向上搜索
合并时只需要把根节点和根节点相联通(使一个节点作为一个节点的父亲)
两个优化:
\(1.\) 路径压缩
因为如果是单链的情形,复杂度会飙升,但如果把访问的节点直接指向树根,就节省了沿路径走的时间
\(2.\) 按秩合并
秩:衡量集合(树)大小标志,可以是树的深度,也可以是集合的大小
合并时把秩小的接到秩大的上,只增加小的结构的查询代价
别名:启发式合并
void init() {
for (int i = 1; i <= n; i++) fa[i] = i ;
}
int get(int x) {
if (x == fa[x]) return x ;
return fa[x] = get(fa[x]) ;
}
void merge(int x, int y) {
fa[get(x)] = get(y) ;
}
程序自动分析 NOI2015
这个例题说明:并查集可以动态维护节点之间的连通性和传递性
离散化 等同合并 查询不等关系 不等的在一组直接 \(NO\)
#include <iostream>
#include <cstdio>
#include <cmath>
#include <algorithm>
#include <vector>
#include <map>
using namespace std ;
typedef long long ll ;
const int N = 1000010 ;
int n, m, T ;
int a[N], b[N], w[N] ;
int fa[2 * N] ;
map <int, int> M ;
int get(int x) {
if (x == fa[x]) return x ;
return fa[x] = get(fa[x]) ;
}
void merge(int x, int y) {
fa[get(x)] = get(y) ;
}
int main() {
scanf("%d", &T) ;
while (T--) {
scanf("%d", &m) ;
n = 0 ;
M.clear() ;
for (int i = 1; i <= m; i++) scanf("%d%d%d", &a[i], &b[i], &w[i]) ;
// 1 = 0 !
for (int i = 1; i <= m; i++) {
if (M.find(a[i]) == M.end()) M[a[i]] = ++n, a[i] = n ;
else a[i] = M[a[i]] ;
if (M.find(b[i]) == M.end()) M[b[i]] = ++n, b[i] = n ;
else b[i] = M[b[i]] ;
}
for (int i = 1; i <= n; i++) fa[i] = i ;
for (int i = 1; i <= m; i++) if (w[i] == 1) merge(a[i], b[i]) ;
bool flag = true ;
for (int i = 1; i <= m; i++)
if (w[i] == 0) {
if (get(a[i]) == get(b[i])) {
flag = false ;
break ;
}
}
puts(flag ? "YES" : "NO") ;
}
}
Supermarket \(POJ_1456\)
这个题的并查集挺有意思 维护的是一个数组中位置的占用情况
贪心思路:优先考虑利润大的商品,且对每个商品尽量晚的卖出(决策包容性)
对天数做并查集,对于一个商品,如果在 \(d\) 天过期,就查询 \(d\) 的树根 \(rt\)
假如 \(rt\) 是 \(0\) 说明前\(d\)天已经塞满,没有时间空余,该物品不考虑
如果 \(rt\) 不为 \(0\) 则取走该物品,把\(rt\)和\(rt-1\)合并
某一个节点的根节点的实际意义是 这颗树(其实是区间)最晚卖出该物品的时间,或者从后往前第一个空闲的日期
#include <iostream>
#include <cstdio>
#include <cmath>
#include <algorithm>
#include <vector>
using namespace std ;
typedef long long ll ;
const int N = 10010 ;
struct node {
int p, d ;
} ;
bool cmp(node a, node b) {
if (a.p != b.p) return a.p > b.p ;
else return a.d > b.d ;
}
int n, mxd, ans ;
node a[N] ;
int fa[N] ;
int get(int x) {
if (fa[x] == x) return x ;
else return fa[x] = get(fa[x]) ;
}
void merge(int x) {
fa[get(x)] = get(x - 1) ;
}
int main() {
while (scanf("%d", &n) != EOF) {
ans = 0 ; mxd = 0 ;
for (int i = 1; i <= n; i++) {
scanf("%d%d", &a[i].p, &a[i].d) ;
mxd = max(mxd, a[i].d) ;
}
sort(a + 1, a + n + 1, cmp) ;
for (int i = 1; i <= mxd; i++) fa[i] = i ;
for (int i = 1; i <= n; i++) {
int r = get(a[i].d) ;
if (r > 0) {
ans += a[i].p ;
merge(r) ;
}
}
printf("%d\n", ans) ;
}
}
NOI 2002 银河英雄传说
这个题除了要并查集维护在同一集合之中,还需要查询从一个节点到另一个节点之间的距离
因为集合是树状的,两个节点 \(i,j\) 之间的距离可以转化为 \(i\) 到 \(root\) 的距离和 \(j\) 到 \(root\) 距离的差值
距离可以通过一遍 \(get\) 获取,完事之后路径压缩不影响结果
然后再考虑合并操作
将 \(i\) 整列拼到 \(j\) 的列的下面,等价于把 \(i\) 的根 \(x\) 变成 \(j\) 的根 \(y\) 的一个子节点,而且 \(x\) 到 \(y\) 的距离要初始成当前 \(y\)的树大小
于是要新增一个记录子树大小的数组,合并之后更新大(新)树的根节点
#include <iostream>
#include <cstdio>
#include <cmath>
#include <algorithm>
#include <vector>
using namespace std ;
typedef long long ll ;
const int N = 500010 ;
const int M = 30010 ;
int n, T ;
int a[N], b[N], fa[M], d[M], size[M] ;
char w[N] ;
int get(int x) {
if (x == fa[x]) return x ;
int rt = get(fa[x]) ;
d[x] += d[fa[x]] ;
return fa[x] = rt ;
}
void merge(int x, int y) {
x = get(x), y = get(y) ;
fa[x] = y ; d[x] = size[y] ;
size[y] += size[x] ;
}
char getch() {
while (true) {
char t ;
scanf("%c", &t) ;
if (isalpha(t)) return t ;
}
}
int main() {
int T ; scanf("%d", &T) ;
for (int i = 1; i <= T; i++) {
w[i] = getch() ;
scanf("%d%d", &a[i], &b[i]) ;
}
for (int i = 1; i <= T; i++) n = max(n, max(a[i], b[i])) ;
for (int i = 1; i <= n; i++) fa[i] = i, d[i] = 0, size[i] = 1 ;
for (int i = 1; i <= T; i++) {
if (w[i] == 'M') {
merge(a[i], b[i]) ;
} else {
if (get(a[i]) != get(b[i])) puts("-1") ;
else {
printf("%d\n", abs(d[a[i]] - d[b[i]]) - 1) ;
}
}
}
}

浙公网安备 33010602011771号