数据结构与算法系列文章目录
【数据结构与算法】data structures & algorithms 第一章:复杂度分析
【数据结构与算法】data structures & algorithms 第二章:基本概念
【数据结构与算法】data structures & algorithms 第三章:线性数据结构
【数据结构与算法】data structures & algorithms 第四章:树的数据结构
【数据结构与算法】data structures & algorithms 第五章:图的数据结构
【数据结构与算法】data structures & algorithms 第六章:各类常见的排序算法
【数据结构与算法】data structures & algorithms 第七章:散列表算法的初步运用
【数据结构与算法】data structures & algorithms 第八章:红黑树的理解与使用
一、什么是图
-
表示多对多的关系
-
包含
- 一组顶点:通常用 V ( V e r t e x ) V(Vertex) V(Vertex)表示顶点集合
- 一组边:通常用
E
(
E
d
g
e
)
E(Edge)
E(Edge)表示边的集合
- 边是顶点对:无向边 ( v , w ) ∈ E (v, w)\in E (v,w)∈E,其中 v , w ∈ V v, w\in V v,w∈V
- 有向边 < v , w > <v, w> <v,w>表示从 v v v指向 w w w的边(单行线)
- 不考虑重边和自回路
-
抽象数据类型定义
- 类型名称:图(Graph)
- 数据对象集: G ( V , E ) G(V, E) G(V,E)由一个非空的有限顶点集合 V V V和一个有限边集合 E E E组成
- 操作集:对于任意图
G
∈
G
r
a
p
h
G\in Graph
G∈Graph,以及
v
∈
V
,
e
∈
E
v\in V, e\in E
v∈V,e∈E
- Graph create():建立并返回空图
- Graph insertVertex(Graph G, Vertex v):将v插入G
- Graph insertEdge(Graph G, Edge e):将e插入G
- void DFS(Graph G, Vertex v):从顶点v出发深度优先遍历图G
- void BFS(Graph G, Vertex v):从顶点v出发宽度优先遍历图G
- void shortestPath(Graph G, Vertex v, int Dist[]):计算图G中顶点v到任意其它顶点的最短距离
- void MST(Graph G):计算图G的最小生成树
-
常见术语
- 无向图:两结点之间没有方向,只有一条路径
- 有向图:两结点之间存在有方向限制的路径
- 网络:上述图的边是没有权重的,如果为每条边附上权重,则称为网络
二、怎么在程序中表示一个图
1、邻接矩阵
- G [ N ] [ N ] G[N][N] G[N][N]——N个顶点从0到N-1编号
- G [ i ] [ j ] = { 1 若 < v i , v j > 是 G 中 的 边 0 否 则 G[i][j] = \begin{cases} 1 \hspace{1cm} 若<v_i, v_j>是G中的边 \\ 0 \hspace{1cm}否则\end{cases} G[i][j]={1若<vi,vj>是G中的边0否则
无向图的邻接矩阵是关于对角线对称的,因此我们应该想办法省下一半的空间来存储无向图
-
无向图的存储,可以用一个长度为 N ( N + 1 ) / 2 N(N+1)/2 N(N+1)/2的一维数组A存储 { G 00 , G 10 , G 11 , ⋯ , G n − 1 , n − 1 } \{ G_{00}, G_{10}, G_{11}, \cdots, G_{n-1,n-1} \} {G00,G10,G11,⋯,Gn−1,n−1},则 G i j G_{ij} Gij在A中对应的下标为: ( i ∗ ( i + 1 ) / 2 + j ) (i*(i+1)/2+j) (i∗(i+1)/2+j)
-
有向图的度,计算某结点的出度,则计算对应结点的行中的非零个数;计算某结点的入度,则计算对应结点的列中的非零个数;
-
对于网络,只要把 G [ i ] [ j ] G[i][j] G[i][j]的值定义为边 < v i , v j > <v_i, v_j> <vi,vj>的权重即可
-
邻接矩阵的优点:
- 直观、简单、好理解
- 方便检查任意一对顶点间是否存在边
- 方便检查任意一对的所有“邻接点”(有边直接相连的顶点)
- 方便计算任一顶点的”度“(从该点发出的边数为”出度“,指向该点的边数为”入度“)
- 无向图:对应行(或列)非零元素的个数
- 有向图:对应行非零元素的个数是”出度“;对应列非零元素个数是”入度“
-
邻接矩阵的缺点:
- 浪费空间——存稀释图(点很多二边很少)有大量无效元素
- 对稠密图(特别是完全图)还是很合算的
- 浪费时间——统计稀释图中一共有多少条边
- 浪费空间——存稀释图(点很多二边很少)有大量无效元素
2、邻接表
- G [ N ] G[N] G[N]为指针数组,对应矩阵每行一个链表,只存非零元素;一定要够稀疏才合算
- 对于网络,结构中要增加权重的域
- 邻接表的优缺点
- 方便找任一顶点的所有”邻接点“
- 节约稀疏图的空间
- 需要N个头指针 + 2E个结点(每个结点至少2个域)
- 方便计算任一顶点的”度“
- 无向图:可以
- 有向图:只能计算”出度“;要计算”入度“,需要构造”逆邻接表“(存指向自己的i边)
- 不方便检查任意一对顶点之间是否存在边
三、图的遍历
1、深度优先搜索
- 深度优先搜索(Depth First Search, DFS)
//类似树的 先序 遍历
void DFS(Vertex V)
{
visited[V] = true;
for (V 的每个邻接点 W)
{
if (!visited[W])
{
DFS(W);
}
}
}
-
DFS先搜索当前顶点是否可以往下遍历,
- 若可以则往下遍历,
- 若不可以则往回退一个结点,并搜索当前结点是否有其它没有遍历过的结点,
- 若有则往下遍历,
- 若没有则往回退一个结点,
- 如此循环,直到退到初始结点为止
-
若有N个结点,E条边,时间复杂度是
- 用邻接表存储图,为 O ( N + E ) O(N+E) O(N+E)
- 用邻接矩阵存储图,为 O ( N 2 ) O(N^2) O(N2)
2、广度优先搜索
- 广度优先搜索(Breadth First Search, BFS)
void BFS(Vertex V)
{
visited[V] = true;
//将当前结点进行入队操作
enQueue(V, Q);
while (!isEmpty(Q))
{
//出队操作
V = deQueue(Q);
//检索当前结点的所有子结点,
//将未入队的子结点进行入队操作
for (V 的每个邻接点 W)
{
if (!visited[W])
{
visited[W] = true;
enQueue(W, Q);
}
}
}
}
-
BFS会遍历当前结点的所有子结点,将没有在队列中的子结点进行入队操作,
- 完成操作后,队列进行出队操作则得到一个结点,
- 此时重复上述所有操作,直到当前结点没有可以入队的子结点为止
-
若有N个结点、E条边,时间复杂度是
- 用邻接表存储图,为 O ( N + E ) O(N+E) O(N+E)
- 用邻接矩阵存储图,为 O ( N 2 ) O(N^2) O(N2)
3、图的连通术语与处理
-
连通:如果从 V 到 W 存在一条(无向)路径,则称为 V 和 W 是连通的
-
路径:V 到 W 的路径是一系列顶点 { V , V 1 , V 2 , ⋯ , V n , W } \{V, V_1, V_2, \cdots, V_n, W\} {V,V1,V2,⋯,Vn,W}的集合,其中任一对相邻的顶点之间都有图的边。
路径的长度是路径中的边数(如果带有权重,则是所有边的权重和)。
如果 V 到 W 之间的所有顶点都不同,则称为简单路径 -
回路:起点等于终点的路径
-
连通图:图中任意两顶点均连通
-
连通分量:无向图的极大连通子图
- 极大顶点数:再加1个顶点就不连通
- 极大边数:包含子图中所有顶点相连的所有边
-
强连通:有向图中顶点 V 和 W 之间存在双向路径,则称 V 和 W 是强连通
-
强连通图:有向图中任意两顶点均强连通
-
强连通分量:有向图的极大强连通子图
-
存在单独图中有不连通的结点的处理方法
void listCompnents(Graph G)
{
//防止漏掉某个不连通的结点
//整个函数将会遍历所有状态不是true的结点
for (each V in G)
{
if (!visited[v])
{
DFS(V); //or BFS(V)
}
}
}
void DFS(Vertex V)
{
visited[V] = true;
for (each W in V)
{
if (!visited[W])
{
DFS(W);
}
}
}
四、应用案例:拯救007
- 007被放在孤岛位置,要通过跳跃每个黑点,抵达岸上;
- 故依次建立一个笛卡尔坐标系,中心点即孤岛位置
-
整个问题本质是图的遍历问题
-
下面是一般的图的遍历程序
void listComponents(Graph G)
{
for (each V in G)
{
if (!visited[V])
{
DFS(V);
}
}
}
- 在这个问题上,需要做出一定的修改
void save007(Graph G)
{
//这里设置循环体,为了避免忽略有不相互连通的子图
for (each V in G)
{
//要考虑子结点是否被访问过
//且要判断007能否跳过去
if (!visited[V] && firstJump(V))
{
//通过DFS算法得到最终答案
answer = DFS(V);
if (answer == "YES")
{
break;
}
}
}
if (answer == "YES")
{
cout << "YES" << endl;
}
else
{
cout << "NO" << endl;
}
}
- 下面是传统的DFS算法
void DFS (Vertex V)
{
visited[V] = true;
for (each W in V)
{
if (!visited[W])
{
DFS(W);
}
}
}
- 在这里,我们需要做出一定的修改
int DFS (Vertex V)
{
//表示当前结点已被访问
visited[V] = true;
//判断是否已经上岸
if (isSafe(V))
{
answer = "YES";
}
else
{
//往下遍历V的子结点
for (each W in V)
{
//判断该子节点是否已被访问
//且判断是否能跳过去
if (!visited[W] && jump(V, W))
{
//递归调用DFS函数
answer = DFS(W);
if (answer == "YES")
{
break;
}
}
}
}
return answer;
}
五、应用案例:六度空间
- 你和任何一个陌生人之间所间隔的人不会超过六个人
- 给定社交网络图,请对每个结点计算符合“六度空间”理论的结点占结点总数的百分比
-
算法思路
- 对每个结点,进行广度优先搜索
- 搜索过程中累计访问的结点数
- 需要记录层数,仅计算六层以内的结点数
-
主函数
void SDS (Graph G)
{
//遍历起点的所有子结点,避免忽略不连通的子图
for (each V in G)
{
count = BFS(V);
cout << (count / N) << endl;
}
}
- 对传统的BFS算法做出修改
int BFS (Vertex V)
{
visited[V] = true;
count = 1; //六层内的结点数
level = 0; //层数
last = V; //本层的最后一个结点
enQueue(V, Q);
while (!isEmpty(Q))
{
V = deQueue(Q);
for (each W in V)
{
if (!visited[W])
{
visited[W] = true;
enQueue(W, Q);
count++;
tail = W; //下一层的最后一个结点
}
}
//判断结点是否为本层的最后一个结点
if (V == last)
{
level++; //更新层数
last = tail; //往下层移动
}
//仅计算六层内的结点数
if (level == 6)
{
break;
}
}
//返回输入结点V的结果
return count;
}
六、如何建立图
1、用邻接矩阵表示图
-
邻接矩阵表示
G [ i ] [ j ] = { 1 若 < v i , v j > 是 G 中 的 边 0 否 则 G[i][j] = \begin{cases} 1 & 若<v_i,v_j>是G中的边 \\ 0 &否则 \end{cases} G[i][j]={10若<vi,vj>是G中的边否则 -
以邻接矩阵表示图
struct GNode
{
int Nv; //顶点数
int Ne; //边数
weightType G[maxVertexNum][maxVertexNum];
dataType data[maxVertexNum]; //存顶点的数据
};
typedef GNode* MGraph //以邻接矩阵存储的图类型
- MGraph初始化
typedef int Vertex; //用顶点下标表示顶点,区别于整型,方便确认
MGraph createGraph (int vertexNum)
{
MGraph Graph = new GNode;
Graph->Nv = vertexNum;
Graph->Ne = 0;
for (Vertex V = 0; V < Graph->Nv; V++)
{
for (Vertex W = 0; W < Graph->Nv; W++)
{
Graph->G[V][W] = 0; //或者是无穷大
}
}
return Graph;
}
- 向MGraph中插入边
struct ENode
{
Vertex V1, V1; //有向边<V1, V2>
weightType Weight; //权重
};
typedef ENode* Edge;
void insertEdge (MGraph Graph, Edge E)
{
//插入边<V1, V2>
Graph->G[E->V1][E->V2] = E->Weight;
//若是无向图,还要插入边<V2, V1>
Graph->G[E->V2][E->V1] = E->Weight;
}
- 完整地建立一个MGraph
MGraph bulidMGraph ()
{
int Nv, Ne;
cout << "enter vertex number: " << endl;
cin >> Nv;
MGraph Graph = createGraph (Nv);
cout << "enter edge number: " << endl;
cin >> Ne;
Graph->Ne = Ne;
if (Graph->Ne != 0)
{
Edge E = new ENode;
for (int i = 0; i < Graph->Ne; i++)
{
cout << "perpectly enter vertex1, vertex2 and weight: " << endl;
cin >> E->V1;
cin >> E->V2;
cin >> E->Weight;
insertEdge(Graph, E);
}
}
//如果顶点有数据的话,读入数据
for (Vertex V = 0; V < Graph->Nv; V++)
{
cin >> Graph->data[V];
}
return Graph;
}
2、用邻接表表示图
- 邻接表: G [ N ] G[N] G[N]为指针数组,对应矩阵每行一个链表,只存非零元素
- 用邻接表表示图
struct GNode
{
int Nv; //顶点数
int Ne; //边数
adjList G; //邻接表
};
typedef GNode* LGraph;
//定义链表中的结点
struct AdjVNode
{
Vertex AdjV; //邻接点下标
weightType Weight; //边权重
AdjVNode* Next;
};
typedef AdjVNode* PtrToAdjVNode;
struct VNode
{
PtrToDjVNode firstEdge;
dataType data; //存顶点的数据
};
typedef VNode adjList[maxVertexNum];
- LGraph初始化
//初始化一个有vertexNum个顶点但没有边的图
typedef int Vertex;
LGraph createGraph (int vertexNum)
{
LGraph Graph = new GNode;
Graph->Nv = vertexNum;
Graph->Ne = 0;
for (Vertex V = 0; V < Graph->Nv; V++)
{
Graph->G[V].firstEdge = NULL;
}
return Graph;
}
- 向LGraph中插入边
struct ENode
{
Vertex V1, V1; //有向边<V1, V2>
weightType Weight; //权重
};
typedef ENode* Edge;
void insertEdge (LGraph Graph, Edge E)
{
//插入<V1, V2>
//为V2建立新的邻接点
PtrToAdjVNode newNode = new AdjVNode;
newNode->AdjV = E->V2;
newNode->Weight = E->Weight;
//将V2插入V1的表头
newNode->Next = Graph->G[E->V1].firstEdge;
Graph->G[E->V1].firstEdge = newNode;
//若是无向图,还要插入边<V2, V1>
//为V1建立新的邻接点
PtrToAdjVNode newNode = new AdjVNode;
newNode->AdjV = E->V1;
newNode->Weight = E->Weight;
//将V1插入V2的表头
newNode->Next = Graph->G[E->V2].firstEdge;
Graph->G[E->V2].firstEdge = newNode;
}
- 完整地建立LGraph
LGraph bulidLGraph ()
{
int Nv, Ne;
cout << "enter vertex number: " << endl;
cin >> Nv;
LGraph Graph = createGraph (Nv);
cout << "enter edge number: " << endl;
cin >> Ne;
Graph->Ne = Ne;
if (Graph->Ne != 0)
{
Edge E = new ENode;
for (int i = 0; i < Graph->Ne; i++)
{
cout << "perspectly enter vertex1, vertex2 and weight: " << endl;
cin >> E->V1;
cin >> E->V2;
cin >> E->Weight;
insertEdge (Graph, E);
}
}
//如果顶点有数据的话,读入数据
for (Vertex V = 0; V < Graph->Nv; V++)
{
cin >> Graph->G[V].data;
}
return Graph;
}
七、习题讲解
1、树的二次遍历(tree traversals again)
-
通过非递归中序遍历,得到先序、中序和后序的数组
-
Push的顺序为先序遍历
-
Pop的顺序为中序遍历
- 后序数组的获取
void solve (int PreL, int inL, int postL, int n)
{
if (n == 0)
{
return;
}
if (n == 1)
{
post[postL] = pre[PreL];
return;
}
root = pre[PreL];
post[postL + n - 1] = root;
for (int i = 0; i < n; i++)
{
if (in[inL + i] == root)
{
break;
}
}
L = i;
R = n - L -1;
solve (PreL + 1, inL, postL, L);
solve (PreL + L + 1, inL + L + 1, postL + L, R);
}
2、完全二叉搜索树(Complete binary search tree)
- 二叉搜索树 + 完全二叉树
-
树的表示法,用数组更好
- 建立完全二叉树,不会浪费空间
- 层序遍历,相当于直接顺序输出
-
核心算法
A L e f t ALeft ALeft为排序后的输入序列 A A A左边第一个结点的下角标
A R i g h t ARight ARight为右边最后一个结点的下角标
T R o o t TRoot TRoot为结果树对应根结点的下角标
void solve (int ALeft, int ARight, int TRoot)
{
//初始调用为 solve (0, N-1, 0)
n = ARight - ALeft + 1;
if (n == 0)
{
return;
}
L = getLeftLength(n); //计算n个结点的树其左子树的结点数
T[TRoot] = A[ALeft + L];
leftTRoot = TRoot * 2 + 1;
rightTRoot = leftTRoot + 1;
solve (ALeft, ALeft + L - 1, leftTRoot);
solve (ALeft + L + 1, ARight, rightTRoot);
}
- 计算左子树的结点数
- 基本公式
Ⅰ、在perfect binary tree中,每层的结点数为 n h = 2 h − 1 n_h = 2^{h-1} nh=2h−1;
Ⅱ、在perfect binary tree中,全部结点数为 2 H − 1 2^H-1 2H−1 - 计算步骤
Ⅰ、通过总结点数 N N N,得到完全二叉树中满二叉树的层数,即 H = ⌊ log 2 N + 1 ⌋ H=\lfloor \log_2^{N+1} \rfloor H=⌊log2N+1⌋
Ⅱ、有了 H H H,通过 2 H − 1 + X = N 2^H-1+X=N 2H−1+X=N得到最后一层的结点数 X X X
Ⅲ、 X X X需要满足条件,即 X = m i n { X , 2 H − 1 } X=min\{X, 2^{H-1}\} X=min{X,2H−1}
Ⅳ、最后得到左子树的总结点数为, L = 2 H − 1 − 1 + X L=2^{H-1}-1+X L=2H−1−1+X
- 基本公式
3、最短路径问题
-
最短路径问题的抽象
在网络中,求两个不同顶点之间的所有路径中,边的权值之和最小的那一条路径- 这条路径就是两点之间的最短路径(Shortest Path)
- 第一个顶点为源点(Source)
- 最后一个顶点为终点(Destination)
-
问题分类
- 单源最短路径问题:
从某固定源点出发,求其到所有其它顶点的最短路径- (有向)无权图
- (有向)有权图
- 多源最短路径问题:
求任意两顶点之间的最短路径
- 单源最短路径问题:
3.1、无权图的单源最短路径算法
- 按照递增(非递减)的顺序找出到各个顶点的最短路
- 这里一般采用BFS-广度优先搜索
d i s t [ W ] = S 到 W 的 最 短 距 离 dist[W] = S到W的最短距离 dist[W]=S到W的最短距离
d i s t [ S ] = 0 dist[S] = 0 dist[S]=0
p a t h [ W ] = S 到 W 的 路 上 经 过 的 某 顶 点 path[W] = S到W的路上经过的某顶点 path[W]=S到W的路上经过的某顶点
初始化dist数组的值为-1,path数组的值为0
void unWeighted (Vertex S)
{
enQueue(S, Q);
while (!isEmpty(Q))
{
V = deQUeue(Q);
for (V 的每个邻接点W)
{
if (dist[W] == -1)
{
dist[W] = dist[V] + 1;
path[W] = V;
enQueue(W, Q);
}
}
}
}
时间复杂度: T = O ( ∣ V ∣ + ∣ E ∣ ) T = O(|V| + |E|) T=O(∣V∣+∣E∣)
举例,假设图有5个结点,寻找最短路径
下标 | 1 | 2 | 3 | 4 | 5 |
---|---|---|---|---|---|
dist | -1 | -1 | -1 | -1 | -1 |
path | 0 | 0 | 0 | 0 | 0 |
3.2、有权图的单源最短路径算法
- 按照递增的顺序找出到各个顶点的最短路径
- 这里一般采用Dijkstra算法
这里要指出Dijkstra算法中,不能出现负值圈,不然会出现循环路线
- Digkstra算法 狄克斯特拉算法
- 令 S = { 源 点 s + 已 经 确 定 了 最 短 路 径 的 顶 点 v i } S = \{源点s + 已经确定了最短路径的顶点v_i\} S={源点s+已经确定了最短路径的顶点vi}
- 对任一未收录的顶点 V V V,定义 d i s t [ v ] dist[v] dist[v]为 s s s到 v v v的最短路径长度,但该路径仅经过S中的顶点。即路径 { s → ( v i ∈ S ) → v } \{s\rightarrow (v_i\in S) \rightarrow v\} {s→(vi∈S)→v}的最小长度
- 若路径是按照**递增(非递减)**的顺序生成的,则
- 真正的最短必须只经过 S S S中的顶点
- 每次从未收录的顶点中选一个 d i s t dist dist最小的收录(贪心)
- 增加一个
v
v
v进去
S
S
S,可能影响另外一个
w
w
w的
d
i
s
t
dist
dist值
- d i s t [ w ] = m i n { d i s t [ w ] , d i s t [ v ] + < v , w > 的 权 值 } dist[w] = min\{dist[w], dist[v] + <v, w>的权值\} dist[w]=min{dist[w],dist[v]+<v,w>的权值}
这里 d i s t dist dist做初始化时,不能用-1,要用∞
d i s t [ v ] dist[v] dist[v]指的是顶点 v v v到源点的最短距离
void Dijkstra (Vertex s)
{
while (1)
{
v = 未收录顶点中dist最小者;
if (这样的v不存在)
{
break;
}
collected[v] = true;
for (v的每个邻接点w)
{
if (collected[w] == false)
{
if (dist[v] + E < dist[w])
{
//E是v到w的边权值
dist[w] = dist[v] + E;
path[w] = v;
}
}
}
}
}
- 时间复杂度
- 方法1:直接扫描所有未收录顶点,这里花费的时间复杂度为
O
(
∣
V
∣
)
O(|V|)
O(∣V∣)
时间复杂度为 T = O ( ∣ V ∣ 2 + ∣ E ∣ ) T = O(|V|^2 + |E|) T=O(∣V∣2+∣E∣)
对于稠密图(边数多的图)效果好 - 方法2:将
d
i
s
t
dist
dist存在最小堆中,这里花费为
O
(
log
∣
V
∣
)
O(\log|V|)
O(log∣V∣)
更新 d i s t [ w ] dist[w] dist[w]的值,花费为 O ( log ∣ V ∣ ) O(\log|V|) O(log∣V∣)
时间复杂度为 T = O ( ∣ V ∣ log ∣ V ∣ + ∣ E ∣ log ∣ V ∣ ) = O ( ∣ E ∣ log ∣ V ∣ ) T = O(|V|\log|V| + |E|\log|V|) = O(|E|\log|V|) T=O(∣V∣log∣V∣+∣E∣log∣V∣)=O(∣E∣log∣V∣)
对于稀疏图(边数少的图)效果好
- 方法1:直接扫描所有未收录顶点,这里花费的时间复杂度为
O
(
∣
V
∣
)
O(|V|)
O(∣V∣)
3.3、多源最短路径算法
-
方法1:直接将单源最短路径算法调用 ∣ V ∣ |V| ∣V∣遍
T = O ( ∣ V ∣ 3 + ∣ E ∣ × ∣ V ∣ ) T = O(|V|^3 + |E|\times |V|) T=O(∣V∣3+∣E∣×∣V∣)
对于稀疏图效果好 -
方法2:Floyd算法
T = O ( ∣ V ∣ 3 ) T = O(|V|^3) T=O(∣V∣3) -
Floyd算法
- D k [ i ] [ j ] = D^k[i][j] = Dk[i][j]= 路径 { i → { l ≤ k } → j } \{ i \rightarrow \{ l \leq k \} \rightarrow j \} {i→{l≤k}→j}的最小长度
- D 0 , D 1 , ⋯ , D ∣ V ∣ − 1 [ i ] [ j ] D^0, D^1, \cdots , D^{|V| - 1}[i][j] D0,D1,⋯,D∣V∣−1[i][j]即给出了 i i i 到 j j j 的真正最短距离
- 但
D
k
−
1
D^{k - 1}
Dk−1已经完成,递推到
D
k
D^k
Dk时:
- 或者 $k \notin $ 最短路径 { i → { l ≤ k } → j } \{ i \rightarrow \{ l \leq k \} \rightarrow j \} {i→{l≤k}→j},则 D k = D k − 1 D^k = D^{k - 1} Dk=Dk−1
- 或者 $k \in $ 最短路径 { i → { l ≤ k } → j } \{ i \rightarrow \{ l \leq k \} \rightarrow j \} {i→{l≤k}→j},则该路径必定由两段最短路径组成: D k [ i ] [ j ] = D k − 1 [ i ] [ k ] + D k − 1 [ k ] [ j ] D^k[i][j] = D^{k - 1}[i][k] + D^{k - 1}[k][j] Dk[i][j]=Dk−1[i][k]+Dk−1[k][j]
void floyd ()
{
for (int i = 0; i < N; ++i)
{
for (int j = 0; j < N; ++j)
{
D[i][j] = G[i][j];
path[i][j] = -1;
}
}
for (int k = 0; k < N; ++k)
{
for (int i = 0; i < N; ++i)
{
for (int j = 0; j < N; ++j)
{
if (D[i][k] + D[k][j] < D[i][j])
{
D[i][j] = D[i][k] + D[k][j];
path[i][j] = k;
}
}
}
}
}
时间复杂度为 T = O ( ∣ V ∣ 3 ) T = O(|V|^3) T=O(∣V∣3)
4、解决最短路径问题的相关算法
4.1、Dijkstra算法(迪杰斯特拉算法)
-
特点
该算法使用了BFS解决赋权有向图或无向图的单源最短路径问题,并最终得到一个最短路径树;
该算法常用于路由算法或作为其它图算法的一个子模块 -
思路
采用的是贪心策划:- 初始化中,给定一个起点 A A A,以及含有所有点到起点的邻接距离的数组;若不是邻接点则初始化为无穷;
- 在数组中找到最短距离的顶点 B B B,可以确定这个距离就是该点到起点的最短距离;(注意:Dijkstra算法不能出现负权值边)
- 查看该点的邻接点 B B B(没有访问过的顶点),判断若以该点为中间点,起点到达其它点的距离是否会更短;若更短,则更新为更短的距离;
- 完成上述操作后,将点 B B B标记为已访问
- 重复上述操作,直到所有顶点都已被访问
4.2、Floyd算法(弗罗伊德算法)
-
特点
该算法是解决任意两点之间的最短路径的,可以正确处理有向图或无向图或**负权(但不可存在负权回路)**的最短路径问题,同时也被用于计算有向图的传递闭包 -
思路
该算法计算图 G = ( V , E ) G = (V, E) G=(V,E)中各个顶点的最短路径时,需要引入两个矩阵
矩阵 D D D的元素 a [ i ] [ j ] a[i][j] a[i][j]表示顶点 i i i到顶点 j j j的距离
矩阵 P P P的元素 b [ i ] [ j ] b[i][j] b[i][j]表示顶点 i i i经过经过 b [ i ] [ j ] b[i][j] b[i][j]记录的顶点,然后到达顶点 j j j
假设图 G G G中顶点个数为 N N N,则需要对两个矩阵进行 N N N次更新- 初始化中,矩阵 D D D记录两个顶点之间邻接距离,如果它们不是邻接点,则用∞表示;矩阵 P P P则在 b [ i ] [ j ] b[i][j] b[i][j]存储顶点 j j j
- 对顶点 k k k进行计算,比较 a [ i ] [ k ] + a [ k ] [ j ] a[i][k] + a[k][j] a[i][k]+a[k][j]与 a [ i ] [ j ] a[i][j] a[i][j]的大小,如果前者较小,则更新 a [ i ] [ j ] a[i][j] a[i][j]的值;其次在矩阵 P P P的对应元素 b [ i ] [ j ] b[i][j] b[i][j]进行更新,存储顶点 k k k
- 如此操作,直到所有的顶点均被访问
4.3、SPFA算法
-
特点
该算法与Dijkstra算法很像,也是求解单源最短路径问题;
原理是对图进行 V − 1 V-1 V−1次操作,得到所有可能的最短路径;
比Dijsktra算法好的地方是边的权值可以是负数,实现简单;缺点是时间复杂度高,高达 O ( V E ) O(VE) O(VE) -
思路
区别与Dijkstra算法的贪心策略,这里使用的是优先队列
用数组dist记录每个结点的最短路径估计值,采取动态逼近法:设立一个先进先出的队列用来保存待优化的结点,每次优化则弹出首结点;- 初始化中,在dist中除源点为0,其余顶点的值为∞;并将源点压入队列;
- 弹出优先队列中的首结点,对其邻接顶点进行松弛操作;
- 若有邻接点被松弛了,且不再队列中,则将其假如队列;
- 重复操作,若有邻接点被松弛,但已经在队列中,则不作操作;
- 重复操作,若没有邻接点被松弛,且队列为空,则停止循环;
- 此时数组dist已经存储了最短路径
5、最小生成树(Minimum Spanning Tree)
5.1、简介
-
定义
满足下面三点- 树
- 无回路
- ∣ V ∣ |V| ∣V∣个顶点一定有 ∣ V ∣ − 1 |V|-1 ∣V∣−1条边
- 生成树
- 包含图中所有顶点
- ∣ V ∣ − 1 |V|-1 ∣V∣−1条边都在图里
- 边的权值和最小
- 树
-
注意
- 最小生成树存在 表示 图是连通的
- 向生成树中任加一条边都一定构成回路
-
贪心算法
- 什么是”贪“:每一步都要最好的
- 什么是”好“:权值最小的边
- 约束:
- 只能用图里有的边
- 只能正好用掉 ∣ V ∣ − 1 |V|-1 ∣V∣−1 条边
- 不能有回路
5.2、Prim算法(用于稠密图合算)
- 步骤
- 随机选择一个顶点作为初始点;
- 寻找初始点邻接权值最小的顶点,并组成生成树;
- 寻找生成树中邻接权值最小的顶点,判断是否构成回路,并更新生成树;
- 重复上一步,直到生成树的边数达到 ∣ V ∣ − 1 |V|-1 ∣V∣−1 或 图中没有边可访问;
void Prim()
{
MST = {s}; //生成树的顶点集合
while (1)
{
v = 未收录顶点中dist最小者;
if (这样的v不存在) break;
将v收录进MST: dist[v] = 0;
for (v的每个邻接点w)
if (dist[w] != 0)
{
if (E < dist[w])
{
dist[w] = E;
parent[w] = v;
}
}
}
if (MST中收录的顶点不到|V|个)
Error ("生成树不存在");
}
- 初始化中, d i s t [ v ] = E ( s , v ) dist[v] \ \ = \ E_{(s,v)} dist[v] = E(s,v) 或正无穷, p a r e n t [ s ] = − 1 parent[s] \ \ = \ -1 parent[s] = −1;
- 时间复杂度为 T = O ( ∣ v ∣ 2 ) T\ =\ O(|v|^2) T = O(∣v∣2); 稠密图合算
5.3、Kruskal算法(用于稀疏图合算)
- 区别于Prim算法,该算法每次先找出未访问过的最小边;
- 判断是否构成回路;
- 重复上述步骤,直到满足条件
void Kruskal (Graph G)
{
MST = {};
while ( MST中不到 |V|-1 条边 && E中还有边)
{
从E中取一条权值最小的边 E(v,w); //用最小堆存储所有边
将E(v,w)从E中删除;
if (E(v,w)不在MST中构成回路) //并查集
将E(v,w)加入MST;
else
彻底无视E(v,w);
}
if (MST中不到|v|-1条边)
Error("生成树不存在");
}
时间复杂度为 T = O ( ∣ E ∣ log ∣ E ∣ ) T\ = \ O(|E|\log|E|) T = O(∣E∣log∣E∣)
八、拓扑排序
1、AOV网络(Activity On Vertex)
-
简介
- 顶点表示活动,有向边表示活动的先导关系;
- AOV网是有向无环图,即不应该带有回路,因为若带有回路,则会陷入死循环;
- 所有活动可排列成一个线性序列,使得每个活动的所有前驱活动都排在该活动的前面,我们把此序列叫做拓扑序列;
- AOV网络的拓扑序列不是唯一的;
- 只有完成了所有前驱事件后才可以进行后续事件;
-
例子
上图是AOV网络,下面列出事件发生的顺序
- 不聪明的算法,选择直接遍历
void topSort()
{
for (int cnt = 0; cnt < |v|; +=cnt)
{
v = 未输出的入度为0的顶; //O(|v|)
if (这样的v不存在)
{
Error("图中有回路");
break;
}
输出v,或记录v的输出序号;
for (v 的每个邻接点w)
{
inDegreee[w]--;
}
}
}
时间复杂度为 T = O ( ∣ v ∣ 2 ) T\ \ =\ \ O(|v|^2) T = O(∣v∣2)
- 聪明的算法,随时将入度为0的顶点放到一个容器里
void topSort()
{
for (图中每个顶点v)
{
if (inDegree[v] == 0)
enQueue(Q, v);
while (!isEmpty(Q))
{
v = deQueue(Q);
输出v,或记录v的输出序号;
cnt++;
for (v 的每个邻接点w)
{
if (--inDegree[w] == 0)
enQueue(Q, w);
}
}
if (cnt != |v|)
Error("图中有回路");
}
}
时间复杂度为 T = O ( ∣ v ∣ + ∣ E ∣ ) T\ \ =\ \ O(|v|\ +\ |E|) T = O(∣v∣ + ∣E∣)
此算法可以用来检测有向图是否DAG(Directed Acyclic Graph,有向无环图)
2、AOE网络(Activity On Edge)
-
简介
- 一种带权有向图,弧上的权值表示活动持续的时间;
- 关键路径问题(由绝对不允许延误的活动组成的路径),一般用于安排项目的工序;
-
例子