代码随想录算法训练营第60期第五十九天打卡
大家好,我们昨天学习的是并查集,我们大致了解了并查集的一些基本功能,比如把两个元素添加到一个集合里和判断两个元素是不是在同一个集合中,我们也是用并查集去解决一些例题,今天我们就继续来使用并查集来解决一些实际的面试中可能会考的题目。
第一题对应卡码网编号为108的题目冗余的边
这道题目我给大家说一下大体的题意,有一个图,它是一棵树,他是拥有 n 个节点(节点编号1到n)和 n - 1 条边的连通无环无向图,现在在这棵树上的基础上,添加一条边(依然是n个节点,但有n条边),使这个图变成了有环图,现在先请你找出冗余边,删除后,使该图可以重新变成一棵树。其实我们是要找到那条边删除后就可以重新变成一棵树,但是大家注意如果存在多条边就请删除标准输入中最后出现的那条边,我们这里大概率是需要考虑并查集来解决这道题目,其实我们就可以从前向后遍历每一条边(因为优先让前面的边连上),边的两个节点如果不在同一个集合,就加入集合(即:同一个根节点)。如果两个节点不在一个集合就可以将两条边连接起来,如果边的两个节点已经出现在同一个集合里,说明着边的两个节点已经连在一起了,再加入这条边一定就出现环了。大家可以看一下图:
这里我们已经判断 节点A 和 节点B 在在同一个集合(同一个根),如果将 节点A 和 节点B 连在一起就一定会出现环。那如果有了这个思路我们就可以尝试写出代码:
#include <iostream>
#include <vector>using namespace std;int n;
vector<int> father(1001, 0);
//初始化
void init()
{for (int i = 0; i < father.size(); ++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;
}//把u,v添加到同一个集合中void join(int u, int v)
{u = find(u);v = find(v);if (u == v) return;father[v] = u;
}int main()
{int s, t;cin >> n;init();while(n--){cin >> s >> t;if (isSame(s, t)){cout << s << " " << t << '\n';return 0;}else{join(s, t);}}return 0;
}
这道题目其实不难了,大家还是需要理解好并查集的基本操作就可以。大家需要清楚我们什么情况下会出现环,然后什么情况下的边可以相连。
第二题对应卡码网编号为109的题目冗余连接II
这道题的大致要求是有一种有向树,该树只有一个根节点,所有其他节点都是该根节点的后继。该树除了根节点之外的每一个节点都有且只有一个父节点,而根节点没有父节点。有向树拥有 n 个节点和 n - 1 条边。现在有一个有向图,有向图是在有向树中的两个没有直接链接的节点中间添加一条有向边。输入一个有向图,该图由一个有着 n 个节点(节点编号 从 1 到 n),n 条边,请返回一条可以删除的边,使得删除该条边之后该有向图可以被当作一颗有向树。输出一条可以删除的边,若有多条边可以删除,请输出标准输入中最后出现的一条边。大家看完题目应该就可以意识到这道题应该与上一道题目差不多的思路,都还是使用并查集,似乎不一样的地方就在于这里是一棵有向树,而上一道题目是一棵无向树,我们就来看看这道题目我们应该如何思考?有向图的话其实复杂一些,因为父与子务必要区分清楚。
我们来想一下 有向树的性质,如果是有向树的话,只有根节点入度为0,其他节点入度都为1(因为该树除了根节点之外的每一个节点都有且只有一个父节点,而根节点没有父节点)。所以情况一:如果我们找到入度为2的点,那么删一条指向该节点的边就行了。
这是第一种情况的示意图,还会有第二种情况:只能删特定的一条边,如下图所示:
上面这种情况其实就只能删除1,3边不能删除4,3边,这点大家务必要清楚,一定要搞清楚所有的情况,综上,如果发现入度为2的节点,我们需要判断 删除哪一条边,删除后本图能成为有向树。如果是删哪个都可以,优先删顺序靠后的边。情况三: 如果没有入度为2的点,说明 图中有环了(注意是有向环)。大家可以看看下面的这种情况:
其实这种情况的话我们只需要删除构成环的边就可以了,这种情况不难考虑,重点是上面这两种情况大家得区分好我们需要删除哪条边,尤其是第二种情况我们务必注意,我们这里是需要统计节点的入度,那么有了思路我们就可以尝试写一下解题代码:
#include <iostream>
#include <vector>
using namespace std;
int n;
vector<int> father (1001, 0);
// 并查集初始化
void init() {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 same(int u, int v) {u = find(u);v = find(v);return u == v;
}// 在有向图里找到删除的那条边,使其变成树
void getRemoveEdge(const vector<vector<int>>& edges) {init(); // 初始化并查集for (int i = 0; i < n; i++) { // 遍历所有的边if (same(edges[i][0], edges[i][1])) { // 构成有向环了,就是要删除的边cout << edges[i][0] << " " << edges[i][1];return;} else {join(edges[i][0], edges[i][1]);}}
}// 删一条边之后判断是不是树
bool isTreeAfterRemoveEdge(const vector<vector<int>>& edges, int deleteEdge) {init(); // 初始化并查集for (int i = 0; i < n; i++) {if (i == deleteEdge) continue;if (same(edges[i][0], edges[i][1])) { // 构成有向环了,一定不是树return false;}join(edges[i][0], edges[i][1]);}return true;
}int main() {int s, t;vector<vector<int>> edges;cin >> n;vector<int> inDegree(n + 1, 0); // 记录节点入度for (int i = 0; i < n; i++) {cin >> s >> t;inDegree[t]++;//终点的入度加加edges.push_back({s, t});//这里存储的是每一条边}vector<int> vec; // 记录入度为2的边(如果有的话就两条边)// 找入度为2的节点所对应的边,注意要倒序,因为优先删除最后出现的一条边for (int i = n - 1; i >= 0; i--) {//如果一个点的入度是2的话我们就将这个点保存到数组里去//因为后续删除的边很可能是以这个点为终点的其余一条边if (inDegree[edges[i][1]] == 2) {vec.push_back(i);}}// 情况一、情况二(如果存在度为2的节点)if (vec.size() > 0) {// 放在vec里的边已经按照倒叙放的,所以这里就优先删vec[0]这条边if (isTreeAfterRemoveEdge(edges, vec[0])) {cout << edges[vec[0]][0] << " " << edges[vec[0]][1];} else {cout << edges[vec[1]][0] << " " << edges[vec[1]][1];}return 0;}// 处理情况三// 明确没有入度为2的情况,那么一定有有向环,找到构成环的边返回就可以了getRemoveEdge(edges);
}
这道题目难度较大,主要在于对于有向图来说不容易判断是否形成环,这个与无向图不一样,有向图还涉及到入度问题,入度为2的边会形成环我们要删除哪一条边我还需要判断删除之后还是不是树,其实这个倒不难理解,只要一条边的起终点还是在一个集合里说明存在回路就不会是树,这个希望大家理解。
今日总结
今天的并查集的题目有难度,大家仔细思考,尤其是注意如何判断无向图与有向图是否有环,而且尤其注意有向图还需要考虑入度的多种情况,大家务必区分理解这些思路,我们今天就分享这么多,我们下次再见!