Dijkstra算法&&邻接表数组

后文:Dijkstra算法堆/优先队列优化

前言

  1. 本文将直接使用邻接表的数组形式进行图的存储,邻接矩阵的存储方式在此算法的实现上较为简单,邻接表的链式存储较为麻烦,且申请释放内存会消耗大量时间,在此不再赘述。
  2. 本文使用的邻接表数组存储来源于《啊哈!算法》,由于我水平有限,只能暂时先使用这种包含5个数组的邻接表。
  3. 本文中的算法不记录路径只记录距离。
  4. 本文中所有数组的第一个存储单元即a[0],都没有被使用。
  5. 此算法不能解决带有负权边的图,因此我们规定本文中的图里面不存在负权边。
  6. 代码采用C++实现,不过只要有C语言的基础就可以看懂,没有用到很多C++的东西。
  7. 若对本文存在疑问、意见或建议,欢迎评论及与我本人联系。
  8. 算法的优化详见后文。

术语简介

  1. 权值:表示两点之间的距离、耗费等具有某种意义的数值
  2. 弧:有向的边,在图上用箭头表示,<x,y>表示从点x到点y有一条弧(请注意<y,x>与其是不同的两条弧)
  3. 弧尾:弧的起点,x
  4. 弧头:弧的终点,y
  5. 路径:由连续的弧构成的顶点序列,在图中,存在路径的两点之间不一定存在直达的弧
  6. 入边:以某点为弧头的边
  7. 出边:以某点为弧尾的边

Dijkstra入门

最短路径问题

