并查集-初步了解

基础知识

并查集:维持许多集合的数据结构
假设有6个集合:
{a},{b},{c},{d},{e},{f}

  1. bool isSameSet(a,e):查询两个元素是否在一个集合
    初始:每个集合都只有一个元素,所以指针都指向本身。
    设两个指针分别指向a,e,然后指针后移到末尾(把它称为代表结点),若两个指针的指向一致,则在一个集合里。
  2. void union(a,e)a所在集合的全体和e所在集合的全体合成一个集合
    比较a所在集合的元素个数和e所在集合的个数,小集合的末尾指向a

两个重要优化:
1.合并时是小的合到大的里(小集合能够最快找到根结点)
2 每次查询某个结点所在集合的代表结点时,就更新沿途路径结点的父结点,都指向代表结点。当下次查找沿途结点所在集合的代表结点时就能直接返回。(链越长时间复杂度越高)
有了这两个重要优化,时间复杂度为O(1)

#include<iostream>
#include<list>
#include<algorithm>
#include<unordered_map>
#include<unordered_set>
#include<queue>
#include <stack>
using namespace std;
class Node {
	int value;
public:
	Node(int val) {
		value = val;
	}
};
class UnionSet {
public:
	unordered_map<int, Node*>nodes;//存放所有的结点,key对应的是结点Node
	unordered_map<Node*, Node*>parents;//key的父亲是value
	unordered_map<Node*, int>sizeMap;//该结点所在集合的元素个数,只有代表结点才会在sizeMap,所以sizeMap的键值对个数就是集合的个数
	UnionSet(vector<int>value) {//初始化,假设{a,b,c,d,e,f},每一个元素都是一个集合
		for (int val : value) {
			Node* node = new Node(val);
			nodes[val] = node;//val对应结点node
			parents[node] = node;//node结点的父亲是node
			sizeMap[node] = 1;//node就是本集合的代表结点,这个集合只有node这一个结点

		}

	}
	//给出一个结点,返回该结点所在集合的代表结点,代表结点的父节点依然是代表结点
	Node* findFather(Node* node) {//找到node结点所在结合的代表结点
		stack<Node*>path;
		
		while (parents[node] != node) {
			path.push(node);
			node = parents[node];
		}
		//此时node就是代表结点
		while (!path.empty()) {//重要优化:修改栈中每一个结点的父亲
			Node* cur = path.top();
			path.pop();
			parents[cur] = node;
		}
		return node;
		
	}
	bool isSameSet(int a, int b) {//查询a和b是否在一个集合
		return findFather(nodes[a]) == findFather(nodes[b]);
	}
	void unionSet(int a, int b) {//合并a所在集合的全体和b所在集合的全体
		Node* aHead = findFather(nodes[a]);//a所在集合的代表结点
		Node* bHead = findFather(nodes[b]);//b所在集合的代表结点
		if (aHead != bHead) {//前提是a和b不在一个集合
			int aSetSize = sizeMap[aHead];
			int bSetSize = sizeMap[bHead];
			Node* big = aSetSize >= bSetSize ? aHead : bHead;
			Node* small = big == aHead ? bHead : aHead;
			parents[small] = big;//small的父亲是big
			sizeMap[big] = aSetSize + bSetSize;//因为是小的放在大的里,所以big依然是代表结点
			sizeMap.erase(small);//small不再是代表结点了,所以删去
		}
	}
};

泛型

template<typename T>
class Node {
	T val;
public:
	Node(T val) :val(val) {}
};

template<typename T>
class UnionSet {
public:
	unordered_map<T, Node<T>*>nodes;//key值对应结点value
	unordered_map<Node<T>*, Node<T>*>parents;//key的父亲是value
	unordered_map<Node<T>*, int>sizeMap;//key所在集合的元素个数是value(key必须为代表结点)
	UnionSet(vector<T>& value) {//初始化value.size()个集合
		for (T val : value) {
			nodes[val] = new Node<T>(val);
			parents[nodes[val]] = nodes[val];
			sizeMap[nodes[val]] = 1;
		}
	}

	Node<T>* findAncestors(Node<T>* cur) {
		stack<Node<T>*>paths;//存放从cur到代表结点的路径
		while (cur != parents[cur]) {
			paths.push(cur);
			cur = parents[cur];
		}
		//此时cur就是代表结点
		while (!paths.empty()) {//修改paths里结点的父亲结点
			Node<T>* node = paths.top();
			paths.pop();
			parents[node] = cur;
		}
		return cur;
	}

