最短路径——Dijskra 算法(C语言实现)

一、Dijskra 算法简介

1.0 引言

        单源最短路径:指从同一个起始点到其它所有顶点的最短路径,共有 n-1 条最短路径。

        多源最短路径:指每一对顶点的最短路径,共有 C_{n}^{2} = \frac{n_{!}}{2_{!}(n-2)_{!}}条对最短路径。

        Dijskra 迪杰斯特拉  算法:解决带权图中的单源最短路径问题。

        Floyd 弗洛伊德 算法:解决带权图中的多源最短路径问题。

1.1 Dijskra 算法思想

思想逻辑:

        1. 设置三个数组,

                Djst[n] 记录 起点至其它结点的最短路径的

                Path[n] 记录 起点至其它结点的最短路径轨迹,

                Set[n] 记录 是否已找到起点至其它结点最短路径(1-已找到,0-未找到)。

                *数组下标对应结点的下标,例如:Dist[n] 表示 起点至结点 n 的最短路径值。

        2. 初始,

                Djst 数组中存储的是与起点直通的结点的路径值(非直通,则值为\infty)

                Path 数组中存储的是与起点直通的结点的下标(非直通,则为 -1)

                Set 数组中存储的是0,表一条最短路径都没找到

        3. 从Djst 数组中,选取权值最小的路径作为起点到此路径尾结点的最短路径,

            然后 Path 数组记录最短路径尾结点的前驱结点(尾结点前面的第一个结点),

            Set 数组再将最短路径的尾结点对应的值置为 1,表示已找到此结点最短路径。

        4. 动态更新点至其它结点的最短路径值,即更新 Djst 数组。再重复步骤3和4,直到 Set[n] 全为 1,则找完了起点至其起它结点的最短路径。

  *Dijskra 迪杰斯特拉 算法并不能直接得到最短路径,但将最短路径信息保存在了 Djst 数组和Path 数组中,可以通过 Djst 数组和 Path 数组间接得到完整的最短路径。

1.2 Dijskra 过程图解

 

1.3 Dijskra 核心问题

问题1 如何在起点至某一个结点的多条路径中得到那条权值最小的路径

        应对策略:创建一个数组 Djst 保存起点至其它结点的当前最短路径,然而其中权值最小的那一条边就是真正的最短路径。

       A) 为什么数组 Djst 中 权值最小的路径就是真正的最短路径,而其它的路径不是呢?

       因为:有些结点对应的权值可能不是起点至这些结点真正的最短路径,因为在动态更新最短路径过程中,是在 直通路径+所有中转路径 中进行比较得到最短路径,但是又不能保证一轮寻找就得到中转结点至其相邻结点的最短路径,可能还有些中继结点至相邻结点的中转路径没有寻找。

       B) 那如何动态更新得到起点至每一个其它结点最短路径呢?

         如果得到每一个结点至其相邻结点的最短路径,然后寻找起点至相邻结点的最短路径,再由相邻结点出发寻找到与自己相邻的结点的最短路径,一直这样进行下去,真正的最短路径 = 每小段最短路径之和,就能得到起点至其它所有结点的最短路径了,由局部最优解得到全局最优解(局部最优解的叠加)。

问题2 如何得到起点至其它所有结点的最短路径(经过的点及边)

        应对策略:创建一个 Path 数组用来保存每一个结点所在最短路径上的前驱结点,然后通过一步步向后回溯就得到完整的最短路径了

问题3 如何区分是否已经找到起点至某一结点的最短路径

        应对策略:若找到起点至某一结点的最短路径,则对此结点作标记,将此结点在 Set 数组中对应位置的值 记为 1,表已找到0表示未找到。

二、Dijskra 算法的实现

2.0 头文件

#include<stdio.h>

// 顶点的最大个数
#define MaxVerNum 30
#define INF	32767		// INF infinite 无穷大,表权重无穷大

// 状态值
#define OK 1
#define ERROR 0
#define TRUE 1
#define FALSE 0

// 状态码 -- 状态值
typedef int Status;

