基本概念
- 并查集是一种数据结构
- 并查集这三个字,一个字代表一个意思。
并(Union),代表合并
查(Find),代表查找
集(Set),代表这是一个以字典为基础的数据结构,它的基本功能是合并集合中的元素,查找集合中的元素 - 并查集的典型应用是有关连通分量的问题
- 并查集解决单个问题(添加,合并,查找)的时间复杂度都是O(1)
- 因此,并查集可以应用到在线算法中
链接:https://leetcode.cn/problems/number-of-provinces/solution/python-duo-tu-xiang-jie-bing-cha-ji-by-m-vjdr/
并查集模板(LeetCode547为例)
1.模板一
class UnionFind{
public:
// 查找祖先:如果节点的父节点不为空,那就不断迭代。
int find(int x){
int root = x;
while(father[root] != -1){
root = father[root];
}
// 压缩路径:也就是把树的深度固定为二,x 到根节点之间的所有节点直接接到根节点下⾯
while(x != root){
int original_father = father[x];
father[x] = root;
x = original_father;
}
return root;
}
// 判断两个节点的连通性
bool is_connected(int x,int y){
return find(x) == find(y);
}
// 合并两个节点:如果发现两个节点是连通的,
// 那么就要把他们合并,也就是他们的祖先是相同的。
// 这里究竟把谁当做父节点一般是没有区别的。
void Union(int x,int y){
int root_x = find(x);
int root_y = find(y);
if(root_x != root_y){
father[root_x] = root_y;
num_of_sets--;
}
}
// 初始化:当把一个新节点添加到并查集中,它的父节点应该为空
void add(int x){
if(!father.count(x)){
father[x] = -1;
num_of_sets++;
}
}
int get_num_of_sets(){
return num_of_sets;
}
private:
// 记录父节点
unordered_map<int,int> father;
// 记录集合数量
int num_of_sets = 0;
};
2.模板二
/*
* Copyright (c) Huawei Technologies Co., Ltd. 2019-2021. All rights reserved.
* Description: 上机编程认证
* Note: 缺省代码仅供参考,可自行决定使用、修改或删除
*/
#include <string>
#include <iostream>
#include <vector>
#include <algorithm>
#include <unordered_map>
using namespace std;
class UnionFind {
public:
/* n为图中节点的个数 */
UnionFind(int n)
{
this->nums_of_sets = n; // ⼀开始互不连通, this可省略
cout << "numss:" << this->nums_of_sets << '\n';
for (int i = 0; i < nums_of_sets; i++) {
this->parent[i] = i; // ⽗节点指针初始指向⾃⼰
}
}
/* 合并两个节点,如果发现两个节点是连通的,那么就要把他们合并,也就是他们的祖先是相同的。这里究竟把谁当做父节点一般是没有区别的 */
void Union(int x, int y)
{
int root_x = Find(x);
int root_y = Find(y);
if (root_x == root_y) { // 根节点相同不合并
return;
}
parent[root_x] = root_y; // 将两棵树合并为⼀棵, parent[rootQ] = rootP 也⼀样
nums_of_sets--; // 两个分量合⼆为⼀
cout << "nums:" << nums_of_sets << endl;
}
/* 查找祖先:如果节点的父节点不为空,那就不断迭代。*/
int Find(int x)
{
// 根节点的 parent[x] == x
while (parent[x] != x) {
x = parent[x];
}
return x;
}
/* 返回当前的联通分量个数 */
int Get_num_of_sets()
{
return nums_of_sets;
}
private:
int nums_of_sets; // 记录联通分量个数
unordered_map<int, int> parent; // 记录每个节点的父节点
};
class Solution {
public:
int findCircleNum(vector<vector<int>>& isConnected) {
UnionFind uf(isConnected.size());
for(int i = 0; i < isConnected.size(); i++){
for(int j = 0; j < i; j++){
if(isConnected[i][j]){
uf.Union(i, j);
}
}
}
return uf.Get_num_of_sets();
}
};
// 以下为考题输入输出框架,此部分代码不建议改动
using namespace std;
int main()
{
int x = 0;
vector<vector<int>> vec;
vector<int> v;
while(cin >> x) {
v.push_back(x);
if(cin.get() == '\n'){ // 从流中提取的字符为换行符的话
vec.push_back(v);
v.clear();
}
if(cin.peek() == '\n'){ //观测到当前字符为换行符的话, 连续输入两个换行结束
break;
}
}
cout << "row:" << vec.size() << endl;
cout << "col:" << vec[0].size() << endl;
Solution S;
int ret = S.findCircleNum(vec);
cout << "ret is " << ret << endl;
return 0;
}
LeetCode130 被围绕的区域
1. 解法一:DFS
class Solution {
public:
void dfs (vector<vector<char>>& board,int x ,int y) //dfs将没有被围绕的O作标记
{
if (x < 0 || x >= n || y < 0 || y >= m || board[x][y] != 'O' )
return ;
board[x][y] = 'M';
dfs (board, x + 1, y);
dfs (board, x - 1, y);
dfs (board, x , y + 1);
dfs (board, x, y - 1);
}
void solve(vector<vector<char> >& board) {
n = board.size(); //行
if (n == 0) return ;
m = board[0].size(); //列,此语句不能放在if(n==0)前面
for (int i = 0 ; i < n; ++i) { //标记第一列和最后一列的‘O’
dfs (board,i,0); //第一列
dfs (board,i,m - 1); //最后一列
}
for (int j = 1 ; j < m -1 ; ++j) { //标记第一行和最后一行的‘O’
dfs (board,0,j); //第一行
dfs (board,n - 1,j); //最后一行
}
for (int i = 0; i < n; ++i) {
for (int j = 0; j < m; ++j) {
if (board[i][j] == 'M')
board[i][j] = 'O';
else
board[i][j] = 'X';
}
}
}
private:
int n, m; //全局变量
};
2.解法二:并查集
/// 方法二:并查集 必须是四⾯被围的 O 才能被换成 X,也就是说边⻆上的 O ⼀定不会被围,进⼀步,与边⻆上的 O 相
// 连的 O 也不会被 X 围四⾯,也不会被替换
class UnionFind {
public:
/* n为图中节点的个数 */
UnionFind(int n)
{
this->nums_of_sets = n; // ⼀开始互不连通, this可省略
cout << "numss:" << this->nums_of_sets << '\n';
for (int i = 0; i < nums_of_sets; i++) {
this->parent[i] = i; // ⽗节点指针初始指向⾃⼰
}
}
/* 合并两个节点,如果发现两个节点是连通的,那么就要把他们合并,也就是他们的祖先是相同的。这里究竟把谁当做父节点一般是没有区别的 */
void Union(int x, int y)
{
int root_x = Find(x);
int root_y = Find(y);
if (root_x == root_y) { // 根节点相同不合并
return;
}
parent[root_x] = root_y; // 将两棵树合并为⼀棵, parent[rootQ] = rootP 也⼀样
nums_of_sets--; // 两个分量合⼆为⼀
cout << "nums:" << nums_of_sets << endl;
}
/* 查找祖先:如果节点的父节点不为空,那就不断迭代。*/
int Find(int x)
{
// 根节点的 parent[x] == x,先找到根节点
int root = x;
while (parent[root] != root) {
root = parent[root];
}
// 然后把 x 到根节点之间的所有节点直接接到根节点下⾯
while (x != root) {
int origin_father = parent[x];
parent[x] = root;
x = origin_father;
}
return root;
}
/* 返回当前的联通分量个数 */
int Get_num_of_sets()
{
return nums_of_sets;
}
/* 判断节点 p 和节点 q 是否连通 */
bool is_connected(int x,int y){
return Find(x) == Find(y);
}
private:
int nums_of_sets; // 记录联通分量个数
unordered_map<int, int> parent; // 记录每个节点的父节点
};
class Solution {
public:
void solve(vector<vector<char>>& board) {
int m = board.size();
int n = board[0].size();
UnionFind uf(m * n + 1);
int dummy = m * n;
// 将首列和末列的 O 与 dummy 连通
for (int i = 0; i < m; i++) {
if (board[i][0] == 'O') {
uf.Union(dummy, i * n);
}
if (board[i][n - 1] == 'O') {
uf.Union(dummy, i * n + n - 1);
}
}
// 将首行和末行的 O 与 dummy 连通
for (int j = 0; j < n; j++) {
if (board[0][j] == 'O') {
uf.Union(dummy, j);
}
if (board[m - 1][j] == 'O') {
uf.Union(dummy, (m - 1) * n + j);
}
}
// 方向数组 d 是上下左右搜索的常用手法
vector<vector<int> > direction{{1, 0}, {0, 1}, {-1, 0}, {0, -1}};
for (int i = 1; i < m - 1; i++) {
for (int j = 1; j < n - 1; j++) {
if (board[i][j] == 'O') {
// 将此O与上下左右的O进行联通
for (int k = 0; k < direction.size(); k++) {
int x = i + direction[i][j];
int y = j + direction[i][j];
if (board[x][y] == 'O') {
uf.Union(x * n + y, i * n + j);
}
}
}
}
}
// 所有不和 dummy 连通的 O,都要被替换
for (int i = 1; i < m - 1; i++) {
for (int j = 1; j < n - 1; j++) {
if (!uf.is_connected(dummy, i * n + j))
board[i][j] = 'X';
}
}
}
};