	bool isSameSet(T t1, T t2) {//判断两个元素是否在一个集合
		return findAncestors(nodes[t1]) == findAncestors(nodes[t2]);
	}
	void unionSet(T t1, T t2) {//合并t1所在集合的全体和t2所在集合的全体
		Node<T>* t1Head = findAncestors(nodes[t1]);
		Node<T>* t2Head = findAncestors(nodes[t2]);
		if (t1Head != t2Head) {
			Node<T>* big = sizeMap[t1Head] >= sizeMap[t2Head] ? t1Head : t2Head;//大元素的集合
			Node<T>* small = big == t1Head ? t2Head : t1Head;//小元素的集合
			parents[small] = big;//改变small的父亲
			sizeMap[big] += sizeMap[small];//合并
			sizeMap.erase(small);//small合到big里了,所以small不再是代表结点了
		}
	}

};

例题

547. 省份数量
分析:
初始假设有n个城市,就是n个省份(集合),如果第i个城市与第j个城市相连,那么i城市和j城市在一个省份,也即合成一个集合,所以判断所有城市的相连关系,相连就合在一起,最后统计集合的个数,即为省份的个数。
直接套用并查集模板就好。

template<typename T>
class Node {
	T val;
public:
	Node(T val) :val(val) {}
};

template<typename T>
class UnionSet {
public: 
	unordered_map<T, Node<T>*>nodes;//key值对应结点value
	unordered_map<Node<T>*, Node<T>*>parents;//key的父亲是value
	unordered_map<Node<T>*, int>sizeMap;//key所在集合的元素个数是value(key必须为代表结点)
	UnionSet(vector<T>& value) {//初始化value.size()个集合
		for (T val : value) {
			nodes[val] = new Node<T>(val);
			parents[nodes[val]] = nodes[val];
			sizeMap[nodes[val]] = 1;
		}
	}

	Node<T>* findAncestors(Node<T>* cur) {
		stack<Node<T>*>paths;//存放从cur到代表结点的路径
		while (cur != parents[cur]) {
			paths.push(cur);
			cur = parents[cur];
		}
		//此时cur就是代表结点
		while (!paths.empty()) {//修改paths里结点的父亲结点
			Node<T>* node = paths.top();
			paths.pop();
			parents[node] = cur;
		}
		return cur;
	}

	bool isSameSet(T t1, T t2) {//判断两个元素是否在一个集合
		return findAncestors(nodes[t1]) == findAncestors(nodes[t2]);
	}
	void unionSet(T t1, T t2) {//合并t1所在集合的全体和t2所在集合的全体
		Node<T>* t1Head = findAncestors(nodes[t1]);
		Node<T>* t2Head = findAncestors(nodes[t2]);
		if (t1Head != t2Head) {
			Node<T>* big = sizeMap[t1Head] >= sizeMap[t2Head] ? t1Head : t2Head;//大元素的集合
			Node<T>* small = big == t1Head ? t2Head : t1Head;//小元素的集合
			parents[small] = big;//改变small的父亲
			sizeMap[big] += sizeMap[small];//合并
			sizeMap.erase(small);//small合到big里了,所以small不再是代表结点了
		}
	}

};

class Solution {
public:

    int findCircleNum(vector<vector<int>>& isConnected) {
        int N=isConnected.size();
        vector<int>value;
        for(int i=0;i<N;i++){
            value.push_back(i);//N个城市
        }
        UnionSet unionset(value);//初始化N个集合的并查集
        for(int i=0;i<N;i++){
            for(int j=i+1;j<N;j++){//因为右下角和左上角是对称的,只需要比较右上角的值就好
                if(isConnected[i][j]==1){
                    unionset.unionSet(i,j);
                }
            }
        }
        return unionset.sizeMap.size();//代表结点的个数就是集合的个数
    }
};


优化版本:
用数组代替哈希表