// 1.邻接矩阵 - 存储结构
// 定点、边的类型
typedef char VertexType;   // 顶点的数据类型
typedef int ArcType;	// 边的数据类型

// 构造数据类型
typedef struct
{
	VertexType Verts[MaxVerNum];
	ArcType DirArcs[MaxVerNum][MaxVerNum];			// 有向图 -- 矩阵表示法
	int VerNum;										// 顶点个数
	int ArcNum;										// 边的个数
}DirAMGraph;		// Adjacency  Matrix  Graph 邻接矩阵

// 函数声明
Status CreateDN(DirAMGraph* G);	// 创建有向图

void Print_DirGraph(DirAMGraph* G);	// 打印有向图的邻接矩阵

int LocateDirVer(DirAMGraph* G, VertexType* v);	// 获取 顶点 ver 的下标

// Dijsktra 狄杰斯特拉算法.  查找某顶点到其它任意顶点的最短路径
Status Dijsktra(DirAMGraph* G, int v, int djst[MaxVerNum], int path[MaxVerNum]);

// 打印起点到其它结点的最短路径 --Dijsktra
void PrintDijsktra(DirAMGraph* G, int djst[MaxVerNum], int path[MaxVerNum]);

2.1 函数实现文件

// 获取 顶点 ver 的下标
int LocateDirVer(DirAMGraph* G,VertexType* v)
{
	if (!G) return ERROR;

	int i;
	for (i = 0; i < G->VerNum; i++)
		if (*v == G->Verts[i])
			return i;

	return -1;	// 返回 -1 表示未找到顶点
}


// 创建有向图
Status CreateDN(DirAMGraph* G)			// UndirectNet
{
	if (!G) return ERROR;

	printf("请输入顶点及边个数(Vers Arcs): ");
	scanf("%d %d", &G->VerNum, &G->ArcNum);
	getchar();

	int i;
	//录入顶点
	printf("\n请输入顶点的值(英文字母): ");
	for (i = 0; i < G->VerNum; i++)
	{
		do
		{
			VertexType v;
			scanf("%c", &v);		//scanf("%[a-zA-Z]", G->VerNum);	// 只接收26个英文字母
			getchar();
			if ((65 <= v && v <= 90) || (97 <= v && v <= 122))
			{
				G->Verts[i] = v;
				break;
			}
			printf("输入错误,请输入英文字母!\n");

		} while (1);	// do-while循环用于处理错误输入

	}

	//初始化所有边的权
	int j;
	for (i = 0; i < G->VerNum; i++)
		for (j = i; j < G->VerNum; j++)
		{
			G->DirArcs[i][j] = INF;	// 权重为无穷大,表示两顶点非邻接
			G->DirArcs[j][i] = INF;
		}

	//录入边的权值
	printf("\n请输入边关联的顶点及权值(v1 v2 weight): \n");
	for (i = 0; i < G->ArcNum; i++)
	{
		VertexType v1, v2;
		int w;
		do
		{
			scanf("%c %c %d", &v1, &v2, &w);
			getchar();

			if (v1 < 65 || (90 < v1 && v1 < 97) || 122 < v1)
			{
				printf("输入错误,请输入英文字母!\n");
				continue;
			}
			if (v2 < 65 || (90 < v2 && v2 < 97) || 122 < v2)
			{
				printf("输入错误,请输入英文字母!\n");
				continue;
			}
			//查找顶点位置
			int a, b;
			a = LocateDirVer(G, &v1);
			b = LocateDirVer(G, &v2);

			if (a < 0)		// 判断顶点是否存在
			{
				printf("输入的顶点%c不存在,请重新输入!\n", v1);
				continue;
			}
			if (b < 0)		// 判断顶点是否存在
			{
				printf("输入的顶点%c不存在,请重新输入!\n", v2);
				continue;
			}

			//链接到两顶点的边赋权值
			G->DirArcs[a][b] = w;
			//G->DirArcs[b][a] = w;
			break;

		} while (1);	// do-while循环用于处理错误输入

	}
	return OK;
}

