并查集
并查集主要有三个功能。
- 寻找根节点,函数:find(int u),也就是判断这个节点的祖先节点是哪个
- 将两个节点接入到同一个集合,函数:join(int u, int v),将两个节点连在同一个根节点上
- 判断两个节点是否在同一个集合,函数:isSame(int u, int v),就是判断两个节点是不是同一个根节点
并查集可以解决的问题:两个节点是否在一个集合,也可以将两个节点添加到一个集合中。
难点在于根的路径压缩的理解
寻找图中是否存在路径
1971. 寻找图中是否存在路径
有一个具有 n
个顶点的 双向 图,其中每个顶点标记从 0
到 n - 1
(包含 0
和 n - 1
)。图中的边用一个二维整数数组 edges
表示,其中 edges[i] = [ui, vi]
表示顶点 ui
和顶点 vi
之间的双向边。 每个顶点对由 最多一条 边连接,并且没有顶点存在与自身相连的边。
请你确定是否存在从顶点 source
开始,到顶点 destination
结束的 有效路径 。
给你数组 edges
和整数 n
、source
和 destination
,如果从 source
到 destination
存在 有效路径 ,则返回 true
,否则返回 false
。
class Solution {
public:
bool validPath(int n, vector<vector<int>>& edges, int source, int destination) {
/*
深搜 / 广搜
这里选择使用并查集进行实现
使用并查集判断两个元素是否在同一个集合内部:
step1: 使用join(u,v)把每条边加入到并查集
step2: 使用 isSame(int u,int v) 判断是否是同一个根【即是否属于同一个集合】
*/
// step0: 并查集初始化
init(n);
// step1: 把每条边加入并查集
for(vector<int> edge : edges) { // 每个元素就是一条边
join(edge[0],edge[1]);
}
// step2: 使用 isSame(int u,int v) 判断是否是同一个根
return isSame(source, destination);
}
private:
vector<int> father = vector<int>(200001,0) ; // 按照节点的大小定义数组长度
void init(int n) { // 并查集初始化
for(int i = 1; i <= n; i++) {
father[i] = i; //初始化。每个元素都是自己的根
}
}
// 并查集里寻找根的过程
int find(int u) {
return u== father[u] ? u : father[u] = find(father[u]);
}
// 判断 u 和 v 是否找到同一个根
bool isSame(int u, int v) {
u = find(u);
v = find(v);
return u == v;
}
// 把 v-> u 这条边加入并查集 father[v] = u
void join(int u, int v) {
// 先判断两个元素是否在同一个集合内部
u = find(u);
v = find(v);
if(u == v) return;
father[v] = u;
}
};
冗余连接
684. 冗余连接
树可以看成是一个连通且 无环 的 无向 图。
给定往一棵 n
个节点 (节点值 1~n
) 的树中添加一条边后的图。添加的边的两个顶点包含在 1
到 n
中间,且这条附加的边不属于树中已存在的边。图的信息记录于长度为 n
的二维数组 edges
,edges[i] = [ai, bi]
表示图中在 ai
和 bi
之间存在一条边。
请找出一条可以删去的边,删除后可使得剩余部分是一个有着 n
个节点的树。如果有多个答案,则返回数组 edges
中最后出现的那个。
class Solution {
public int[] findRedundantConnection(int[][] edges) {
/**
图论:删除相对于数来说的多余的一条边
使用并查集的思想:
把每条边都加入到其中,如果在加入的时候发现两个顶点已经同根;(即在一个并查集中)
此时就说明这条边是一条冗余边,删除这条边即可
*/
int[] ans = null;
init(edges.length);
for(var edge : edges) {
if(!join(edge[0],edge[1])) {
ans = edge;
break;
}
}
return ans;
}
private int[] father;
private void init(int vLen) { // 并查集的初始化 // 传入顶点数
father = new int[vLen+1];
for(int i=0; i < vLen; i++) {
father[i] = i; // father[i] = i; 自身是自身的根,即刚开始所有节点都是单项的
}
}
// 找到一个元素的根
int find(int u) {
return father[u] == u ? u: (father[u] = find(father[u]));
}
// 把 u->v 加入并查集
private boolean join(int u, int v) {
u = find(u);
v = find(v);
if(u == v) return false;
father[u] = v;
return true;
}
// 判断两个节点是否同根
public boolean isSame(int u, int v) {
u = find(u);
v = find(v);
return u == v;
}
}
冗余连接Ⅱ
685. 冗余连接 II
在本问题中,有根树指满足以下条件的 有向 图。该树只有一个根节点,所有其他节点都是该根节点的后继。该树除了根节点之外的每一个节点都有且只有一个父节点,而根节点没有父节点。
输入一个有向图,该图由一个有着 n
个节点(节点值不重复,从 1
到 n
)的树及一条附加的有向边构成。附加的边包含在 1
到 n
中的两个不同顶点间,这条附加的边不属于树中已存在的边。
结果图是一个以边组成的二维数组 edges
。 每个元素是一对 [ui, vi]
,用以表示 有向 图中连接顶点 ui
和顶点 vi
的边,其中 ui
是 vi
的一个父节点。
返回一条能删除的边,使得剩下的图是有 n
个节点的有根树。若有多个答案,返回最后出现在给定二维数组的答案。
class Solution {
public:
/* 算法分成三种情况:1:找到入度为2的节点,删除其中的一条边,要注意删除边后剩余的部分依然能构成一颗有向树
情况2:如果没有入度为2的节点,则说明题目中有环,删除构成环的边即可
*/
vector<int> findRedundantDirectedConnection(vector<vector<int>>& edges) {
// 顶点数 = 边数
int n = edges.size();
vector<int> inDegree(n+1,0);
// step1: 先统计每个节点的入度
for(vector<int> edge : edges) {
inDegree[edge[1]] ++;
}
// for(int degree : inDegree) {
// cout << degree << endl;
// }
// return inDegree;
// 情况1和2:
// 记录其中入度为2的边(若有的话就两条边)
vector<int> edge;
// 从后往前:因为优先要删除后面的那条边
for(int i = n-1; i>=0; i--) {
if(inDegree[edges[i][1]] == 2) { // 这条边后面的入度节点为2
edge.push_back(i); // edge存入的是要删除边的下标
}
}
// 考虑情况1与2
if(edge.size() > 0){
if(isTreeAfterRemoveEdge(edges,edge[0])) {
return edges[edge[0]];
}else{
return edges[edge[1]];
}
}
// 情况三
// 此时只有入度为1的顶点,即一定会存在有向环,需要找到构成环的边返回
return getRemoveEdge(edges);
}
private:
vector<int> father = vector<int>(1001,0);
// 并查集
void init(int n) {
for(int i = 1; i <= n; ++i) {
father[i] = i;
}
}
// 寻找根
int find(int u) {
return u == father[u] ? u : father[u] = find(father[u]);
}
// 将 v->u 这条边加入并查集
void join(int u,int v) {
u = find(u);
v = find(v);
if(u == v) return;
father[v] = u;
}
// 判断u与v是否找到同一个根(即是否在同一棵上)
bool isSame(int u, int v) {
u = find(u);
v = find(v);
return u == v;
}
// 删除一条边后判断是不是树
bool isTreeAfterRemoveEdge(const vector<vector<int>>& edges, int delEdge) {
int n = edges.size();
init(n);
for(int i=0; i<n ; i++) {
if(i == delEdge) continue;
if(isSame(edges[i][0],edges[i][1])) {
return false; // 构成了有向环,则一定不是树
}
join(edges[i][0],edges[i][1]); // 两条边加入并查集
}
return true;
}
// 在已经是环的情况下找到删除的那边条
// 如果说一条边的两端点已经在并查集中,那这条边不就是多余的吗
vector<int> getRemoveEdge(const vector<vector<int>>& edges) {
int n = edges.size();
init(n);
int ans = 0;
for(int i=0; i<n; i++) {
if(isSame(edges[i][0],edges[i][1])) { // 构成环了就是要找的边
ans = i;
break;
}else {
join(edges[i][0], edges[i][1]);
}
}
return edges[ans];
}
};