基础知识
并查集:维持许多集合的数据结构
假设有6个集合:
{a},{b},{c},{d},{e},{f}
- bool isSameSet(a,e):查询两个元素是否在一个集合
初始:每个集合都只有一个元素,所以指针都指向本身。
设两个指针分别指向a,e,然后指针后移到末尾(把它称为代表结点),若两个指针的指向一致,则在一个集合里。 - 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;
}
};
解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;
}
};