在图论中,最短路径是一类十分常见的问题,常见的有SSSP(Single-source Shortest Paths,单源最短路径)和APSP(All Pairs Shortest Paths,多源最短路径。而今天讲到的Dijkstra算法,主要用来解决SSSP问题。它指的是以某个点为起点,从该点到其余各个顶点的最短路程/最小代价。此算法也可以解决APSP问题,只是它较为复杂,不推荐使用。请注意此类问题与最小生成树问题的区别与联系:最小生成树问题结果会涉及到图中的所有顶点,而最短路径问题的结果可能只涉及到图中的一部分顶点。

算法思想介绍

在这里插入图片描述
以此有向图为例,为了求解从顶点1开始到各顶点的最短路径,Dijkstra算法首先确定了两个集合A、B,其中A集合中包含已经确定最短路径的顶点,B集合包含未确定最短路径的点。其次我们需要一个数组dis来记录从起点到各个顶点的最短距离,一个数组book来记录顶点是否在A集合中。
接下来我们要对上述两个数组进行初始化,这里我们以1号顶点作为起点。先将book数组全部置为false,表示所有点都未确定最短距离。同时对dis数组进行初始化,访问邻接表,若起点与点i之间存在弧,则将dis[i]置为弧的权值,否则置为inf(无穷)。全部初始化完成后,将起点——1号顶点加入集合A中,即book[1]置为true,众所周知自己到自己的最短路径是0,因此将dis[1]置为0。至此准备工作完成。
此时数组情况如下:

编号bookdis
1true0
2false1
3false12
4falseinf
5falseinf
6falseinf

下面正式开始算法!!!(敲黑板.jpg)

  1. 从除1号顶点之外的其他所有顶点中(即集合B中的顶点)找到离1号顶点最近的顶点,即从以1号顶点为弧尾/起点的弧中找到权值最小的那条弧(这里就是顶点2啦),并将其加入集合A中(即book[2]=true)
  2. 接下来对其余顶点进行处理。以在集合B中的顶点j为弧头/终点,访问以2号顶点为弧尾/起点的弧,观察以2号点为中转点,从1号到j号顶点的路径长度是否缩短,即dis[j]是否大于dis[2]+弧<2,j>权值 ,若大于,则进行松弛。此时我们首先发现,dis[3]>dis[2]+9,因此将dis[3]更新为10。此时数组情况如下:
编号bookdis
1true0
2true1
3false10
4falseinf
5falseinf
6falseinf

紧接着我们又发现,dis[4]>dis[2]+3,因此将dis[4]更新为4。此时数组情况如下

编号bookdis
1true0
2true1
3false10
4false4
5falseinf
6falseinf
  1. 重复上述过程,直到所有的顶点都进入集合A中,算法结束。
    接下来发现集合B中离1号顶点最近的顶点是4号,进行处理后数组如下:
编号bookdis
1true0
2true1
3false8
4true4
5false17
6false19

继续寻找,这次选择3号顶点,进行处理后数组如下

编号bookdis
1true0
2true1
3true8
4true4
5false13
6false19

继续寻找,这次选择5号顶点,进行处理后数组如下:

编号bookdis
1true0
2true1
3true8
4true4
5true13
6false17

继续寻找,这次选择6号顶点,将其加入集合A后,由于它没有出边,且此时所有顶点都已在集合A中,因此无需进行处理。

算法实现

构造出的输入数据如下:

6 9
1 2 1
1 3 12
2 3 9
2 4 3
3 5 5
4 3 4
4 5 13
4 6 15
5 6 4

六个顶点九条边

代码如下:

#include<iostream>//Dijkstra迪杰斯克拉算法模板
#define inf 999999999//定义inf
using namespace std;

int cur=1;//记录当前边的编号
int dis[10];//记录源点到各顶点的最短路径
bool book[10];//记录已经找到最短路径的点
int from[10],to[10],value[10];//分别表示起点,终点,权值
int start[10],nt[10];//分别表示起点和"链域"
//使用邻接表存储图

void reset(int n)
{
	int i;
	for (i=1;i<=n;i++)
		start[i]=-1;
    	return;
}//邻接表初始化

inline void add(int a,int b,int c)//inline可提高此函数运行效率,多用于经常调用的函数
{
	from[cur]=a;//起点/弧尾
  	to[cur]=b;//终点/弧头
    	value[cur]=c;//权值
    	nt[cur]=start[from[cur]];
    	start[from[cur]]=cur;//修改"指针"
    	cur++;
    	return;
}

inline int find(int st,int ed)
{
	int k;
    	k=start[st];
    	while (k!=-1)//在未到最后一条弧时
    	{
   		if (to[k]==ed) break;//表示找到终点/弧头为i的弧
        		k=nt[k];//向后寻找
 	}
  	return k;
}

void dijkstra(int n)
{
   	int i,j,k,min,p;
    	for (i=1;i<=n;i++)
    	{
    		book[i]=false;//开始时都未找到最短路径
  		j=find(1,i);//取1号点(起点)
 		if (j==-1) dis[i]=inf;//未找到弧
    		else dis[i]=value[j];//找到相应弧
  	}//初始化
  	book[1]=true;//将起点加入集合
  	dis[1]=0;//起点到自己距离为0

  	for (i=2;i<=n;i++)//处理剩下的点
   	{
    		min=inf;//记录当前情况下权值最小的弧
  		for (j=1;j<=n;j++)
  		{
   			if (!book[j]&&dis[j]<min)
        		{
  				min=dis[j];
 				p=j;//记录最短弧的终点/弧头
			}
    		}
		book[p]=true;//将最短弧的终点/弧头加入集合
		for (j=1;j<=n;j++)
		{
			if (!book[j])//如果顶点j在集合B中
			{
				k=find(p,j);
				if (k!=-1&&dis[j]>dis[p]+value[k])//如果从p点到j点有弧,且从起点到j点的已有路径长度大于从起点经p点中转再到j点的路径长度
					dis[j]=dis[p]+value[k];//则进行松弛(贪心的体现)
			}
		}
	}
	return;
}

int main()
{
	int m,n,i,x,y,z;
	cin>>n>>m;//n个点,m条边
	reset(n);

	for (i=0;i<m;i++)
	{
		cin>>x>>y>>z;
		add(x,y,z);//添加弧
	}
	dijkstra(n);
	for	(i=1;i<=n;i++)
		cout<<dis[i]<<" ";//输出结果
		
	return 0;
}

邻接表数组简介

存储结构

我使用的邻接表数组包含了5个数组,分别是from ,to ,value ,start ,nt .它们分别表示某条路径的起点、终点、权值,start[i]表示顶点i的第一条边所在前三个数组中的位置/编号,相当于邻接链表中表示顶点的头结点,nt[i]表示编号为i的边的下一条边的编号,类似链表中的链域,-1相当于NULL。
注意,前三个数组和nt数组大小至少要为最大边数+1,start数组大小至少为总点数+1。还要有一个变量来记录当前弧的编号。

int cur=1;//记录当前边的编号
int from[10],to[10],value[10];//分别表示起点,终点,权值
int start[10],nt[10];//分别表示起点和"链域"
//使用邻接表存储图

初始化

void reset(int n)
{
	int i;
	for (i=1;i<=n;i++)
		start[i]=-1;//将各顶点第一条边的编号置为-1,类似邻接链表中各顶点的头结点链域置为NULL 
	return;
}

按照题设,初始化后结果如下

numstart
1-1
2-1
3-1
4-1
5-1
6-1

存储方式

6 9
1 2 1
1 3 12
2 3 9
2 4 3
3 5 5
4 3 4
4 5 13
4 6 15
5 6 4

假设此时我们已经读入第一条弧1 2 1,即a=1,b=2,c=1,首先此时cur=1,第一条边编号为1,首先我们将编号为1的边的各项数据存入数组,接下来开始修改“指针”,这里使用的方法类似于链表插入中的头插法,即将新结点插入头结点与第一个结点之间,新结点将代替原来的第一个结点,原来的第一个结点位置后移。这里首先将新的弧与已存在的弧(或者是-1)相连接,再将其连接到头结点上。

inline void add(int a,int b,int c)
{
       from[cur]=a;//起点/弧尾
       to[cur]=b;//终点/弧头
       value[cur]=c;//权值,全部存入数组中
       nt[cur]=start[from[cur]];//修改"链域"
       start[from[cur]]=cur;//与头结点相连接
       cur++;
       return;
}

处理后数组情况如下

numfromtovaluentnumstart
1121-111

接下来读取第二条弧,处理后结果如下

numfromtovaluentnumstart
1121-112
2131212-1

以此类推,全部读入后,结果如下

numfromtovaluentnumstart
1121-112
21312124
3239-135
4243348
5355-159
6434-16-1
745136
846157
9564-1

访问方式

单个访问

这里以访问出边最多的顶点4号顶点为例,若想访问所有以4号顶点为起点/弧尾的弧,要首先访问start[4],得到8,则从编号为8的弧开始访问,得到弧4 6 15。之后访问nt[8],得到7,则访问编号为7的弧,得到4 5 13。再访问nt[7],得到6,则访问编号为6的弧,得到4 3 4。再访问nt[6],得到-1,至此访问结束。可以发现访问顺序与输入顺序正好相反,这也正是头插法的特性之一。

inline void find(int st,int ed)//st=4
{
       int k;
       k=start[st];
       while (k!=-1)//在未到最后一条弧时
       {
              cout<<from[k]<<" "<<to[k]<<" "<<value[k]<<endl;//访问(这里用输出代替访问)
              k=nt[k];//向后寻找
       }
       return;
}

遍历

遍历只要在上述算法的条件下,加上一层循环,即可实现顺序表的遍历

int k;
for (k=1;k<=n;k++)
{
	k=start[st];
	while (k!=-1)//在未到最后一条弧时
	{
		cout<<from[k]<<" "<<to[k]<<" "<<value[k]<<endl;//访问(这里用输出代替访问)
		k=nt[k];//向后寻找
	}
}

参考书目

  1. 《啊哈!算法》 ——啊哈磊 人民邮电出版社
  2. 《数据结构》 ——严蔚敏,李冬梅,吴伟民 人民邮电出版社

后记

  1. Dijkstra算法由于使用贪心策略,所以不能解决带有负权边的问题。若有一条不与起点直接相连的负权边,那么当扩展到这条边时,A中顶点到起点的路径长度可能会更短,而根据算法规定,集合A中的顶点到起点的距离不会被更新,这与此算法的前提——集合A中的点已找到最短路径 相矛盾,因此算法的正确性将无法保证,结果将会出错,这也是贪心算法的局限性所在。
  2. 由于使用邻接表存储路径,其按照权值是无序的,这将导致在查找边的时候浪费大量时间
inline int find(int st,int ed)
{
	int k;
	k=start[st];
 	while (k!=-1)//在未到最后一条弧时
 	{
  		if (to[k]==ed) break;//表示找到终点/弧头为i的弧
  		k=nt[k];//向后寻找
 	}
 	return k;
}

为了进一步提高效率,可以使用堆/优先队列进行优化,使边按照权值是有序的。这样每次取最短边时,只需要使队首元素出列即可,大大提高了时间效率。

  1. 未优化的Dijkstra算法时间复杂度为O(n^2)
  2. 最后挂一道例题 COGS 2 旅行计划,此题采用了以邻接矩阵为存储结构的Dijkstra算法,非常经典的一道题
  • 10
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值