void Print_DirGraph(DirAMGraph* G)
{
	int i, j;
	printf("\n  ");
	for (i = 0; i < G->VerNum; i++)
		printf("%-7c", G->Verts[i]);

	//printf("\n  %5c %5c  %5c  %5c  %5c",);
	for (i = 0; i < G->VerNum; i++)
	{
		//printf("%c ", 'A' + i);
		for (j = 0; j < G->VerNum; j++)
		{
			if (j == 0)  printf("\n%c ", G->Verts[i]);
			printf("%-7d", G->DirArcs[i][j]);
		}
	}
}

Status Dijsktra(DirAMGraph* G, int star, int djst[MaxVerNum], int path[MaxVerNum])
{
/*
	思路:
		1.用三个数组,分别记录: 
		   a.起点起点到其它顶点的最短路径开销
		   b.起点走最短路径到其它结点所经过的中介点,
		        且这个中介点是与结点直接相连的直接中介点(如:a->b->c->d 中c是d的直接中介点,而b不是)
		   c.已经找到的距离起点路径开销最小的结点

		2.初始阶段,直接找与起点直连的结点,并将起点与它们之间的路径开销视作最小开销。起点属于最短路径集合
		3.在不属于最短路径集合的结点中,找出到起点路径开销最小的结点,然后合并到最短路径集合
		4.动态更新起点到非最短路径集合结点的最小路径开销
			a.起点到中介点的开销 + 中介点到目的结点的开销 是否 小于之前计算得到的开销值,若是,则更新开销值
			  这里的逻辑是:局部最优,则整体最优。
			    如:a->b 是最优解,若 b->c是最优解,则 a->b->c 也是最优解
				又比如:有一个复合函数 F(x) = f1(x) + f2(x) + ... + fn(x),求复合函数 F(x) 的最小值
				   你会怎么求?求出 f1,f2,...,fn 的最小值,然后加起来不就是 F(x)的最小值吗?
	*/


	//处理空指针、不存在的数组
	if (!G || !djst || !path)
		return ERROR;

	// 找到的最短路径集。Shortest Path Set
	int Set[MaxVerNum] = { 0 };	// 0表不属于最短路径集合,1表属于最短路径集合

	int i;
	for (i = 0; i < G->VerNum; i++)
	{
		//初始化起点到其它顶点的 最小路径开销值
		djst[i] = G->DirArcs[star][i];

		if (djst[i] < INF)  // 权重小于无穷大,则两顶点连通
			path[i] = star;	// 将起点设置为与其相连通结点的前端结点
		else
			path[i] = -1;	//初始化与起点非连通的结点的前端结点为-1,表示不是其中介点
	}
	
	Set[star] = 1;	// 起点属于最短路径集合

	int j, k;

	for (i = 0; i < G->VerNum - 1; i++)
	{
		int min = INF,idx = star;
		// 寻找起点到其它顶点的最小路径,然后将其并入最短路径
		for (j = 0; j < G->VerNum; j++)
		{
			// 起点到其它结点路径最小的结点,且未不属于最短路径集合的结点
			if (djst[j] < min && Set[j] != 1)
			{
				idx = j;
				min = djst[j];
			}
		}
		
		Set[idx] = 1;	// 找到的最小路径,合并到最短路径集合

		// 动态更新起点到其它结点的路径最小开销值
		for(j = 0; j < G->VerNum; j++)		// j 表从起点出发到其它结点所经过的中介点
			for (k = 0; k < G->VerNum; k++)
			{
				// 若起点到中介点的路径开销 + 中介点到其它顶点的开销 < 之前计算的起点到其它结点的开销,就更新开销值
				if (djst[j] + G->DirArcs[j][k] < djst[k])
				{
					// 更新起点到其它结点的 路径最小开销值
					djst[k] = djst[j] + G->DirArcs[j][k];

					// 起点到其它结点的路径最小开销值更新后,记录结点的前端结点(也是中介点)
					path[k] = j;
				}
			}
	}
	return OK;
}