class UnionSet {
public:
    vector<int>parent;//parent[i]=k:i的父亲是k
    vector<int>size;//只有i为代表结点时才有意义,size[i]=k:i所在的集合大小是k
    vector<int>help;//模拟栈
    int sets;//集合个数
    UnionSet(int N){
        parent.resize(N);
        size.resize(N);
        help.resize(N);
        sets=N;
        for(int i=0;i<N;i++){
            parent[i]=i;//i的父亲是i
            size[i]=1;//i所在的集合大小1,只有自己
        }
       
    }
    int findFather(int cur){//找cur所在集合的代表结点
        int index=0;
        while(parent[cur]!=cur){
            help[index++]=cur;//将沿途路径压入堆栈
            cur=parent[cur];
        }
        //当前cur就是代表结点,栈中的元素最多就是N个
        for(index--;index>=0;index--){
            parent[help[index]]=cur;//修改沿途路径的父亲
        }
        return cur;
    }
    void unionSet(int i,int j){
        int iHead=findFather(i);//i所在集合的代表结点
        int jHead=findFather(j);//j所在结合的代表结点
        if(iHead!=jHead){
            if(size[iHead]>=size[jHead]){//小的合在大的里
                parent[jHead]=iHead;
                size[iHead]+=size[jHead];
                sets--;
            }
            else{
                parent[iHead]=jHead;
                size[jHead]+=size[iHead];
                sets--;
            }
        }
    }

};
class Solution {
public:
    int findCircleNum(vector<vector<int>>& isConnected) {
        int N=isConnected.size();
        UnionSet unionSet(N);
        for(int i=0;i<N;i++){
            for(int j=i+1;j<N;j++){
                if(isConnected[i][j]==1){
                    unionSet.unionSet(i,j);
                }
            }
        }
        return unionSet.sets;
    }
};

200. 岛屿数量

解1:
一行一行从左往右,只有当前位置为1,就往四周扩散(前提是四周为1),然后做好标记。

class Solution {
public:
    void infect(vector<vector<char>>& grid,int i,int j){//从i j位置开始向四周扩散,扩散规则是只有相邻位置为1才扩散,标记扩散位置
        if(i<0 || i==grid.size() || j==grid[0].size() || j<0 || grid[i][j]!='1'){
            return ;//非法位置以及不为1直接返回
        }
        //i j位置为1
        grid[i][j]='2';//标记已经扩散到这了
        //上下左右扩散
        infect(grid,i-1,j);
        infect(grid,i+1,j);
        infect(grid,i,j-1);
        infect(grid,i,j+1);
    }
    int numIslands(vector<vector<char>>& grid) {
        int islands=0;
        for(int i=0;i<grid.size();i++){
            for(int j=0;j<grid[0].size();j++){
                if(grid[i][j]=='1'){
                    islands++;
                    infect(grid,i,j);
                }
            }
        }
        return islands;
    }
};

解2 并查集
分析:如果当前位置为1,就判断左边是否为1,上边是否为1.
难点在于如何区分这些1,这里用自定义类的地址来区分。
再创建一个grid2数组,将原数组中为1的位置创建自定义类型的变量放在grid2中

template<typename T>
class Node {
	T value;
public:
	Node(T val) {
		value = val;
	}
};
template<typename T>
class UnionSet {
public:
	unordered_map<T, Node<T>*>nodes;//存放所有的结点,key对应的是结点Node
	unordered_map<Node<T>*, Node<T>*>parents;//key的父亲是value
	unordered_map<Node<T>*, int>sizeMap;//只有代表结点才会在sizeMap,所以sizeMap的键值对个数就是集合的个数
	UnionSet(vector<T>value) {//初始化,假设{a,b,c,d,e,f},每一个元素都是一个集合
		for (T val : value) {
			Node<T>* node = new Node<T>(val);
			nodes[val] = node;//val对应结点node
			parents[node] = node;//node结点的父亲是node
			sizeMap[node] = 1;//node所在集合的元素个数,node就是本集合的代表结点,这个集合只有node这一个结点

		}

	}
	//给出一个结点,返回该结点所在集合的代表结点,代表结点的父节点依然是代表结点
	Node<T>* findFather(Node<T>* node) {//找到node结点所在结合的代表结点
		stack<Node<T>*>path;
		
		while (parents[node] != node) {
			path.push(node);
			node = parents[node];
		}
		//此时node就是代表结点
		while (!path.empty()) {//重要优化:修改栈中每一个结点的父亲
			Node<T>* cur = path.top();
			path.pop();
			parents[cur] = node;
		}
		return node;
		
	}
	bool isSameSet(T a, T b) {//查询a和b是否在一个集合
		return findFather(nodes[a]) == findFather(nodes[b]);
	}
	void unionSet(T a, T b) {//合并a所在集合的全体和b所在集合的全体
		Node<T>* aHead = findFather(nodes[a]);//a所在集合的代表结点
		Node<T>* bHead = findFather(nodes[b]);//b所在集合的代表结点
		if (aHead != bHead) {//前提是a和b不在一个集合
			int aSetSize = sizeMap[aHead];
			int bSetSize = sizeMap[bHead];
			Node<T>* big = aSetSize >= bSetSize ? aHead : bHead;
			Node<T>* small = big == aHead ? bHead : aHead;
			parents[small] = big;//small的父亲是big
			sizeMap[big] = aSetSize + bSetSize;//因为是小的放在大的里,所以big依然是代表结点
			sizeMap.erase(small);//small不再是代表结点了,所以删去
		}
	}
};
class dot{

};
class Solution {
public:

