森林与并查集

并查集是什么?

并查集是一种用来管理元素分组情况的数据结构
可以高效进行如下操作
  • 查询元素a和元素b 是否属于同一组。
  • 合并元素a和元素b 所在的组
并查集解决的是连通性问题

连通性问题:

  • 定义:对于集合中的点,连通某些点及查询某些点是否连通

并查集的quick_find(查找是否连通很快)算法:

  • 1、基于染⾊的思想,⼀开始所有点的颜⾊不同
  • 2、连接两个点的操作,可以看成将⼀种颜⾊的点染成另⼀种颜⾊ 
  • 3、如果两个点颜⾊⼀样,证明连通,否则不连通
1.合并(连通)操作-- O(n)
  • 连通的不是两个点,而是两个点集(点集内的点颜色相同),将一个点集的颜色染给另一个点集
  • 通常采用遍历所有的点,找到其中要染色的点,进行染色--O(n)

2.连通判断--O(1)
  • 判断颜色是否相同
代码演示:
#include <iostream>
#include <cstdio>
using namespace std;
#define MAX_N 10000
int color[ MAX_N + 5];
void init( int n ) {
    for ( int i = 0; i <= n ; i++) color[i] = i;
    return ;
}
//连通判断
int find( int a ) {
    return color[ a ];
}
//合并操作
int merge( int a , int b , int n ) {
    int aa = find( a ), bb = find( b );
    if (aa == bb) return 0;
    //将 aa 染成 bb
    for ( int i = 0; i <= n ; i++) {
        if (color[i] == aa) {
            color[i] = bb;
        }
    }
    return 1;
}
void output( int n ) {
    int ret = 0;
    for ( int i = 0; i <= n ; i++) {
        ret += printf( "%3d" , i);
    }
    printf( "\n" );
    for ( int i = 0; i < ret; i++) printf( "-" );
    printf( "\n" );
    for ( int i = 0; i <= n ; i++) {
        printf( "%3d" , color[i]);
    }
    printf( "\n" );
    return ;
}
int main() {
    int n, a, b;
    cin >> n;
    init(n);
    while (cin >> a >> b) {
        printf( "merge %d with %d : %d\n" , a, b, merge(a, b, n));
        output(n);
    }
    return 0;
}

并查集的quick_union算法

4->3
3->8
根节点的判断
  • 当父节点的值等于当前节点的值,则该节点为根节点。如上图的:0,1,2,5,7,8,9
合并操作:
1.将相互连通的节点集合用树型结构保存
2. 合并操作时, 连通的不是两个点,而是两个点集,实际上是将⼀棵树作为另⼀棵树的⼦树
3.一个节点的入度可以有很多个,但出度最好为0/1(便于数组保存)

 查询操作

为了查询两个节点是否同一组,只要沿着树向上走,查询根节点是否相同,根节点相同时同一组,否则不同组。
例子:
                                                                                       
#include <iostream>
using namespace std;
#define MAX_N 10000
int fa[ MAX_N + 5];
void init( int n ) {
        for ( int i = 0; i <= n ; i++) {
              fa[i] = i;
       }
        return ;
}
//查找当前节点的连通点集的根节点
int find( int x ) {
        if (fa[ x ] == x ) return x ;
        return find(fa[ x ]);
}
//合并
int merge( int a , int b ) {
        int aa = find( a ), bb = find( b );
        if (aa == bb) return 0;
       fa[aa] = bb;
        return 1;
}
void output( int n ) {
    int ret = 0;
    for ( int i = 0; i <= n ; i++) {
        ret += printf( "%3d" , i);
    }
    printf( "\n" );
    for ( int i = 0; i < ret; i++) printf( "-" );
    printf( "\n" );
    for ( int i = 0; i <= n ; i++) {
        printf( "%3d" , fa[i]);
    }
    printf( "\n" );
    return ;
}
int main() {
    int n, a, b;
    cin >> n;
    init(n);
    while (cin >> a >> b) {
        printf( "merge %d with %d : %d\n" , a, b, merge(a, b));
        output(n);
    }
    return 0;
}

quick_union的问题

1、极端情况下会退化成⼀条链 
2、将节点数量多的接到少的树上面,导致了退化
3、将树⾼深的接到浅的上面,导致了退化

按秩优化:

  • 目的:防止并查集的树形结构退化成一个链表,保持一颗树形结构的查找效率
  • 操作:按节点数量/树的高度合并
  • 算法: Weighted Quick-Union
Weighted Quick-Union
  • 对于每棵树,记录树的高度(treeheight)/每棵子树的节点数量(size)
  • 合并时,如果两棵树的 treeheight/size 不同,那么 treeheight/size 小的向 treeheight/size 大的连边。