void Trail(DirAMGraph* G, int v, int djst[MaxVerNum], int path[MaxVerNum])
{
	//若顶点 v 为起点 -1,则输出顶点
	if (path[v] == -1)
	{
		printf("%c", G->Verts[v]);
		return;
	}

	//顶点 v 不是起点,则寻找顶点 v 的直接中介点
	Trail(G, path[v], djst, path);

	
	// 打印当前结点及路径开销
	int node = path[v];
	printf("-%d->%c", G->DirArcs[node][v],G->Verts[v]);
}

// 打印起点到其它结点的最短路径
void PrintDijsktra(DirAMGraph* G, int djst[MaxVerNum], int path[MaxVerNum])
{
	// 处理不存在的数组
	if (!djst || !path) return;

	int i,star = 0;
	//找起点
	for (i = 0; i < G->VerNum; i++)
		if (path[i] == -1)
		{
			star = path[i];
			break;
		}

	for (i = 0; i < G->VerNum; i++)
	{
		if (path[i] == star) continue;

		Trail(G, i,djst, path);
		printf("  -总开销:%d\n",djst[i]);
	}
}

2.3 主函数

int main()
{
	DirAMGraph amg;

	CreateDN(&amg);

	Print_DirGraph(&amg);

	int path[MaxVerNum];
	int djst[MaxVerNum];

	VertexType v;
	printf("\n请输入最短路径的起点: ");
	scanf("%c", &v);
	int index = LocateDirVer(&amg, &v);

	if (index < 0)
	{
		printf("图中不存在顶点%c\n", v);
		return 0;
	}

	Dijsktra(&amg, index, djst, path);

	int i;
	// 打印下标
	printf("\n下标: ");
	for (i = 0; i < amg.VerNum; i++)
		printf("%-6d", i);

	//打印起点到各顶点的最短路径开销
	printf("\ndjst: ");
	for (i = 0; i < amg.VerNum; i++)
		printf("%-6d",djst[i]);

	//打印路径
	printf("\npath: ");
	for (i = 0; i < amg.VerNum; i++)
		printf("%-6d",path[i]);
	printf("\n");
	
	printf("\n起点%c到所有顶点的最短路径: \n",v);
	PrintDijsktra(&amg, djst, path);

	return 0;
}
/*
	    B -→7--→ E
	  ↗ ↘1   6↗↑↘6
	↗4    ↘ ↗  ↑  ↘
   A--→6---→ C   ↑1   G
	↘6    ↗ ↘  ↑  ↗
	  ↘ ↗2   4↘↑↗8
	    D -→5--→ F


请输入顶点及边个数(Vers Arcs): 7 12

请输入顶点的值(英文字母): A B C D E F G

请输入边关联的顶点及权值(v1 v2 weight):
A B 4
A C 6
A D 6
B C 1
B E 7
C E 6
C F 4
D C 2
D F 5
E G 6
F E 1
F G 8

  A      B      C      D      E      F      G
A 32767  4      6      6      32767  32767  32767
B 32767  32767  1      32767  7      32767  32767
C 32767  32767  32767  32767  6      4      32767
D 32767  32767  2      32767  32767  5      32767
E 32767  32767  32767  32767  32767  32767  6
F 32767  32767  32767  32767  1      32767  8
G 32767  32767  32767  32767  32767  32767  32767
请输入最短路径的起点: A

下标: 0     1     2     3     4     5     6
djst: 32767 4     5     6     10    9     16
path: -1    0     1     0     5     2     4

起点A到所有顶点的最短路径:
A-4->B  -总开销:4
A-4->B-1->C  -总开销:5
A-6->D  -总开销:6
A-4->B-1->C-4->F-1->E  -总开销:10
A-4->B-1->C-4->F  -总开销:9
A-4->B-1->C-4->F-1->E-6->G  -总开销:16

	*/

 

em ~ ~ ~ ~ ~ 

        在此,感谢您的倾心阅读!

        在茫茫人海中,你我能够相遇属实是一件难得的事!

        以上是本人的一些浅显理解,如有不妥之处,还望指出,咋们共同进步哟!

 

  • 12
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值