    int numIslands(vector<vector<char>>& grid) {
        int row=grid.size();
        int col=grid[0].size();
        vector<dot*>value;//value中元素的个数就是初始并查集中集合的个数
        vector<vector<dot*>>grid2(row,vector<dot*>(col));
        for(int i=0;i<grid.size();i++){
            for(int j=0;j<grid[0].size();j++){
                if(grid[i][j]=='1'){//用dot的地址来标记每个不同的1
                    grid2[i][j]=new dot();
                    value.push_back(grid2[i][j]);//该位置的1就可以用grid2[i][j]来代替
                }
            }
        }
        UnionSet<dot*> unionSet(value);
        for(int i=0;i<grid.size();i++){
            for(int j=0;j<grid[0].size();j++){
                if(grid[i][j]=='1'){
                    if(i>0 && grid[i-1][j]=='1'){
                        unionSet.unionSet(grid2[i-1][j],grid2[i][j]);//合并上边
                    }
                    if(j>0 && grid[i][j-1]=='1'){
                        unionSet.unionSet(grid2[i][j-1],grid2[i][j]);//合并左边
                    }
                }
            }
        }
        return unionSet.sizeMap.size();
    }
};

优化:不要dot,也不用哈希表
对于一个二维位置(i,j)都可以转化为一维坐标(i*列数+j)

class UnionSet {
public:
	vector<int>parents;
	vector<int>size;
	vector<int>help;
	int sets;
	int col;
	int index(int i, int j) {
		return i * col + j;
	}
	UnionSet(vector<vector<char>>& grid) {
        
		int row = grid.size();
		col = grid[0].size();
		parents.resize(row*col);
		size.resize(row * col);
        help.resize(row*col);
		sets = 0;
		for (int i = 0; i < row; i++) {
			for (int j = 0; j < col; j++) {
				if (grid[i][j] =='1') {
					int t = index(i, j);
					sets++;
					parents[t] = t;
					size[t] = 1;
				}
			}
		}
	}

	int findFather(int cur) {
		int index = 0;
		while(cur != parents[cur]) {
			help[index++] = cur;
			cur = parents[cur];
		}
		for (index--; index >= 0; index--) {
			parents[help[index]] = cur;

		}
		return cur;
	}
	void unionSet(int r1, int col1, int r2, int col2) {
		int index1 = index(r1, col1);
		int index2 = index(r2, col2);
		int index1Head = findFather(index1);
		int index2Head = findFather(index2);
		if (index1Head != index2Head) {
			sets--;
			if (size[index1Head] >= size[index2Head]) {
				parents[index2Head] = index1Head;
				size[index1Head] += size[index2Head];
			}
			else {
				parents[index1Head] = index2Head;
				size[index2Head] += size[index1Head];
			}
		}
	}
};

class Solution {
public:
    int numIslands(vector<vector<char>>& grid) {
        int row=grid.size();
        int col=grid[0].size();
        UnionSet unionSet(grid);
        for(int i=0;i<row;i++){
            for(int j=0;j<col;j++){
                if(grid[i][j]=='1'){
                    if(i>0 && grid[i-1][j]=='1'){
                        unionSet.unionSet(i,j,i-1,j);
                    }
                    if(j>0 && grid[i][j-1]=='1'){
                        unionSet.unionSet(i,j,i,j-1);
                    }
                }
                
            }
        }
        return unionSet.sets;
    }
};

