随便写写
上周说过了,自己写了一个Redundant Connection II的题目,难度级别是hard的,但是看了评论区网友的解答自己又做了答案之后觉得挺简单的,就准备随便给个博客写一下(就不当做作业的博客了)。但是后来发现这个题还有一个无向图的版本Redundant Connection,仍然采用Redundant Connection II类似的做法,发现无法通过测试,我把错误样例分析了一下发现之前的做法根本不正确,只不过leetcode测试样例有限,没有测试出错误。于是把这道题拿出来仔细审视了一下,发现之前的做法不对(可见评论区网友也不一定对,虽然那篇评论浏览数很高,但是错的就是错的),希望以后自己以后对什么东西都能保持一定的理性思考,emmm,这很重要。
然后自己用dfs/disjoint union两种方法做了一遍,发现其中dfs方法不仅代码难写,而且复杂度也高,disjoint union恰恰相反。
下面是对于这道题 有向图/无向图 两个不同版本的题解(从中主要收获了学习disjoint set union数据结构,这是算法这门课程以后会涉及到的,但是在学数据结构的时候是没怎么用过)
正文
题目描述
问题分析
题目大概意思是:给出一个特殊的无向图结构,它是一个有n个节点的树 加 一条额外的边构成的图,我们需要找出额外的那条边,从而将图还原为树。如果有多个这样的边,我们就取最后一个
解题思路&算法步骤及其复杂度分析
方法一(一个很质朴的想法):
- 解题思路:
对于一个特殊的无向图,它是一个n个节点的树,在它的节点中取两个本来不相连的节点连接起来,那么这个新形成的图必然构成环。因为从连接这两个点的路径必然包括:1.第一点->经过根节点->到第二点,2.第一点->直接到第二点,在无向图情形下,就形成 “第一点->…->根节点->…->第二点->第一点” 的环。我们通过遍历找到这个环,然后找到环上最后出现的边就行了。 - 算法步骤 :
- 随便找一个点,然后对这个点进行深度优先遍历,那么必然能够在遍历完所有节点之前/之后找到环(如果这个起始点刚好在环上面,那么对它深度优先遍历能够回到起点;如果起始点不在环上,那么对它深度优先遍历也能到达图上任何的节点,因为这是个连通图,所以自然能遍历到环上的点,那么最后自然也能够找到环),将这一路经过的边存起来,记为line
- 然后遍历line中的边,直到遇到一个点,它的终点就是整个集合的终点,这就是整个环的起点,以此截取环的部分,记为cycle
- 之后遍历题目中输入的edges边集合,查找最后一个出现在cycle中的元素。
- 复杂度分析:
首先是第一步中深度优先搜索一共需要遍历n个节点,并且对于每个节点都需要判断它所在的边是否在现有的cycle集合中,cycle集合中的元素个数为O(n)级别的,所以这个步骤的复杂度为nO(n)=O(n^2);然后是对cycle遍历查找环的起始边,cycle大小是O(n)级别的,所以这个复杂度为O(n);最后对于edges的遍历也是O(n),所以这个算法复杂度为O(n^2)
- 解题思路:
方法二(使用disjoint set union):
背景知识:
- 首先我们需要知道叫做disjoint set union(不相交集)的数据结构:
顾名思义,如果对一组集合的元素(在这个题目可以把节点看作这样的元素)按照某种规则分类,那么可以将它们分成若干个不相交的集合,这些集合就是不相交集。 接下来我们需要了解一些关于disjoint set union的细节:
它常被用来表示连通的图 以及 快速查询两个元素是否连通
根据定义描述我们需要支持至少两个操作:1.查询两个元素是否连通(find x将返回一个该集合的id,这个id唯一对应该集合,对于两个连通的点该操作返回相同id),2.合并两个不相交集(union x, y会把两个点集合并,对应的实际含义就是连通两个非连通的子图,然后选出一个新的id代表这个集合)
一个简单的实现是:每个点使用parent连接上一个点,一直追溯parent直到parent就是自己(可以认为是该子图的根),将这个点作为该集合的id;合并操作就是将某个集合id1设置为另一个集合id2的parent,如此一来这两个集合中任何点都能够追溯到id1;如下代码所示:int find(x) { while (parent[x] != x) x = parent[x]; return x; } int union(x, y) { parent[find(x)] = find(y); }
嗯,如果按照上面的想法做这个题目,代码会简单易懂,但是算法复杂度并不会改变,仍然是O(n^2),原因是外层是大小为n的edges容器,内层的find操作每次就是一个寻找树根的操作,这个操作对于一颗普通的比较均衡的树而言是O(log(n)),但是这里面是一未知状况的树,所以我们应该认为寻根的复杂度为O(n),所以总共的复杂度为O(n^2)
- 但是其实有更好的方法,就是使用路径压缩和按秩合并的策略
路径压缩:就是在进行find操作的时候,将路径上所有节点的父节点都设置为根节点,这样在下次对同一个元素进行find操作时复杂度就是O(1)。具体实现是将前面提到的伪代码中x = parent[x]
改为parent[x] = find(parent[x])
按秩合并:就是使用带权合并的策略,我们将一棵树的高度记为rank,将它作为权,我们在合并两个union的时候,取权重高的作为父节点,这样能够使得一棵树尽可能的平衡(试想我们将高度高的树作为子树接到高度低的树上,那么树将偏向一边),从而在搜索的时候减少回溯的次数(只需要较少的次数就能够找到根)。具体实现是在合并l两点u,v分属的两个集合之前比较rank[find(u)]
和rank[find(v)]
,将权重更大的集合的id设置为权重小的集合的id的parent。
相关的复杂度分析在稍后提到
- 首先我们需要知道叫做disjoint set union(不相交集)的数据结构:
解题思路:
如果利用不相交集,这个题目可以这样做:将“两点是否连通”作为分类条件,那么不连通的两个点自然处于不相同的集合,连通的两点处于相同的集合。我们可以遍历给出的这个图的edges集合,其中每一条edge都连通两点,更准确广泛的说是连通这两个点所在的集合(通过这条边,与第一个点相连通的点集都能 和 与第二个点连通的点集相连),这样我们就把这两个集合合并,反复如此,直到遍历到的某条边的两个顶点已经是连通的,那么这条边会导致成环,这就是我们要找的边。算法步骤 :
- 遍历给出的edges集合
- 对于集合中每一条边上的两点u,v判断
find(x) == find(y)
是否成立,如果成立说明这条边造成环(因为这条边连接了两个本来就连通的点,每条边只出现以此所以之前的连通不是通过这条边实现,所以现在有两条路径连接两个点,自然成环)并输出它,如果不成立则继续上述操作直到遇到造成环的边。 - 上述操作都应使用前面背景知识中提到的路径压缩和按秩合并策略
- 复杂度分析:
在算法导论一书的330页有详细的证明,这里就不说怎么证明(很复杂,如果在这里给出那么博客将增长一倍),如果有需要可以参考算法导论第三版P330
根据算法导论一书,同时使用前面提到的两种策略情况下,使用n个find操作和n个union操作的复杂度是O(n*α(n)),其中α(n)在大多数应用场景下不超过4,所以这里我们可以认为这个复杂度为O(1),所以使用这个方法解题的复杂度只有O(n)
可以说相比方法一是有了质的飞跃。
代码实现&运行结果分析
684使用方法一实现:
这个方法一看代码量就知道蛇皮麻烦,别人用的方法虽然也是O(n^2),但是在环的寻找上好多了,别人的方法在这里
class Solution {
public:
vector<vector<int>> cycle;
bool isOver = false;
int endPoint = -1;
vector<int> res;
bool myFind(int level, vector<vector<int>>& cycle, vector<int>& temp) {
for ( auto i = cycle.begin() + level; i != cycle.end(); i++ ) {
if (*i == temp) return true;
}
return false;
}
void BFS(int curNode, vector<vector<int>>& treeNode, vector<bool>& isVisited) {
if (isVisited[curNode]) {
endPoint = curNode;
isOver = true;
return;
}
isVisited[curNode] = true;
//cout <<curNode<< endl;
for ( auto i = treeNode[curNode].begin(); i != treeNode[curNode].end(); i++ ) {
int l = (*i) > curNode ? curNode : (*i);
vector<int> temp = { l, (*i) + curNode - l };
if ( !myFind(0, cycle, temp) ) {
cycle.push_back(temp);
BFS(*i, treeNode, isVisited);
if (isOver) return;
}
}
cycle.pop_back();
}
vector<int> findRedundantConnection(vector<vector<int>>& edges) {
vector<vector<int>> treeNode(edges.size() + 1);
vector<bool> isVisited(edges.size()+1, false);
for ( auto i = edges.begin(); i != edges.end(); i++ ) {
treeNode[(*i)[0]].push_back((*i)[1]);
treeNode[(*i)[1]].push_back((*i)[0]);
}
BFS(edges[0][0], treeNode, isVisited);
int level = 0;
int curNode = edges[0][0];
while (curNode != endPoint) {
curNode = cycle[level][0] == curNode ? cycle[level][1] : cycle[level][0];
level++;
}
for ( auto i = edges.begin(); i != edges.end(); i++ ) {
if ( myFind( level, cycle, (*i) ) ) {
res = *i;
}
}
return res;
}
};
684使用方法二实现:
class Solution {
public:
int findP(int x, int* & parents) {
if ( parents[x] == x ) return x;
else {
parents[x] = findP(parents[x], parents);
return parents[x];
}
}
void unionP(int x, int y, int* & parents, int* & ranks) {
if (ranks[findP(x, parents)] < ranks[findP(y, parents)]) {
parents[findP(x, parents)] = findP(y, parents);
ranks[findP(y, parents)]++;
} else {
parents[findP(y, parents)] = findP(x, parents);
ranks[findP(x, parents)]++;
}
}
vector<int> findRedundantConnection(vector<vector<int>>& edges) {
int* ranks = new int[edges.size() + 1];
int* parents = new int[edges.size() + 1];
for (int i = 0; i < edges.size() + 1; ++i)
{
ranks[i] = 0;
parents[i] = i;
}
for ( auto i = edges.begin(); i != edges.end() ; i++) {
if ( findP((*i)[0], parents) == findP((*i)[1], parents) ) {
return (*i);
} else {
unionP((*i)[0], (*i)[1], parents, ranks);
}
}
return *edges.begin();
}
};
使用方法一和方法二的结果情况对比:(显然方法二更好,超过99%以上,,,)
685代码实现:这里说一下,685直接使用dfs也能够达到O(n)复杂度,这是因为它是单向的图,所以任意一点寻根必能找到根/环,这个操作复杂度为O(n),如果直接找到环那么遍历一遍edges即可得到解,如果是找到根,那么再多遍历一次也能够找到环,即使是两个O(n)操作相加,最终还是O(n)复杂度
class Solution {
public:
vector<vector<int>> cycle;
bool isOver = false;
int endPoint = -1;
vector<int> res;
vector<int> findRedundantDirectedConnection(vector<vector<int>>& edges) {
vector<vector<int>> parents(edges.size()+1);
int order [edges.size() + 1]{};
vector<bool> isVisited(edges.size()+1, false);
int twoParentNode = -1;
int count = 0;
for ( auto i = edges.begin(); i != edges.end(); i++ ) {
count++;
parents[(*i)[1]].push_back((*i)[0]);
order[(*i)[1]] = count;
if ( parents[(*i)[1]].size() > 1 ) {
twoParentNode = (*i)[1];
}
}
if ( twoParentNode != -1 ) {
int curNode = parents[twoParentNode][0];
while ( parents[curNode].size() != 0 && curNode != twoParentNode ) {
curNode = parents[curNode][0];
}
if ( parents[curNode].size() == 0 ) {
return {parents[twoParentNode][1], twoParentNode};
} else {
return {parents[twoParentNode][0], twoParentNode};
}
}
int curNode = edges[0][0];
isVisited[curNode] = true;
while ( !isVisited[parents[curNode][0]] ) {
curNode = parents[curNode][0];
isVisited[curNode] = true;
}
int max = -1;
vector<int> res;
isVisited.assign( edges.size(), false );
while ( !isVisited[curNode] ) {
isVisited[curNode] = true;
if ( max < order[curNode] ) {
max = order[curNode];
res = {parents[curNode][0], curNode};
}
curNode = parents[curNode][0];
}
return res;
}
};
对应结果:
因为是O(N)复杂度所以效果也很不错。