路径压缩:

  • 对于每个节点,一旦向上走到了一次根节点,就把这个点到父亲的边改成为直接连向根。
  • 如需要查询(7),就可以直接将(7)连接到根上。
  • 在此之上,不仅查询的节点,所有在查询过程中经过的所有节点,都可以直接连接到根上。再次查询时,就可以很快查询到根是谁了。
  • 如下,将(2)(3)(4)(5)都连接到(1)中。
代码演示:
#include <iostream>
using namespace std;
#define MAX_N 10000
int fa[ MAX_N + 5];
int _size[ MAX_N + 5];
void init( int n ) {
        for ( int i = 0; i <= n ; i++) {
              fa[i] = i;
        _size[i] = 1;
       }
        return ;
}
//查找当前节点的连通点集的根节点,并将返回的根节点赋值给路径上所有的子节点
int find( int x ) {
    return fa[ x ] = (fa[ x ] == x ? x : find(fa[ x ]));
}
//合并,节点数目较少的树的根节点,连接到节点数量较大的树的根节点下
int merge( int a , int b ) {
        int aa = find( a ), bb = find( b );
        if (aa == bb) return 0;
    if (_size[aa] < _size[bb]) {
        fa[aa] = bb;
        _size[bb] += _size[aa];
    }
    else {
        fa[bb] = aa;
        _size[aa] += _size[bb];
    }
        return 1;
}
void output( int n ) {
    int ret = 0;
    for ( int i = 0; i <= n ; i++) {
        ret += printf( "%3d" , i);
    }
    printf( "\n" );
    for ( int i = 0; i < ret; i++) printf( "-" );
    printf( "\n" );
    for ( int i = 0; i <= n ; i++) {
        printf( "%3d" , fa[i]);
    }
    printf( "\n" );
    for ( int i = 0; i <= n ; i++) {
        printf( "%3d" , _size[i]);
    }
    printf( "\n" );
    return ;
}
int main() {
    int n, a, b;
    cin >> n;
    init(n);
    while (cin >> a >> b) {
        printf( "merge %d with %d : %d\n" , a, b, merge(a, b));
        output(n);
    }
    return 0;
}
一些映射关系:
1.二维数组:坐标 -->下标
2.哈希函数
注意:
不等关系不是连通关系

带权的并查集

简单定义

 1.N个节点有M对关系(M条边),每对关系(每条边)都有一个权值w,可以表示距离或划分成多个集合时的集合编号.

全值的理解:

    连通关系比较复杂,所以在路径上加上权值,表示连通关系中的 不同情况。比如连通关系为输赢,节点之间就有输,赢,平的不同情况,这时我们就可以在路径上赋不同的值,表示不同的情况。
代码演示:
HZOJ: 72
#include <iostream>
#include <algorithm>
#include <string>
#include <unordered_map>
#include <unordered_set>
#include <vector>
#include <algorithm>
using namespace std;
class UnionSet {
public :
        vector < int > fa, value;
       UnionSet( int n ) :fa( n + 1), value( n + 1) {
               for ( int i = 0; i <= n ; i++) {
                      fa [ i ] = i;
                       //value[i]表示从i出发的路径的权值
                      value [ i ] = 0;
              }
       }
       
        int get( int x ) {
               if (fa [ x ] == x ) return x ;
               int root = get(fa [ x ] );
               //权值跟着路径压缩,%3是例题的权值范围是0,1,2
              value [ x ] = (value [ x ] + value [ fa [ x ]] ) % 3;
               return fa [ x ] = root;
       }
        void merge( int a , int b , int t ) {
               int aa = get( a ), bb = get( b );
               if (aa == bb) return ;
              fa [ aa ] = bb;
               //保证对3取余的结果是整数
              value [ aa ] = ( t + value [ b ] - value [ a ] + 3) % 3;
               return ;
       }
};
int main() {
        int n, m;
        ios ::sync_with_stdio( false );
       cin.tie( NULL );
       cin >> n >> m;
        UnionSet u(n);
        for ( int i = 0; i < m; i++) {
               int a, b, c;
              cin >> a >> b >> c;
               if (a == 1) {
                      u.merge(b, c, 2);
              }
               else {
                       if (u.get(b) != u.get(c)) {
                             cout << "Unknown" << endl;
                      }
                       else {
                              //保证对3取余的结果是整数
                              switch ((u.value [ b ] - u.value [ c ] + 3) % 3) {
                              case 0:cout << "Tie" << endl; break ;
                              case 1:cout << "Loss" << endl; break ;
                              case 2:cout << "Win" << endl; break ;
                             }
                      }
              }
       }
        return 0;
}
  • 21
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值