并查集结构

并查集结构用于将一定量的元素合并到同一个集合,并随时查看两个元素所在的集合是否是同一个。即并查集结构必须提供isSameSet()接口与uinon()接口。实现并查集时有多种结构可以考虑,如list、hashMap等,但这些结构在合并时都需要进行集合的合并,代价比较高,最高效的做法是底层结构采用一种类似单链表的结构。

实现单链表时,一个节点的值就是输入的元素的值,而节点的指针指向同一个集合的另一个节点,所以一个集合就是一个单链表,该单链表的头节点就是这个集合的代表节点。

现在有5个元素,初始化的结果如下。它们的指针都指向自身,即自身自成一个集合,或者说每一个节点都是自己所在集合的代表节点。

实现union()接口时,将代表两个元素的节点所在的链表的头节点变为1个节点即可。

1元素与2元素所在的链表的长度相同,所以可以将其中任意一个链表挂在另外一个的头节点上即可。原则上你可以将其中一个链表挂在另外一个的任意节点上,但挂在头节点在执行isSameSet时所需要的操作更少。同样的原因,一般会将链表长度较短的挂在长链表上。

在实现isSameSet()接口时,判断两个元素是否在同一个集合,就是判断代表两个元素的节点所在的链表的头节点是否相同。

可以看出,并查集结构最耗时的操作就是寻找头节点时的操作,针对这个操作有优化的方法。在找到头节点后,将寻找头节点过程中经过的节点的指针都指向头节点,使链表扁平化。

并查集结构的时间复杂度非常低,设输入的数据量为N,执行isSameSet()的次数为a,执行union()的次数为b,如果a + b 接近或超过O(N)的话,那么并查集的时间复杂度可以看作O(1)。

template<typename T> class Node{
public:
    T value;
    Node<T>* father;
    int size;
    Node(T value, T* father){
        this->value = value;
        this->father = father;
        this->length = 0;
    }
    Node(T value){
        this->value = value;
        this->father = NULL;
        this->length = 0;
    }
    Node(){
        this->value = 0;
        this->father = 0;
        this->size = 0;
    }
};

template<typename T> class UnionFind{
public:
    Node<T>* nodes;
    int nodeSize;
    map<T,Node<T>* > headMap;//元素对应的Node
    UnionFind(T arr[],int n){
        nodes = new Node<T>[5]();
        nodeSize = n;
        for(int i = 0; i < n; i++){
            nodes[i].value = arr[i];
            nodes[i].father = &nodes[i];
            nodes[i].size = 1;
            headMap[arr[i]] = &nodes[i];;
        }
    }

    ~UnionFind(){
        delete nodes;
    }

    //寻找一个集合的代表节点
    //优化:将上溯寻找过程中经过的节点都直接挂在头节点上,使它扁平化。
    Node<T>* findHead(Node<T>* node){
        stack<Node<T>*> s;
        for( ; node->father != node; node = node->father){
            s.push(node);
        }
        while(!s.empty()){
            Node<T>* n = s.top();
            n->father = node;
            s.pop();
        }
        return node;
    }


    bool isSameSet(T a,T b){
        if(headMap.find(a) != headMap.end() || headMap.find(b) != headMap.end()){
            return findHead(headMap.find(a)->second) == findHead(headMap.find(b)->second);
        }
        return false;
    }

    bool unionSet(T a,T b) {
        if(headMap.find(a) != headMap.end() || headMap.find(b) != headMap.end()){
            Node<T>* node_a = headMap.find(a)->second;
            Node<T>* node_b = headMap.find(b)->second;
            if(node_a != node_b){
                Node<T>* smallHead = node_a->size > node_b->size ? node_b : node_a;
                Node<T>* bigHead = smallHead == node_a ? node_b : node_a;
                smallHead->father = bigHead;
                bigHead->size += smallHead->size;
                return true;
            }
        }
        return false;
    }


};

 

扩展:岛问题

【题目】 一个矩阵中只有0和1两种值,每个位置都可以和自己的上、下、左、右 四个位置相连,如 果有一片1连在一起,这个部分叫做一个岛,求一个矩阵中有多少个岛?

【举例】 001010 111010 100100 000000 这个矩阵中有三个岛

【进阶】 如何设计一个并行算法解决这个问题

这道题目的解法非常简单,难点在于并行。

//将与传入1相邻的所有的1都改为2
void infect(int* arr,int rowSize,int colSize,int row, int col){
    if(arr[row*colSize + col] == 2 || arr[row*colSize + col] == 0 || row == rowSize || col == colSize || row == -1 || col == -1){
        return ;
    }
    if(arr[row*colSize + col] == 1){
        arr[row*colSize + col] = 2;
        infect(arr,rowSize,colSize,row + 1,col);
        infect(arr,rowSize,colSize,row - 1,col);
        infect(arr,rowSize,colSize,row,col + 1);
        infect(arr,rowSize,colSize,row,col - 1);
    }
}

int islands(int* arr,int rowSize,int colSize){
    if(arr == NULL || rowSize <= 0 || colSize <= 0)
        return 0;
    int res = 0;
    for(int i = 0; i < rowSize; i++){
        for(int j = 0; j < colSize; j++){
            if(arr[i * colSize + j] == 1){
                res++;
                infect(arr,rowSize,colSize,i,j);
            }
        }
    }
    return res;
}

 

 

当我们在尝试使用并行算法解决这个问题时就可以使用并查集结构。先对输入域进行划分,比如划分为两部分:

对两个部分使用不同的CPU开始计算,得出结果后对进行合并,并查集就可以在合并时使用。

两边执行完上述算法后的结果如下图,a、b、c、d分别代表一块感染区域的感染源,在边界附近同时需要记录被感染时的感染源。

在判断a、b、c、d能否合并时,可以选出一个(如a),对他的边界进行判断,如果边界上有节点是2,而与它相邻区域的节点也是2,说明它们应该是联通在一起的,此时可以执行unionSet,将两个区域合并(合并ac),此时岛的数量  - 1;继续遍历a的边界,发现b可以c联通,并且bc不在同一个集合,继续合并,岛的数量  - 1;继续遍历,发现b可以d联通,并且bd不在同一个集合,继续合并,岛的数量  - 1;继续遍历,发现a可以d联通,但ad在同一个集合,不需要合并,岛的数量为1。可以看出,并查集结构对于解决成块联通非常方便。

所以并行解决这个问题分为2步:第一步计算两部分岛的数量并收集边界信息;第二步利用并查集实现减少岛的过程。如果有多个CPU或计算单元,可以将整个输入域划分为多个部分,对每个部分计算岛的数量并收集4个边界的信息;在合并时依然利用并查集实现任意两个相邻部分的合并,这样速度就更快了。

 

 

 

 

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值