434 · 岛屿的个数II
题目描述:
假设你设计一个游戏,用一个 m 行 n 列的 2D 网格来存储你的游戏地图。
起始的时候,每个格子的地形都被默认标记为「水」。我们可以通过使用 addLand 进行操作,将位置 (row, col) 的「水」变成「陆地」。
你将会被给定一个列表,来记录所有需要被操作的位置,然后你需要返回计算出来 每次 addLand 操作后岛屿的数量。
注意:一个岛的定义是被「水」包围的「陆地」,通过水平方向或者垂直方向上相邻的陆地连接而成。你可以假设地图网格的四边均被无边无际的「水」所包围。
请仔细阅读下方示例与解析,更加深入了解岛屿的判定。

示例:
输入: m = 3, n = 3, positions = [[0,0], [0,1], [1,2], [2,1]]
输出: [1,1,2,3]

解析:
起初,二维网格 grid 被全部注入「水」。(0 代表「水」,1 代表「陆地」)
0 0 0
0 0 0
0 0 0

操作 #1:addLand(0, 0) 将 grid[0][0] 的水变为陆地。
1 0 0
0 0 0
0 0 0
Number of islands = 1

操作 #2:addLand(0, 1) 将 grid[0][1] 的水变为陆地。
1 1 0
0 0 0
0 0 0
岛屿的数量为 1

操作 #3:addLand(1, 2) 将 grid[1][2] 的水变为陆地。
1 1 0
0 0 1
0 0 0
岛屿的数量为 2

操作 #4:addLand(2, 1) 将 grid[2][1] 的水变为陆地。
1 1 0
0 0 1
0 1 0
岛屿的数量为 3

分析:
动态连接:计算当前的集合数

struct Point {
	int x;
	int y;
	Point() : x(0), y(0) {}
	Point(int a, int b) : x(a), y(b) {}
};


class UnionFind {
public:
	vector<int>parents;
	vector<int>size;//size还可以用来标记有没有初始化过,如果size[i]是0,说明之前没有初始化过
	vector<int>help;
	int row;
	int col;
	int sets;
	int index(int i, int j) {
		return i * col + j;
	}
	UnionFind(int m, int n) {//初始化m行n列全为0的数组
		row = m;
		col = n;
		sets = 0;
		int len = row * col;
		parents.resize(len);
		size.resize(len);
		help.resize(len);

	}
	int find(int i) {
		int hi = 0;
		while (i != parents[i]) {
			help[hi++] = i;
			i = parents[i];
		}
		for (hi--; hi >= 0; hi--) {
			parents[help[hi]] = i;
		}
		return i;
	}
	void unionSet(int r1, int c1, int r2, int c2) {
		if (r1 < 0 || r1 == row || c1 < 0 || c1 == col || r2 < 0 || r2 == row || c2 < 0 || c2 == col) {//越界跳过
			return;
		}
		int index1 = index(r1, c1);
		int index2 = index(r2, c2);
		if (size[index1] == 0 || size[index2] == 0) {
			return;//只有index1和index2都被初始化了才进行连接
		}
		
		int f1 = find(index1);
		int f2 = find(index2);
		
		if (f1 != f2) {
			if (size[f1] >= size[f2]) {
				size[f1] += size[f2];
				parents[f2] = f1;
			}
			else {
				size[f2] += size[f1];
				parents[f1] = f2;
			}
			sets--;
		}
	}
	int connect(int r, int c) {//如果将r行c列置1,返回当前的岛屿数
		int index1 = index(r, c);
		if (size[index1] == 0) {//前提是之前没有初始化过
			parents[index1] = index1;
			size[index1] = 1;
			sets++;
			unionSet(r - 1, c, r, c);//上下左右连接
			unionSet(r + 1, c, r, c);
			unionSet(r, c - 1, r, c);
			unionSet(r, c + 1, r, c);

		}
		return sets;
	}
};
class Solution {
public:
	vector<int> numIslands2(int n, int m, vector<Point>& operators) {
		// write your code here
		vector<int>result;
		UnionFind unionSet(n, m);
		for (int i = 0; i < operators.size(); i++) {
			result.push_back(unionSet.connect(operators[i].x, operators[i].y));
		}
		return result;
	}
};
  • 15
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值