经过这一学期的学习,实际上感觉自己对运筹仍然理解浅显。特别是在问老师单纯形法当目标函数要求最大,检验数相同时,是否选取 θ i \theta_i θi 越小的 σ j \sigma_j σj 对应的入基变量能更快求得最优解时,才明白单纯形法也不过是在可行域中,对顶点进行一种枚举,这一本质从第二章线性规划与单纯形法一直延续到第六章整数线性规划,之所以需要约定例如取检验数 σ i \sigma_i σi 最大的变量为入基变量,主要是为了在处理自变量非常多的优化问题时,易于编程。
从而我又意识到,运筹是一门与实际联系非常紧密的学科,所以算法的时间复杂度也应该是着重考虑的,例如既然我们所学的单纯形法本质是枚举,那么他的时间复杂度也应该是线性规划后对顶点进行枚举的时间复杂度,这在教材中基本上没有提到,希望未来能有机会进行更深入的学习。
所以本文我尝试对最短路问题的算法——Dijkstra算法进行一定阐述,并尽量加入自己的理解,最后对一道求最短路的问题进行代码实现。
提起“图”(graph),我的第一反应就是地图(map),有用点标注出的城市、村县,点越大代表着人口数量越多,以及由不同颜色曲线标注出的经过这些点的道路。将这两个概念抽象出来,也就是数学上所说的由点和连接点之间的边构成的图。
这里好玩的是,英文中的graph往往指用纵横坐标之间关系的曲线表示两个量之间的图表,和map相去甚远,不知是我理解有误,还是英文起名的不妥。
定义 1.1 图 图是由一些点及点之间的连线(不带箭头或带箭头)所组成的,记为 G = ( V , E ) G=(V,E) G=(V,E),其中 V , E V,E V,E 分别是 G G G 的点集合和边集合。
特别的,两个顶点 v i , v j v_i,v_j vi,vj 之间的边若没有方向,记为 [ v i , v j ] [v_i,v_j] [vi,vj],若从 v i v_i vi 出发指向 v j v_j vj 则记为 ( v i , v j ) (v_i,v_j) (vi,vj),按图的边是否有方向,又分别称为有向图和无向图,用 e i j e_{ij} eij 统一表示 v i , v j v_i,v_j vi,vj 之间的有向边或无向边。对于我们本篇的主题Dijkstra算法而言,我们讨论的重点是有向图的问题。
定义 1.2 赋权图 对图 G = ( V , E ) G=(V,E) G=(V,E),对 G G G 中的每一条边 e i j e_{ij} eij,给一个相应的数 w i j w_{ij} wij,则称这样的图 G G G 为赋权图, w i j w_{ij} wij 称为边 e i j e_{ij} eij 的权。
陈述 2.1 最短路问题: 在一个共有 n n n 个顶点的赋权有向图 G = ( V , E , W ) G=(V,E,W) G=(V,E,W),其中对每一个有向边 ( v i , v j ) (v_i,v_j) (vi,vj) 相应的有权 w i j w_{ij} wij,对于 G G G 中给定的两个顶点 v s , v t v_s,v_t vs,vt,设 P P P 是 G G G 中从 v s v_s vs 到 v t v_t vt 的一条路,其权重 w ( P ) w(P) w(P) 为 P P P 中所有边的权重之和,所谓最短路问题即解决最优问题:
w ( P min ) = min P w ( P ) . w(P_{\text{min}})=\min_Pw(P). w(Pmin)=Pminw(P).
换句话说,最短路径问题就是说在赋权有向图 G G G 中从 v s v_s vs 到 v t v_t vt 的多条路径中,寻找一条各边权值之和最小的路径。
当权重 w i , j ≥ 0 w_{i,j}\geq0 wi,j≥0 时,目前公认最好的方法由荷兰计算机科学家Dijkstra在1959年提出,算法的主要想法与贪心算法类似,第一步首先从初始点 v 0 v_0 v0 出发,记 S 0 = { v 0 } S_0=\{v_0\} S0={v0},先选取与 v 0 v_0 v0 直接相连的点中,边的权重最小的顶点 v 1 v_1 v1,并记 S 1 = { v 0 , v 1 } , w ( P 1 ) = w 01 S_1=\{v_0,v_1\},w(P_1)=w_{01} S1={v0,v1},w(P1)=w01;第二步从 v 0 v_0 v0 出发,选取与 v 0 v_0 v0 直接相连或者与 v 1 v_1 v1 直接相连的点中,边的权重最小的顶点 v 2 v_2 v2,记 S 2 = { v 0 , v 1 , v 2 } , w ( P 2 ) = w 01 + w 12 S_2=\{v_0,v_1,v_2\},w(P_2)=w_{01}+w_{12} S2={v0,v1,v2},w(P2)=w01+w12,以此类推下去,这样通过找每一步的最优解,最终得到全局的最优解。当然这是需要证明的。从过程中我们知道至多经过 n − 1 n-1 n−1 次比较大小,我们就能确定 v 0 v_0 v0 到任意点的最短路径。
这样语言直接描述还是挺考验人阅读的耐心的,一下用图表进行解释:
例题2.2
如上图,我们有 6 6 6 个顶点之间的有向图,并且距离都写在旁边,我们要寻找从 v 0 v_0 v0 出发,到各个顶点距离的最小值,我们可以列出下表:
也就是先从与 v 0 v_0 v0 直接相连的顶点中找到最短路径为 v 0 v_0 v0 到 v 2 v_2 v2,因此确定了他们之间的最短距离,再继续寻找下一个最短路径,列表的结果如上,最后当 S S S 中包含了所有能够到达的顶点即可停止,本例中因为不存在从 v 0 v_0 v0 出发到达 v 1 v_1 v1 的路径,所以 S S S 中不包含 v 1 v_1 v1.
接下来我们证明Dijstra算法通过逐步求最优,最后得到的最短路径就是全局的最短路径。
证明 2.3 Dijkstra算法证明
用归纳法进行证明,对 i = 0 i=0 i=0,也就是 v 0 v_0 v0 到自身的最短路为 w ( P 0 ) = 0 w(P_0)=0 w(P0)=0,显然为一条最短路。
假设 i = k i=k i=k 时得到的 P k P_k Pk 是 v 0 v_0 v0 到点 v k v_k vk 的最短路径,往证 i = k + 1 i=k+1 i=k+1 时得到 v 0 v_0 v0 到 v k + 1 v_{k+1} vk+1 时的最短路径。事实上此时 P k P_k Pk 是从 v 0 v_0 v0 到 v i , 0 ≤ i ≤ k v_i,0\leq i\leq k vi,0≤i≤k 各点的最短路径,因为如果有 P i ′ P_i' Pi′ 使得 w ( P i ′ ) < w ( P i ) w(P_i')
首先从 v 0 v_0 v0 到 v k + 1 v_{k+1} vk+1 的最短路径必然经过 v 1 , … , v k v_1,\dots,v_k v1,…,vk,否则若 v 0 , … , v i , v k + 1 v_0,\dots,v_i,v_{k+1} v0,…,vi,vk+1 为最短路径,这意味着 w i , k + 1 < w i , i + 1 w_{i,k+1}
因此, P k + 1 P_{k+1} Pk+1 为从 v 0 v_0 v0 到 v k + 1 v_{k+1} vk+1 的最短路径,Dijstra算法的正确性得证。
要实现Dijkstra算法,我参考了书籍 [ Yan 07 ] [\text{Yan}07] [Yan07] 第七章,这一章主要讲述了对于图的存储结构相关的编程方法,算法基于 C++ 实现,编译软件使用Dev-C++.
既然“图”这一对象的构成要素主要是点和边,那么我们可以定义图的邻接矩阵:
定义 3.1 图的邻接矩阵 对于有 n + 1 n+1 n+1 个顶点的图 G = ( V , E ) G=(V,E) G=(V,E),其邻接矩阵可用二维数组表示:
G . e d g e [ i ] [ j ] = { 1 e i j ∈ E , 0 e i j ∉ E . \begin{aligned} G.edge[i][j]= \begin{cases} 1 & e_{ij}\in E, \\ 0 & e_{ij}\notin E. \end{cases} \end{aligned} G.edge[i][j]={10eij∈E,eij∈/E.
对于无向图,因为 v i v_i vi 与 v j v_j vj 相连意味着 v j v_j vj 也与 v i v_i vi 相连,因此无向图的邻接矩阵必然为对称阵,而有向图从 v i v_i vi 指向 v j v_j vj 的边,则在邻接矩阵中表示为 g i j = 1 g_{ij}=1 gij=1,反之 g j i g_{ji} gji 则不一定。
对于赋权有向图,我们有类似的定义,只需要把邻接矩阵中等于 1 1 1 的元素,改为相应的权重即可。
在算法实现时,我们需要引入一个辅助向量 D D D,其每个分量表示当前找到的,从初始点 v 0 v_0 v0 到除 v 0 v_0 v0 外每个点的最短路径长度,先为 D [ i ] , i = 1 , … , n D[i],i=1,\dots,n D[i],i=1,…,n 赋初值:若 e 0 i e_{0i} e0i 存在,则 D [ i ] = w 0 i D[i]=w_{0i} D[i]=w0i,否则 D [ i ] = ∞ D[i]=\infty D[i]=∞,也就是算法的第一步,先在直接相连结的点中寻找最短路径,因此按照上节中Dijkstra算法步骤的描述,
D [ j ] = min i { D [ i ] ∣ v i ∈ V } , D[j]=\min_i\{D[i]|v_i\in V\}, D[j]=imin{D[i]∣vi∈V},
这样的 j j j 对应的长度 D [ j ] D[j] D[j] 就是从 v 0 v_0 v0 出发到 v j v_j vj 的最短路径长度,若 v j v_j vj 就是我们要计算的终点,此时就可以停止,若不是,则再次进行迭代:
首先我们记 S S S 为已经求得从 v 0 v_0 v0 出发的最短路径的重点集合,那么从Dijstra算法的证明中可以看出,对于下一条具有最小权重的顶点 v k v_k vk,路径或者为 ( v 0 , v k ) (v_0,v_k) (v0,vk) 或者为 ( v 0 , v j , v k ) (v_0,v_j,v_k) (v0,vj,vk),因此下条最短路径的长度为:
D [ k ] = min i { D [ i ] ∣ v i ∈ V − S } , D[k]=\min_i\{D[i]|v_i\in V-S\}, D[k]=imin{D[i]∣vi∈V−S},
其中 D [ i ] D[i] D[i] 或者是 w 0 i w_{0i} w0i,或者是 D [ j ] + w j i D[j]+w_{ji} D[j]+wji.
经过以上分析,我们知道以下算法步骤(主要用到数组和链表):
我们用如下例题来进行实验:
例题3.2 给定厦门大学校园地点以及不同地点之间的距离,构成一个无向赋权图,用Dijkstra算法实现输入指定两个地点,输出其间最短距离以及最短路径的程序。
Dijkstra算法求出的结果是从初始点 v 0 v_0 v0 到其余各顶点的最短路径,那么当然包含了目的地,因此只需要对每个顶点使用Dijkstra算法求解,再输出到目的地的最短路径即可。
这里我的输入地图如下:
其中,图书馆到科艺距离为110,图书馆到芙蓉餐厅距离为400,科艺到建南礼堂距离为100,科艺到三家村广场距离为130,科艺到芙蓉餐厅距离为170,三家村广场到芙蓉餐厅距离为50,建南礼堂到芙蓉餐厅距离为200,芙蓉餐厅到学生公寓距离为500.
其中最关键的步骤为dijkstra算法的实现步骤:
void AdjacencyList::ShortestPath_dijkstra(GraphAdjList *G, int P[6][6], int D[6][6]) {
//初始化D与P
for (int v = 0; v < G->numVertexes; ++v)
{
for (int w = 0; w < G->numVertexes; ++w)
{
if(_distance[v][w]==0&&v!=w){
_distance[v][w] = 10000;
}
D[v][w] = _distance[v][w];
P[v][w] = w;
}
}
for (int k = 0; k < G->numVertexes; ++k)
{
for (int v = 0; v < G->numVertexes; ++v)
{
for (int w = 0; w < G->numVertexes; ++w)
{
if (D[v][w] > D[v][k] + D[k][w])
{
D[v][w] = D[v][k] + D[k][w];
P[v][w] = P[v][k];
}
}
}
}
}
其中数组 D D D 即为记录最短路径的辅助数组,而数组 P P P 则用来记录与顶点最短相连接的前驱,也就是记录最短路径是怎样走的。
编译后的结果如下,其中图书馆编号0,科艺编号1,建南礼堂编号2,三家村广场编号3,芙蓉餐厅编号4,学生公寓编号5.
程序结果展示如下:
比如从科艺出发到芙蓉餐厅的路径,呈一个三角形,其中直达的距离为170,经过三家村广场的路径距离为180,那么结果为:
从图书馆到芙蓉餐厅的路径共有四条:
#include
#include
#include
using namespace std;
//存储最短路径值
int ShortestPathvalue[6][6] = {0};
//存储具体路径
int ShortestPathmatrix[6][6] = {0};
//地点信息
char _mapName[6][50] = {"图书馆", "科艺", "建南礼堂", "三家村广场", "芙蓉餐厅", "学生公寓"};
//距离信息,_distance[0][1] = 50;代表从下标为0到下表为1地点距离为50
int _distance[6][6] = {0};
//边表结点
typedef struct EdgeNode {
//顶点对应的下标
int adjvex;
//权值
int weight;
//指向下一个邻接点
struct EdgeNode *next;
} edgeNode;
//顶点表结点
typedef struct VertexNode {
//顶点数据
char data[50];
//边表头指针
edgeNode *firstedge;
} VertexNode, AdjList[100];
//集合
typedef struct {
AdjList adjList;
//顶点数和边数
int numVertexes, numEdges;
} GraphAdjList;
class AdjacencyList {
public:
void ShowALGraph(GraphAdjList *G);
void Test();
//初始化地图
void InitMap(GraphAdjList *G);
//创建地图
void CreateALGraph(GraphAdjList *G);
//计算各个顶点之间最短路径
void ShortestPath_dijkstra(GraphAdjList *G, int P[6][6], int D[6][6]);
//输出路径长度和具体路径
void ShowShortestResult(int originPos,int endPos);
};
//创建地图
void AdjacencyList::CreateALGraph(GraphAdjList *G) {
edgeNode *e;
//读入顶点信息,建立顶点表
for (int i = 0; i < G->numVertexes; i++)
{
//读入顶点信息
strcpy(G->adjList[i].data, _mapName[i]);
//将边表置为空表
G->adjList[i].firstedge = NULL;
}
//建立边表(头插法)
for (int i = 0; i < G->numVertexes; i++)
{
for (int j = 0; j < i; j++)
{
int temp;
if (_distance[i][j] != 0 || _distance[j][i] != 0)
{
if (_distance[i][j] != 0)
{
temp = _distance[i][j];
_distance[j][i] = _distance[i][j];
}
else
{
temp = _distance[j][i];
_distance[i][j] = _distance[j][i];
}
e = new EdgeNode;
e->adjvex = j;
e->next = G->adjList[i].firstedge;
e->weight = temp;
G->adjList[i].firstedge = e;
e = new EdgeNode;
e->adjvex = i;
e->next = G->adjList[j].firstedge;
e->weight = temp;
G->adjList[j].firstedge = e;
}
}
}
}
void AdjacencyList::ShowALGraph(GraphAdjList *G) {
for (int i = 0; i < G->numVertexes; i++)
{
cout << "顶点" << i << ": " << G->adjList[i].data << "--firstedge--";
edgeNode *p = new edgeNode;
p = G->adjList[i].firstedge;
while (p)
{
cout << p->adjvex << "--Weight: " << p->weight << "--Next--";
p = p->next;
}
cout << "--NULL" << endl;
}
}
//初始化地图基本数据
void AdjacencyList::InitMap(GraphAdjList *G) {
//输入顶点数和边数
G->numVertexes = 6;
G->numEdges = 50;
_distance[0][1] = 110;
_distance[0][4] = 400;
_distance[1][2] = 100;
_distance[1][3] = 130;
_distance[1][4] = 170;
_distance[2][4] = 200;
_distance[3][4] = 50;
_distance[4][5] = 500;
}
void AdjacencyList::ShortestPath_dijkstra(GraphAdjList *G, int P[6][6], int D[6][6]) {
//初始化D与P
for (int v = 0; v < G->numVertexes; ++v)
{
for (int w = 0; w < G->numVertexes; ++w)
{
if(_distance[v][w]==0&&v!=w){
_distance[v][w] = 10000;
}
D[v][w] = _distance[v][w];
P[v][w] = w;
}
}
for (int k = 0; k < G->numVertexes; ++k)
{
for (int v = 0; v < G->numVertexes; ++v)
{
for (int w = 0; w < G->numVertexes; ++w)
{
if (D[v][w] > D[v][k] + D[k][w])
{
D[v][w] = D[v][k] + D[k][w];
P[v][w] = P[v][k];
}
}
}
}
}
void AdjacencyList::ShowShortestResult(int originPos,int endPos) {
int temp;
cout << "地点" << _mapName[originPos] << "到地点" << _mapName[endPos] << "最短距离为" << ShortestPathvalue[originPos][endPos] << endl;
temp = ShortestPathmatrix[originPos][endPos];
cout<<"具体路径为:"<<_mapName[originPos]<<"——>";
while (temp!=endPos){
cout<<_mapName[temp]<<"——>";
temp = ShortestPathmatrix[temp][endPos];
}
cout<<_mapName[endPos]<<endl<<endl;
}
int main() {
AdjacencyList adjacencyList;
int originPos,endPos;
GraphAdjList *GA = new GraphAdjList;
adjacencyList.InitMap(GA);
adjacencyList.CreateALGraph(GA);
adjacencyList.ShortestPath_dijkstra(GA,ShortestPathmatrix,ShortestPathvalue);
while(1){
cout<<"输入任意两个景点:"<<endl;
cin>>originPos>>endPos;
adjacencyList.ShowShortestResult(originPos,endPos);
}
return 0;
}
[ Qian 12 ] [\text{Qian}12] [Qian12] 《运筹学》第四版,北京清华大学出版社,2012.
[ Yan 07 ] [\text{Yan}07] [Yan07] 《数据结构(C语言版)》,北京清华大学出版社,2007.
以及在调试代码中遇到困难时参考的csdn,博客园中关于Dijkstra算法的博客。