5 - 图论

ps markdown 中的公式语法,博客园中不支持啊

1,预备知识

图的类型:有向图,无向图,带权图

图的存储类型:邻接矩阵,邻接表

在C语言中的实现:

1, 通过结点数组中的元素来组成邻接表

2,通过 int 型的二维数组来实现。

在C++中的实现:

vector<int> node[10] 即向量的数组,一个向量表示一个点的所有相邻边(或出边)

2,并查集

数据结构: 集合 ; 对其的基本操作:并查集

实现 :在数组单元 i 中保存结点 i 的父亲结点编号,若该结点已经是根结点,则其双亲结点信息保存为-1。

利用一个树表示一个集合(一个集合内的点是相互连通的)

树用数组表示,对应位置的值表示其双亲(其实是单亲)元素。

合并两个集合:就是把一个树的根节点指向另一个树的结点。

路径压缩:在查找某个结点的根节点的时候,顺便把路径上所有的结点都改为指向根节点。

// 添加了路径压缩的 迭代式的 查找某个结点所在根节点 的函数
int findRoot(int x) {
    if (Tree[x] == -1) return x;
    else {
        int tmp = findRoot(Tree[x]);
        Tree[x] = tmp; //将当前结点的双亲结点设置为查找返回的根结点编号
        return tmp;
    }
}

// 主要调用方式
a = findRoot(a);
b = findRoot(b); 		//查找边的两个顶点所在集合信息
if (a != b) Tree[a] = b; //若两个顶点不在同一个集合则合并这两个集合

并查集的应用1

计算一个图中的连通分量个数。

并查集的应用2

查找一个图中的最大连通分量。

该应用要求在每个树的根节点中保存该树的大小。并且在初始化以及集合的合并的时候维护该值。

3,最小生成树

如果一个连通子图包含原图中的所有结点和部分边,且不含回路,那么就是生成树。

生成树中所有边的权值和最小的树就是最小生成树。

注意: 最小生成树的边数为顶点数 减一。

表示: $T = (V_T,E_T)$

一个定理:

图中结点的一个非空子集分为两个集合,那么连接两个集合的边中权值最小的边,一定属于最小生成树。

Prim算法

算法从 $ V_T = {u_0},E_T = \left {\right} $开始,重复执行下述操作:在所有$ u \in V_T,V \in T- V_T $ 的边 $(u,v)\in E$中找出一条代价最小的边$ (u_0,v_0)$并入集合 $E_T$ ,同时将 $ v_0 并入V_T$ ,直到$ V_T = V$ 为止。

时间复杂度为$ O(|V|^2)$,不依赖于$ |E|$,因此适合求解稠密的图的最小生成树。

Krustal算法

  1. 初始时候所有边是孤立的集合

  2. 将所有边按照权值递增排序

  3. 按照递增顺序遍历边,若遍历到的边的两个顶点属于不同的集合(即连接后不构成回路),那么确定该边是最小生成树的一条边,并将两个顶点分属的集合合并。

  4. 遍历完所有边后,若只剩下一个集合(即有了N-1条边),则成功。否则原图不连通。

算法实现:

定义一个边结构数组,包括边的两个顶点与权值。还有一个顶点的数组用于表示树结构。

即:使用堆来存放边的集合,则每次选择最小权值的边只需要$O(log|E|)$的时间。而生成树中边可以看做一个等价类,使用并查集的方法来添加新的边。

特点:适合边稀疏的图。

4 最短路径

即寻找图中两个结点之间的最短路径。FLOYD算法和Dijkstra算法都是基于贪心算法,也就是说找出一个代价的衡量值,然后使得每一步的代价最小。

一个定理:

​ 两点之间的最短路径也包括了路径上其他顶点的最短路径。

FLOYD算法

目标:求解各个顶点之间的最短路径问题。

/* 算法的关键的代码如下:
利用允许经过前K-1个顶点的i到j的最短路径,
计算允许经过前K个顶点的i到j的最短路径  */

for (int k = 1;k <= n;k ++) {
	for (int i = 1;i <= n;i ++) {
		for (int j = 1;j <= n;j ++) {
			if (ans[i][k] == 无穷 || ans[k][j] == 无穷) continue;
			if (ans[i][j] == 无穷 || ans[i][k] + ans[k][j] < ans[i][j])
			ans[i][j] = ans[i][k] + ans[k][j];
		}
	}
}

