图说之最短路径

Dijkstra的理解

Dijkstra算法是最短路径算法之一
下面这道题和代码基本来自于《挑战程序设计》这本书,其算法核心来说我个人认为就是从起点出发遍历相邻的的点并对比其距离,选取最短距离加入数组dis【i】,在前一步基础上重复直到到达目标点,看到这种遍历方式你也许跟我最初想的差不多嘿嘿———那就是BFS,提到这个我把它留到spfa再讲。

下面这是一道例题

定加权有向图 G = (V,E) 的单源最短路径的成本。请以 G的顶点 0 为起点,输出 0 到各顶点 V 的最短路径上各边权值的总和 d[v]
输入:第一行输入G的顶点数n,下面n行输入其领接表
u k v1 c1 v2 c2 …
(G各顶点编号1-n,u为顶点编号,k为出度数,vi代表与u相邻的编号ci代表u到vi的权值)
输出:按顺序输出各顶点编号 v 及距离 d[v], 相邻数据间用 1 个空格隔开。*

#include<iostream>
#include<cstdio>
using namespace std;
#define INF 0x3f3f3f3f//设为最大值
#define MAX 100
int n, M[MAX][MAX];
void dijkstra()
{
  int v,w;
  int minv;
  int dis[MAX],vis[MAX];//前者dis【i】是起始点(0)到各个顶点的最短距离,后者为访问路径记录0为未访问,1为已访问
  for (v = 0; v < n; v++)//初始化数组dis,vis
  {
	  dis[v] = INF;
	  vis[v] = 0;
  }
  dis[0] = 0;//由于起点为0所以0到0即为0;
  while (1) {
	  minv = INF;//设定比较初始值
	  int u = -1;
	  for (int i = 0; i < n; i++)//寻找离其实点最近的距离
	  {
		  if (vis[i] != 1&&dis[i] < minv)
		  {
			  u = i;
			  minv = dis[i];//u离起点进;
		  }
	  }
	  if (u == -1)break;//当所有路径遍历完毕
	  vis[u] = 1//该路径已经确定
	  for (v = 0; v < n; v++)//更新当前最短路径及距离
	  {
		  if (vis[v] != 1 && M[u][v] != INF) {//如果经过v顶点的路径比现在这条路径短的话
			  if (dis[v] > M[u][v] + dis[u]) {
				  dis[v] = M[u][v] + dis[u];//找到并且修改
			  }
		  }
	  }
  }
  for (int i = 0; i < n; i++) {
	  cout << i << " " << (dis[i] == INF ? -1 : dis[i]) << endl;
  }

}
int main() {
	int k, c, v, u;
	cin >> n;
	for (int i = 0; i < n; i++)//初始化
		for (int j = 0; j < n; j++)
			M[i][j] = INF;
	for (int i = 0; i < n; i++) //将邻接表转化为邻接矩阵
	{
		cin >> u >> k;
		for (int j = 0; j < k; j++) {
			cin >> v >> c;
			M[u][v] = c;
		}
	}
	dijkstra();//进行迪杰斯特拉算法
	return 0;

}

这道题是通过输入邻接表转化成邻接矩阵来进行dijkstra算法,这种算法可以将解决加权图的一部分,那就是负权图无法解决,这是为什么呢?
来看这段代码。

 for (int i = 0; i < n; i++)//寻找离其实点最近的距离
	  {
		  if (vis[i] != 1&&dis[i] < minv)
		  {
			  u = i;
			  minv = dis[i];//u离起点进;
		  }
	  }

在这里插入图片描述
上图一位大佬绘制的。
用Dijkstra算法,第一步就能得到所谓的最短路径长度4,不会去考虑边( C,B)。而实际上最短路径却需要加入边(C, B),长度是2。
摘自:link.
也就是说最开始查找就使得只与相邻边开始搜素,若有负权边与之相邻边的相邻边就不会遍历了。
所以若是负权图就得使用弗洛伊德算法了。

Floyd的理解

为了对比两者的差异我们还是给出问题

列举出加权有向图G=<V,E>中每两点之间的最短路径长度
输出:V,E
s0 t0 d0
s1 t1 d1

V,E代表图G的顶点数和边数,图各顶点编号0,1,2,3…v-1。Si,Ti,Di分别表示图的第i条边连接的2个定点编号以及权值
输出
如 果 图 G 包含负环( 各边权值总和为负数的环 ),则在 1 行中输出如下内容。
NEGATIVE CYCLE
否则按照其矩阵形式输出最短路径
总 共 占 m 行。在第 / 行中按顺序输出顶点 / 到各顶点y 之间最短路径的长度。/ 到./ 之间不存在路径时输岀 INF。相邻数值之间用 1 个空格隔开。

