文章目录
记录连通块的某种和(累加祖宗的值)
如计算每个连通块的v[i]的和
见AcWing1252.搭配购买 AcWing837.连通块中点的个数
并查集维护与祖宗距离详解
说明
p[x]的值在计算距离之前必须先要find(p[x])一遍
因为d[x]要变成从x到根节点的距离,所以在执行d[x] += d[p[x]]之前,要先将d[p[x]]变成p[x]到根节点的距离
find递归语句写在最前面,在做这个操作: 按距离根节点从近到远的顺序,把这条x到根节点路径上的点的距离都更新一遍
并查集 1
一些细节问题:并查集一般用一维坐标比较方便,所以如果是二维点阵n * m 判断是否有环,可以先转换为一维坐标(x,y) -> x * n + y
int get(x,y) {
return x *n + y;
}
如:AcWing 1250. 格子游戏
一.并查集—合并+查找
并查集两个操作: 近乎0(1)时间复杂度内快速维护这两个操作
- 将两个集合合并
- 询问两个元素是在一个集合中
- 维护每个集合中元素的个数 见连通块中的点的个数
- 维护每个集合中元素与根节点的关系 见食物链
并查集的最本质是find函数,记这个就行了
二.基本原理
每个集合用一棵树来表示。树根的编号就是整棵树的编号。每个节点储存它的父节点,p[x]表示x的父节点
问题1. 如何判断树根:if(p[x] == x)
问题2. 如何求x的集合标号: while(p[x] != x) x = p[x];
问题3. 如何合并两个集合: p[find(x)] = find(y)
优化-路径压缩
我找一次后,所有路径上的点指向跟节点
第一次求要三次,第二次就只需要一次
模版题 AcWing 836.合并集合
版本1 简练
#include <iostream>
#include <cstdio>
#include <vector>
using namespace std;
const int N = 100010;
int n, m;
int p[N];
int find(int x)
{ // 返回x的祖宗节点 + 路径压缩
if (p[x] != x)
p[x] = find(p[x]); // 写find(p[x])这样才会递归下去
return p[x];
}
int main() {
string c; // 用string, 不会遇到M,Q1,Q2这种询问的麻烦
int a, b;
cin >> n >> m;
for (int i = 1; i <= n; i ++) {
p[i] = i;
}
for (int i = 0; i < m; i ++) {
cin >> c >> a >> b;
int pa = find(a), pb = find(b); // 祖宗点
if (c == "M") p[pa] = pb; // union操作 合并
// p[pb] = pa;也可以,影响不大,因为只要a和b的祖宗结点相同即可
else {
if (pa == pb)
cout << "Yes" << endl;
else cout << "No" << endl;
}
}
return 0;
}
版本2 用了UnionFind类 vector初始化遇到0比较麻烦
#include <iostream>
#include <cstdio>
#include <vector>
using namespace std;
class UnionFind {
public:
vector<int> father;
UnionFind(int num) {
for (int i = 0; i <= num; i ++) {
father.push_back(i);
}
}
int Find(int n) {
if (father[n] == n) return n;
father[n] = Find(father[n]);
return father[n];
}
void Union(int a, int b) {
int fa = Find(a);
int fb = Find(b);
father[fb] = fa;
}
};
int main() {
int n, m;
char c;
int a, b;
cin >> n >> m;
UnionFind UF(n);
for (int i = 0; i < m; i ++) {
cin >> c >> a >> b;
if (c == 'M') UF.Union(a, b);
else {
if (UF.Find(a) == UF.Find(b)) {
cout << "Yes" << endl;
} else {
cout << "No" << endl;
}
}
}
return 0;
}
与祖宗的关系
并查集维护与祖宗距离详解
*238.银河英雄传说(维护与祖宗的距离)
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 30010;
int m;
int p[N], size[N], d[N];
int find(int x)
{
if (p[x] != x)
{
int root = find(p[x]); // 递归到底
d[x] += d[p[x]]; // 从底往回,更新距离
p[x] = root; // 压缩路径
}
return p[x];
}
int main()
{
scanf("%d", &m);
for (int i = 1; i < N; i ++ )
{
p[i] = i;
size[i] = 1;
}
while (m -- )
{
char op[2]; // 用scanf读单个字符的巧妙写法
int a, b;
scanf("%s%d%d", op, &a, &b);
if (op[0] == 'M')
{
int pa = find(a), pb = find(b);
d[pa] = size[pb];
size[pb] += size[pa];
p[pa] = pb;
}
else
{
int pa = find(a), pb = find(b);
// 必须find一次,才能把d[a],d[b]更新到最新
if (pa != pb) puts("-1");
else printf("%d\n", max(0, abs(d[a] - d[b]) - 1));
}
}
return 0;
}
AcWing 837.连通块中点的个数(累加祖宗上的值)
// 三种操作都包含了——特别是维护每个集合的个数
#include <iostream>
#include <cstdio>
#include <vector>
using namespace std;
const int N = 100010;
int n, m;
int p[N], size[N];
// 只记录根结点的size
int find(int x) { // 返回x的祖宗节点 + 路径压缩
if (p[x] != x) p[x] = find(p[x]);
return p[x];
}
int main() {
string c; // 用string, 不会遇到M,Q1,Q2这种询问的麻烦
int a, b;
cin >> n >> m;
// 初始化
for (int i = 1; i <= n; i ++) {
p[i] = i;
size[i] = 1;
}
// 分三种情况
for (int i = 0; i < m; i ++) {
cin >> c;
if (c == "C") {
cin >> a >> b;
if (find(a) != find(b)) { // 如果已经连通了,就不用再加结点数了,因为我们只看根的size
size[find(b)] += size[find(a)];
}
p[find(a)] = find(b); // union操作 合并
} else if (c == "Q1") {
cin >> a >> b;
if (find(a) == find(b)) puts("Yes");
else puts("No");
} else {
cin >> a;
cout << size[find(a)] << endl;
}
}
return 0;
}
*AcWing1252.搭配购买(累加祖宗上的值)
/**
* @Author: Wilson79
* @Datetime: 2019年12月20日 星期五 15:21:04
* @Filename: 1252.搭配购买(并查集).cpp
*/
#include <iostream>
using namespace std;
const int N = 10005;
int n, m, vol;
int p[N], v[N], w[N], f[N];
int a, b;
int find(int x) {
if (p[x] != x) p[x] = find(p[x]);
return p[x];
}
int main() {
cin >> n >> m >> vol;
for (int i = 1; i <= n; i ++) {
p[i] = i;
}
for (int i = 1; i <= n; i ++) {
cin >> v[i] >> w[i];
}
// 计算每个连通块的v和w 最终一定连通块的祖宗一定满足p[x] == x
for (int i = 1; i <= m; i ++) {
cin >> a >> b;
int pa = find(a), pb = find(b);
if (pa != pb) {
v[pb] += v[pa]; // 累计每个祖宗结点的值
w[pb] += w[pa];
p[pa] = pb;
}
}
// 01背包模版
for (int i = 1; i <= n; i ++) {
if(p[i] == i)
for (int j = vol; j >= v[i]; j --) {
f[j] = max(f[j - v[i]] + w[i], f[j]);
}
}
cout << f[vol] << endl;
return 0;
}
AcWing 1250.格子游戏(二维点阵)
// 2019年12月20日 星期五 15:35:09
#include <iostream>
using namespace std;
const int N = 210;
int p[N * N];
int n, m;
int x, y, a, b;
char c;
int find(int x) {
if (p[x] != x) p[x] = find(p[x]);
return p[x];
}
int main() {
cin >> n >> m;
for (int i = 0; i < n * n; i ++) {
p[i] = i;
}
int i;
for (i = 1; i <= m; i ++) {
cin >> x >> y >> c;
x--, y--;
a = x * n + y;
if (c == 'R') {
b = x * n + y + 1;
} else {
b = (x + 1) * n + y;
}
if (find(a) != find(b)) {
p[find(a)] = find(b);
} else {
cout << i << endl;
break;
}
}
if (i > m) cout << "draw" << endl;
return 0;
}
AcWing 240.食物链(记录与根节点的关系)
/*
0->1->2->0 0是根节点
距离d:记录当前节点到根节点的距离
余1:可以吃根节点
余2:可以被根节点吃 x
余0:与根节点是同类 y
x吃y:x - y mod 3 为1
x被y吃:x - y mod 3 为2
*/
#include <iostream>
using namespace std;
const int N = 50010;
int n, m;
int p[N], d[N];
int find(int x)
{
if (p[x] != x)
{
int t = find(p[x]); // 递归语句必须写在前面,这样可以保证你先到达了根节点,然后倒过来一步步执行后面的语句
// 因为p[a] = b, p[b] = c;你合并了三棵树,这是你再取a树中的点,find(x)它的d[p[x]]需要先更新,因为d[p[x]]还存的是x到a的距离
d[x] += d[p[x]];
p[x] = t;
}
return p[x];
}
// int find(int x) {
// if (p[x] != x) {
// d[x] += d[p[x]]; // 这样写距离会变短,比如你Union了三棵树 d[p[x]]一直没被更新 这个问题我想了很久才搞明白,每次碰到递归就头疼
// p[x] = find(p[x]);
// }
// return p[x];
// }
int main() {
cin >> n >> m;
for (int i = 1; i <= n; i ++) {
p[i] = i;
}
int res = 0;
while (m --) {
int t, x, y;
cin >> t >> x >> y;
if (x > n || y > n) res ++;
else {
int px = find(x), py = find(y);
if (t == 1) { // 判断是否同类
if (px == py && (d[x] - d[y]) % 3 != 0) res ++;
else if (px != py) {
p[px] = py;
d[px] = d[y] - d[x];
}
} else { // t == 2 给出谁吃谁
if (px == py && (d[x] - d[y] - 1) % 3 != 0) res ++;
else if (px != py) { // 不是一个集合,因此需合并
p[px] = py;
d[px] = d[y] - d[x] + 1; // (d[x] + ? - d[y]) mod 3 == 1
}
}
}
}
cout << res << endl;
return 0;
}
LeetCode 765.情侣牵手(并查集)
/**
* @Author: Wilson79
* @Datetime: 2019年12月22日 星期日 00:23:24
* @Filename: 765.情侣牵手(并查集).cpp
*/
/* ====算法分析====
并查集
1.首先初始状态把这N对情侣分别构成一个连通块
2.考虑k对情侣相互错开的情况,他们形成一个环,可以知道需k-1次交换使排列正确
3.这样相互错开的情况,分别构成连通块,最后用N - 连通块个数即为答案
例如:0,1|2,3|... |2N-2,2N-1
|0 3| ... |7 2|...|6 1| 看相对顺序,可以发现这三对构成一个环,只需2次交换
同理还有其他类型的环构成连通块
*/
const int N = 80;
int p[N];
int find(int x) {
if (p[x] != x) p[x] = find(p[x]);
return p[x];
}
class Solution {
public:
int minSwapsCouples(vector<int>& row) {
int n = row.size();
// 初始化p[x]数组
for (int i = 0; i < n; i += 2) {
p[i + 1] = i;
p[i] = i;
}
// |0 3| ... |7 2|...|6 1| 合并0,3 7,2 6,1 最终这两个人到同一连通块
for (int i = 0; i < n; i += 2) {
p[find(row[i + 1])] = find(row[i]);
}
int res = n / 2;
for (int i = 0; i <= n; i ++) {
if (p[i] == i) res --;
}
return res;
}
};
1319.连通网络的操作次数
class Solution {
public:
static const int N = 1e5 + 7;
int p[N];
int find(int x) {
if (p[x] != x) p[x] = find(p[x]);
return p[x];
}
int makeConnected(int n, vector<vector<int>>& connections) {
int Size = connections.size();
for (int i = 0; i < n; i++) {
p[i] = i;
}
int cnt = 0;
for (int i = 0; i < Size; i ++) {
int a = connections[i][0], b = connections[i][1];
int pa = find(a), pb = find(b);
if (pa == pb) cnt ++; // 多余的线
else p[pa] = pb;
}
// 算出连通块的个数,用多余的线去连
int connect = 0;
for (int i = 0; i < n; i ++) {
if (p[i] == i) connect ++;
}
if (connect - cnt > 1) return -1;
return connect - 1;
}
};
并查集 2
并查集是面试笔试和竞赛中非常常用的一个数据结构算法,因为它代码比较精简,但思维含量高
并查集两个操作: 近乎0(1)时间复杂度内快速维护这两个操作
- 将两个集合合并
- 询问两个元素是在一个集合中
一:并union 查 find
判断两个元素是不是在同一个集合就看他们的老大是不是同一个。
1,2; 3,4;两个集合 2和4合并需要选出一个新的老大。
4的原老大是3,现在他最大的老大是1,相当于他原来是个小公司被大公司合并了。所以这个小公司的所有人都要服从于最后的大老板
所以判断一个元素是不是老大就看它的箭头是不是指向它自己。
二:find和union
find:一直顺着箭头找,直到找到最大的boss
union:先找两个元素的老大,再选一个作为最终的老大
三:路径压缩
第一次一步一步找到了最终boss,那么此时建立一个直接到最终boss的箭头,那之后要做查询时就不需要再通过boss的boss的boss一步步找到最终boss了,相当于你直接和最终boss建立了联系。 这就是路径压缩
并查集模板
class UnionFind {
public:
vector<int> father;
UnionFind(int num) {
for (int i = 0; i < num; i ++) {
father.push_back(i);
}
}
int Find(int n) {
if(father[n] == n) return n;
father[n] = Find(father[n]);
return father[n];
}
bool Union(int a, int b) {
int fa = Find(a);
int fb = Find(b);
father[fb] = fa;
return fa == fb; // 判断两个点是否连通
}
};
示例代码
class UnionFind {
public:
vector<int> father;
// num表示元素个数
// 这里的UnionFind定义在这里其实是为了方便初始化并查集
// 如UnionFind UF(n);
UnionFind(int num) {
for (int i = 0; i < num; i ++) {
father.push_back(i); // 箭头指向自己
}
}
// 4->3->1->1 finish
int Find(int n) {
// 非递归
while(father[n] != n) {
n = father[n];
}
return n;
}
int Find(int n) {
// 递归
if (father[n] == n) return n;
return Find(father[n]);
}
// 不仅返回了4的最终boss,还让4和1建立了一个联系,方便下次直接找1
int Find(int n) {
// 递归 + 路径压缩
if(father[n] == n) return n;
father[n] = Find(father[n]);
return father[n]; // 这里改为Find(father[n])也是可以的,但是会多占用很多空间
}
void Union(int a, int b) {
int fa = Find(a); // 2的原老大是1
int fb = Find(b); // 4的原老大是3
father[fb] = fa; // 让3指向1
}
};
LeetCode 547.FriendCircles
// 版本一 UnionFind初始化
class UnionFind {
public:
vector<int> father;
UnionFind(int num) {
for (int i = 0; i < num; i ++) {
father.push_back(i);
}
}
int Find(int n) {
// 递归 + 路径压缩
if(father[n] == n) return n;
father[n] = Find(father[n]);
return father[n];
}
void Union(int a, int b) {
int fa = Find(a); // 2的原老大是1
int fb = Find(b); // 4的原老大是3
father[fb] = fa; // 让3指向1
}
};
class Solution {
public:
int findCircleNum(vector<vector<int>> &M) {
int n = M.size();
UnionFind UF(n);
for (int i = 0; i < n; i ++) {
for (int j = 0; j < n; j ++) {
if (M[i][j] == 1) UF.Union(i, j);
}
}
int res = 0;
for (int i = 0; i < n; i ++) {
if (UF.Find(i) == i) {
res ++;
}
}
return res;
}
};
LeetCode 200.NumberOfIslands
// 并查集
// 向四个方向进行Union操作,注意两个参数的先后,后面的服从前面的
class UnionFind {
public:
vector<int> father;
UnionFind(int num) {
for (int i = 0; i < num; i ++) {
father.push_back(i);
}
}
int Find(int n) {
if (father[n] == n) return n;
father[n] = Find(father[n]);
return father[n];
}
void Union(int a, int b) {
int fa = Find(a);
int fb = Find(b);
father[fb] = fa;
}
};
int encode(int i, int j, int m) {
return i * m + j;
}
class Solution {
public:
int numIslands(vector<vector<char>> &grid) {
if (!grid.size() || !grid[0].size()) return 0;
int n = grid.size(), m = grid[0].size();
UnionFind UF(n * m);
// 合并
int dx[] = {1, 0, -1, 0}, dy[] = {0, -1, 0, 1};
for (int i = 0; i < n; i ++) {
for (int j = 0; j < m; j ++) {
if (grid[i][j] == '1') {
for (int d = 0; d < 4; d ++) {
int x = dx[d] + i, y = dy[d] + j;
if (x >= 0 && x < n && y >= 0 && y < m && grid[x][y] == '1') {
UF.Union(encode(i, j, m), encode(x, y, m));
}
}
}
}
}
// 查找
int res = 0;
for (int i = 0; i < n; i ++) {
for (int j = 0; j < m; j ++) {
if (grid[i][j] == '1' && UF.Find(encode(i, j, m))== encode(i, j, m)) {
res ++;
}
}
}
return res;
}
};
// dfs算法
void dfs(vector<vector<char>> &grid, int a, int b) {
//因为要修改原来的grid,所以这里必须用引用
int dx[] = {1, -1, 0, 0}, dy[] = {0, 0, 1, -1};
int m = grid.size(), n = grid[0].size();
grid[a][b] = '0';
for (int i = 0; i < 4; i++) {
int x = a + dx[i], y = b + dy[i];
if (x >= 0 && x < m && y >= 0 && y < n && grid[x][y] == '1') {
dfs(grid, x, y);
}
}
return;
}
class Solution {
public:
int numIslands(vector<vector<char>> &grid) {
int ans = 0;
if (!grid.size() || !grid[0].size()) return 0;
int m = grid.size(), n = grid[0].size();
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (grid[i][j] == '1') {
dfs(grid, i, j);
ans++;
}
}
}
return ans;
}
};
LeetCode 684.Redundant Connection
// 判断两个顶点是否连通,看他们的祖先是不是相同
// 685进阶题
class UnionFind
{
public:
vector<int> father;
UnionFind(int num)
{
for (int i = 0; i < num; i++)
{
father.push_back(i);
}
}
int Find(int n)
{
if (father[n] == n)
return n;
father[n] = Find(father[n]);
return father[n];
}
bool Union(int a, int b)
{
int fa = Find(a);
int fb = Find(b);
father[fb] = fa;
return fa == fb; // 判断两个点是否连通
}
};
class Solution
{
public:
vector<int> findRedundantConnection(vector<vector<int>> &edges)
{
int n = edges.size();
UnionFind UF(n);
for (int i = 0; i < n; i++)
{
int x = edges[i][0], y = edges[i][1];
// 初始化是0,1..n-1,所有下标要先减1,不然会越界
if (UF.Union(x - 1, y - 1))
{
return {x, y};
}
}
return {1, 1};
}
};