性能:Floyd 算法的时间复杂度为 $O (N^3)$,空间复杂度为$ O(N^2)$,其中 N 均为图中结点的个数。 可以看出,FLOYD的代码中刚好有三个循环,所以时间复杂度是 $O (N^3)$。

通常N小于300时,性能可以接受。

Dijkstra 算法

目的:只能求得某特定结点到其它所有结点的最短路径长度,即单源最短路路径问题。(不能有负权值的边)

算法思想:假设集合 K 中已经 保存了最短路径长度最短的前 m 个结点,它们是 P1, P2……Pm,并已经得出它 们的最短路径长度。那么第 m+1 近的结点与结点 1 的最短路径上的中间结点一 定全部属于集合 K。

那 么第 m+1 近结点的最短路径必是由以下两部分组成:从结点 1 出发经由已经确 定的最短路径到达集合 K 中的某结点 P2,再由 P2 经过一条边到达该结点。

所有对横跨K内外的边进行循环,直到找出第 m+1 近的结点 。

松弛: Dis 数组是在更新过程中不断递减的,我们把对Dis数组中的每个元素进行缩小的操作叫做“松弛”。

步骤 :

  1. 初始化,集合 K 中加入结点 1,结点 1 到结点 1 最短距离为 0,到其它结 点为无穷(或不确定)。

  2. 遍历与集合 K 中结点直接相邻的边(U, V, C),其中 U 属于集合 K, V 不属于集合 K,计算由结点 1 出发按照已经得到的最短路到达 U,再由 U 经过 该边到达 V 时的路径长度。比较所有与集合 K 中结点直接相邻的非集合 K 结点 该路径长度,其中路径长度最小的结点被确定为下一个最短路径确定的结点,其 最短路径长度即为这个路径长度,最后将该结点加入集合 K。

  3. 若集合 K 中已经包含了所有的点,算法结束;否则重复步骤 2。

实现:很好的支持邻接链表,也可以使用邻接矩阵。

注意: 有些时候还需要一个int数组 path[n] 来记录从源点到某个结点的最短路径上该结点的前驱结点

当要统计等价路径的时候,还需要一个int数组 com_in[n] 来记录在等价路径网中该结点的入度是多少。便于统计最后共有几条等价最短路径。(统计过程不是一个最短路径上所有结点的 com_in 值的和)

性能:它的时间复杂度为$ O(N^2)​$(若在 查找最小值处利用堆进行优化,则时间复杂度可以降到 $O(N*logN)​$, N 为结点的 个数。空间复杂度为 $O(N)​$(不包括保存图所需的空间)。

考虑从一个点到另外一个点的最短路径的难度和考虑一个点到其他所有点的最短路径的难度是相同的。(即使只想找一个点到另一个点的最短路径,也必须画出以起点为最低点的全局所有点的“等高线图”)

关于它和BFS的关系 当所有的边的权值都是1的时候,Dijkstra算法就是BFS算法,所以可以说,BFS算法是它在所有边的权值都是1 的时候的特例。

考虑等价最短路径的Dijkstra算法(未完成)

一点到另一点的所有等价最短路径构成了一个有向无环图。

原理: 后加入集合的点的最短路径一定比后加入的点的最短路径短,这个前提并不能使得我们在选择侯选边的时候不再考虑已经加入两个顶点都在集合中,但是该边没有没有考虑的边。以为这个边的加入,虽然不可能使得路径更短,但是可能会有新的等价最短路径出现。

5 拓扑排序

设有一个有向无环图(DAG 图),对其进行拓扑排序即求其中结点的一个拓 扑序列,对于所有的有向边(U, V)(由 U 指向 V),在该序列中结点 U 都排列 在结点 V 之前。

图的存储: 邻接链表

排序方法

我们选择一个入度为 0 的结点,作为序列的第一个结点。当该结点 被选为序列的第一个顶点后,我们将该点从图中删去,同时删去以该结点为弧尾 的所有边,得到一个新图。

然后重复以上步骤。

应用:当需要判断某个图是否属于有向无环图时,我们都需要立刻联想 到拓扑排序。

实现:为了保存在拓扑排序中不断出现的和之前已经出现的入度为 0 的结点,供作为新的起点。我们 使用一个队列。(其实用栈,数组啥的都行)

使用到的主要数据: 存储图的邻接链表,统计入度的list,缓存0入度节点的list

复杂度:复杂度为$ O(N+E)$,其中 N 为结点的个数, E 为边的个数。

posted @ 2019-03-15 11:25  天叨哥  阅读(190)  评论(0编辑  收藏  举报