代码如下:

#include<iostream>
#include<cstdio>
#include<algorithm>
using namespace std;
#define INF 0x3f3f3f3f//设为最大值
#define MAX 100
int n, d[MAX][MAX];
void floyd()
{
	for (int k = 0; k < n; k++) {//k为其中转站
		for (int i = 0; i < n; i++) {
			if (d[i][k] == INF)continue;
			for (int j = 0; j < n; j++) {
				if (d[k][j] == INF)continue;
				d[i][j] = min(d[i][j], d[i][k] + d[k][j]);//如果经过k后比原来两点之间距离更短则替换
			}
		}
	}

}
int main() {
	int e, u, v, c;
	cin >> n >> e;
	for (int i = 0; i < n; i++) {
		for (int j = 0; j < n; j++) {
			d[i][j] = ((i == j) ? 0 : INF);//初始化
		}
	}
	for (int i = 0; i < e; i++) {
		cin >> u >> v >> c;
		d[u][v] = c;
	}
	floyd();//进行弗洛伊德算法
	return 0;

}

输出代码我就不做详细写了,主要是分析弗洛伊德算法关键在于三重循环

for (int k = 0; k < n; k++) {//k为其中转站
		for (int i = 0; i < n; i++) {
			if (d[i][k] == INF)continue;
			for (int j = 0; j < n; j++) {
				if (d[k][j] == INF)continue;
				d[i][j] = min(d[i][j], d[i][k] + d[k][j]);//如果经过k后比原来两点之间距离更短则替换
			}
		}

这三重循环是不是非常简洁只可惜时间复杂度提升到了O(n^3)。但他完美避开了dijikstra在负权图中跳过的边。而我下面要讲的spfa就是在floyd算法基础上的改进。

SPFA

我为什么要写这个算法呢?说实话我第一次见到这个算法是因为acm的题就是我后面讲到的poj1860。这个算法我真的看了很久,我个人认为是floyd+队列实现的算法综合,他以floyd的遍历方法边来遍历还在此程度上利用优先队列优化了算法复杂度。

下面在程序中录入以下图,求单元路径的最短路径。
在这里插入图片描述
其中录入图是以边集录入,其中的 next 这一点如果大家感觉有不懂你可以结合 floyd中的采用所谓中间节点遍历(以途径点)

#include<cstdio>
#include<iostream>
#include<cstring>
#include<algorithm>
#include<queue>
using namespace std;
typedef long long ll;
#define INF 0x3f3f3f3f
#define maxn 1111
int tol;
struct edge {
	int next,to;
	int cost;
}g[maxn];//定义边
int dis[maxn];//所有点到起点的最短距离
int head[maxn];
int cnt[maxn];//统计每个点的入队次数
void add(int u, int v, int cost)//录入边
{
	g[tol].cost = cost;
	g[tol].to = v;
	g[tol].next = head[u];
	head[u] = tol++;
}
void init()
{
	memset(head, -1, sizeof(head));
	 tol = 0;
}
int  spfa(int s,int n)
{
	bool vis[maxn];
    memset(vis, 0, sizeof(vis));
    memset(cnt, 0, sizeof(cnt));
	queue<int>que;
	for (int i=0; i < n; i++) dis[i] = INF;
	cnt[s]++;
	vis[s]=1;
	dis[s]=0;
	que.push(s);
	while (!que.empty())
	{
		int u = que.front();
		que.pop();
		vis[u] = 0;
		for (int i = head[u]; i != -1; i = g[i].next) {
			int v = g[i].to;
			int cost = g[i].cost;
			if (dis[v] > (dis[u]+cost))
			{
				dis[v]= dis[u] + cost;
					if (!vis[v])
					{
						vis[v]=1;
						que.push(v);
						cnt[v]++;
				}
			}
         }
	}
	return 0;
}

int main()
{
	int n, m, s;
	double v;
	init();
	for (int i = 0; i < 5; i++)
	{
		int u, v;
		int cost;
		scanf("%d%d%d", &u, &v, &cost);
		add(u,v,cost);
	}
	spfa(1, 5);
	for (int i = 0; i < 5; i++) {
		cout << i << " " << (dis[i] == INF ? -1 : dis[i]) << endl;
	}
	return 0;

}

其中以上图为例遍历顺序为1->2->3在从2->1继续3->2,3->1;他的更新dis【i】最短路径的原理大致和前几种方式方法一样。

第一次写博客还请大家批评指正。下次见!!!!!

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值