github项目:地址
(1)掌握图的连通性。
(2)掌握并查集的基本原理和应用。
桥的定义
在图论中,一条边被称为“桥”代表这条边一旦被删除,这张图的连通块数量会增加。等价地说,一条边是一座桥当且仅当这条边不在任何环上。一张图可以有零或多座桥。
图 1 没有桥的无向连通图
图 2 这是有16个顶点和6个桥的图
(桥以红色线段标示)
求解问题
找出一个无向图中所有的桥。
算法
(1)基准算法
For every edge (u, v), do following a) Remove (u, v) from graph b) See if the graph remains connected (We can either use BFS or DFS) c) Add (u, v) back to the graph.
(2)应用并查集设计一个比基准算法更高效的算法。不要使用Tarjan算法,如果使用Tarjan算法,仍然需要利用并查集设计一个比基准算法更高效的算法。
算法原理
(1) 由桥的定义可知,删除桥之后,图的连通分支数量就会增加。所有只需要比较删除前后连通分支数量即可判断该边是否为桥。
(2) 利用DFS算法计算连通分支数量。
(3) 计算连通分支数量方法:
伪代码
BASIC: A = Count_connected_components For every edge (u, v): Remove (u, v) from graph B = Count_connected_components If B > A (u,v) is a bridge Add (u, v) back to the graph.
Count_connected_components: Count = 0 For every vertex (v): If visited[v] = false: DFS (v) Count++ return Count
DFS(v): visited[v] = true for every adjacent points (next) of (v): if visited[next] = false: DFS (next)
时间复杂度分析
对全图做DFS需要
O
(
n
+
e
)
O(n+e)
O(n+e)
有e条边,需要对全图做DFS,总时间复杂度为
O
(
e
n
+
e
2
)
O(en+e^2)
O(en+e2)
对于稀疏图
e
=
n
e = n
e=n,对于稠密图
e
=
n
2
e = n^2
e=n2
稀疏图:
O
(
n
2
)
O(n^2)
O(n2),稠密图:
O
(
n
4
)
O(n^4)
O(n4)
优化模型
IMPROVED_BASIC: For every edge (u, v): Remove (u, v) from graph DFS (u) If v is visited during DFS (u,v) is a bridge Add (u, v) back to the graph.
时间复杂度分析:
有e条边,需要做1次DFS,总时间复杂度上限为
O
(
e
n
+
e
2
)
O(en+e^2)
O(en+e2)
对于稀疏图
e
=
n
e = n
e=n,对于稠密图
e
=
n
2
e = n^2
e=n2
稀疏图:
O
(
n
2
)
O(n^2)
O(n2),稠密图:
O
(
n
4
)
O(n^4)
O(n4)
优化分析:
图越稀疏时,连通分支增多,优化后只需要对一个连通分支进行DFS,可以极大程度上增快搜索速度。
对于10000个节点,对比算法效率。
5. 数据分析
稀疏图:
e
=
n
e = n
e=n ,时间复杂度:
O
(
n
2
)
O(n^2)
O(n2)
稠密图:
e
=
n
2
e = n^2
e=n2, 时间复杂度:
O
(
n
4
)
O(n^4)
O(n4)
*并查集 图源自:https://zhuanlan.zhihu.com/p/93647900
初始化: Init() for i = 0 to n parent[i] = i;
查询 Get_Parent(x) if(parent[x] == x) return x; else return Get_Parent(parent[x]);
合并 Union(i, j) parent[Get_Parent(i)] = Get_Parent(j);
a. 路径压缩
随着链越来越长,我们想要从底部找到根节点会变得越来越难。可以使用路径压缩的方法。既然只关心一个元素对应的根节点,那就将每个元素直接指向根节点。
实现的方式是在查询的过程中,把沿途的每个节点的父节点都设为根节点即可。下一次再查询时,就可以省很多事。
Get_Parent(x) If (x == parent[x]) return x; else parent[x] = Get_Parent(parent[x]);//父节点设为根节点 eturn parent[x]; //返回父节点
b. 平衡处理
由于路径压缩只在查询时进行,也只压缩一条路径,所以并查集最终的结构仍然可能是比较复杂的。
显然应该把简单的树往复杂的树上合并,而不是相反。因为这样合并后,到根节点距离变长的节点个数比较少。
实现方法是用一个数组rank[]记录每个根节点对应的树的深度(如果不是根节点,其rank相当于以它作为根节点的子树的深度)。一开始,把所有元素的rank设为1。合并时比较两个根节点,把rank较小者往较大者上合并。
Union(i, j) x = Get_Parent(i), y = Get_Parent(j); //先找到两个根节点 if (rank[x] <= rank[y]) parent[x] = y; else parent[y] = x; if (rank[x] == rank[y] && x != y) rank[y]++; //如果深度相同且根节点不同,则新的根节点的深度+1
BASIC_Union: A = Count_connected_components B = v For every edge (e1, e2): Remove (e1, e2) from graph For every edge (e3, e4): Union (e3, e4) # if parent[e3] = parent[e4], B = B - 1 If B > A (e1, e2) is a bridge Add (e1, e2) back to the graph.
复杂度分析
初始化为求连通分支数,时间复杂度
O
(
e
2
)
O(e^2)
O(e2)
外层循环需要遍历e次删除情况
内层循环中每条边需要做2次Get_Parent操作,每次时间复杂度为
O
(
α
(
n
)
)
O(\alpha(n))
O(α(n))
循环总时间复杂度为
O
(
e
∗
(
e
∗
2
∗
α
(
n
)
)
)
=
O
(
e
2
)
O(e*(e*2*\alpha(n)))=O(e^2)
O(e∗(e∗2∗α(n)))=O(e2)
总体时间复杂度为
O
(
e
2
)
O(e^2)
O(e2)
对于稀疏图
e
=
n
e = n
e=n,对于稠密图
e
=
n
2
e = n^2
e=n2
稀疏图:
O
(
n
2
)
O(n^2)
O(n2),稠密图:
O
(
n
4
)
O(n^4)
O(n4)
数据分析
稀疏图:
e
=
n
e = n
e=n, 时间复杂度:
O
(
n
2
)
O(n^2)
O(n2)
稠密图:
e
=
n
2
e = n^2
e=n2 ,时间复杂度:
O
(
n
4
)
O(n^4)
O(n4)
算法原理
(1) 利用DFS算法,获得图的一个生成森林。
(2) 因为桥边是连接两个连通分支的唯一边,所以桥边一定会出现在原图的生成森林之中。只需要遍历生成森林上的边,就可以找到所有桥边。
(3) 生成生成森林的方法是对全图进行DFS,如果前后两节点都没有被搜索到过,说明是一条生成树边并进行标记。
伪代码
MST_Create: Init visited[] For i = 1 to v: if (visited[i] == false) MST_DFS(i, -1);
MST_DFS (cur, pre): If (pre != -1) Edge (cur, pre) is a bridge visited[v] = true for every adjacent points (next) of (v): if visited[next] = false: MST_DFS (next, cur)
时间复杂度分析
大体上与基准法类似,区别在于只需要确认生成树上的边是否为桥。
对全图做DFS需要
O
(
n
+
e
)
O(n+e)
O(n+e),生成树至多有有
n
−
1
n-1
n−1条边,需要做一次DFS,总时间复杂度为
O
(
n
∗
(
n
+
e
)
)
=
O
(
n
2
+
n
e
)
O(n*(n+e))=O(n^2+ne)
O(n∗(n+e))=O(n2+ne)。(DFS优化算法)
初始化为求连通分支数,时间复杂度,外层循环需要遍历之多
n
−
1
n-1
n−1次删除情况
内层循环中每条边需要做2次Get_Parent操作,每次时间复杂度为
O
(
α
(
n
)
)
O(\alpha(n))
O(α(n)),循环总时间复杂度为
O
(
e
∗
(
n
∗
2
∗
α
(
n
)
)
)
=
O
(
n
e
)
O(e*(n*2*\alpha(n)))=O(ne)
O(e∗(n∗2∗α(n)))=O(ne)。(并查集算法)
对于稀疏图
e
=
n
e = n
e=n,对于稠密图
e
=
n
2
e = n^2
e=n2
稀疏图:
O
(
n
2
)
O(n^2)
O(n2),稠密图:
O
(
n
3
)
O(n^3)
O(n3)
数据分析
对于10000个节点,对比DFS/并查集加入生成树后的算法效率。
DFS+生成树算法
并查集+生成树算法
稀疏图:
e
=
n
e = n
e=n, 时间复杂度
O
(
n
2
)
O(n^2)
O(n2)
稠密图:
e
=
n
2
e = n^2
e=n2 时间复杂度O(n^3)$
LCA算法用于求两结点的共同祖先,如果两个结点拥有相同的祖先,则必能形成闭环。
得到生成树之后,我们需要将每条非生成树边依次加入生成树。对于这条边的两个节点,寻找它们的最小公共祖先节点,并且将寻找过程中遇到的边记录下来。下面我们拿(11,15)这条边进行解释。
显然10-11-14-15可以形成闭环,在这颗生成树中11、15的公共祖先为10,向上遍历的过程中11-10,10-14,14-15均被标记为环边。对于其他非生成树边同理。
LCA具体步骤:(以边11-15为例)
(1) 获得两节点的深度,11:4,15:5。首先需要对节点15沿着父亲节点遍历,直到其高度为3(与11的高度一样)。于是我们首先找到14-15边。
(2) 此时节点11和节点14的高度都为3,然后让它们同时寻找父亲节点10,记录下来10-11边和10-14边。
(3) 找到共同父亲节点后结束搜索。
最终找到三条环边。
部分数据结构解析:
(1) Depth[]:记录节点在生成树中的深度
(2) Parent[]:记录父节点
(3) Edges_visited[]:因为生成树中的每一条边都有且仅有一个父节点,所以可以以子节点序号作为边数组的索引(例如Edges[15]代表边14-15)
LCA(): // 以DFS为模板,构建生成树 For i = 1 to v: If visited[i] = false: LCA_DFS (i, -1, 0) // 检查非生成树边 Lca_num = 0 For every edge (l, r) not in MST: Find_Lca (l, r, lca_num) Return Edges_Sum - Edges_Not_In_MST - Lca_num
LCA_DFS(cur, pre, dep): If pre != -1 Edge (cur, pre) is in MST Visited[cur] = true Parent[cur] = pre Depth[cur] = dep for every adjacent points (next) of (v): if visited[next] = false: LCA_DFS (next, cur, dep + 1)
Find_Lca (l, r): left_dep = Depth[l], right_dep = Depth[r] // 右边深度大于左边深度 If left_dep < right_dep: delta = right_dep - left_dep // 向上搜索到同一高度 While delta >= 0: If Edges_visited[r] == false: Edges_visited[r] = True Lca_num++ r = Parent[r] // 同时向上搜索 while l != r: if Edge_visited[right] == false Edge_visited[right] = true Lca_num++ if Edge_visited[l] == false: Edge_visited[l] = true Lca_num++ l = parent[l] r = parent[r] Else if left_dep > right_dep: ... Else if left_dep == right_dep: ...
时间复杂度分析
DFS+并查集的时间复杂度为
O
(
n
+
e
)
O(n+e)
O(n+e)
依次加入找到的非生成树边进行LCA算法,其中LCA算法与并查集的搜索类似,因此查询时间复杂度为
算法复杂度为
O
(
e
∗
l
o
g
(
n
)
)
O(e*log(n))
O(e∗log(n))
对于稀疏图
e
=
n
e = n
e=n,对于稠密图
e
=
n
2
e = n^2
e=n2
稀疏图:
O
(
n
l
o
g
(
n
)
)
O(nlog(n))
O(nlog(n)),稠密图:
O
(
n
2
l
o
g
(
n
)
)
O(n^2log(n))
O(n2log(n))
数据分析
稀疏图:
e
=
n
e = n
e=n, 时间复杂度:
O
(
n
l
o
g
(
n
)
)
O(nlog(n))
O(nlog(n))
稠密图:
e
=
n
2
e = n^2
e=n2 ,时间复杂度:
O
(
n
2
l
o
g
(
n
)
)
O(n^2log(n))
O(n2log(n))
MediumDG.txt数据集:0s
LargeG数据集:5.489s
本次实验完成了图论中对于桥边的搜索。其中不同算法对于稀疏图、稠密图有着不同程度的优化,经过测试后可以发现图的稠密程度会有较大影响。