最短路径问题:任给一个简单带权图 G=及 u,v属于V,求 u,v之间的最短路径及距离。

下面介绍最短路径问题的一个有效算法,它是 E. W. Dijkstra 于 1959 年给出的。Dijkstra算法适用于所有边的权大于等于 0 的情况,它可以求从给定的一个顶点到其余所有顶点的最短路径及距离。设G=,V={v1,v2,…,vn},求从 v1 到其余各顶点的最短路径和距离。Dijkstra 算法是一种标号法,每一个顶点有一个标号,标号分永久标号和临时标号两种。在顶点 vi的标号记作(li,pi).若 vi在第t 步获得永久标号,则此后它的标号不再改变,其中 li是v1到vi的距离,pi是v1到 vi的最短路径上vi的前一个顶点,若是临时标号,则 li 是v1经过具有永久标号的顶点到vi的最短长度,pi是这条路径上vi
的前一个顶点。记 P 为已获得永久标号的顶点集,T=V-P 为仍为临时标号的顶点集。

算法描述:

(1)初始时,P只包含起点v1;T包含除v1外的其他顶点,且T中顶点的距离li为起点v1到该顶点的距离

(2)从T中选出距离起点v1最短的顶点k,并将顶点k加入到P中;同时,从T中移除顶点k。

(3) 更新T中各个顶点到起点v1的距离。之所以更新T中各顶点的距离,是由于上一步中确定了k是求出最短路径的顶点,从而可以利用k作为跳板来更新其它顶点的距离。例如,T中有一点s和k相邻,s到起点v1的距离可能大于k到v1的距离+k到s的距离

(4)重复步骤(2)和(3),直到遍历完所有顶点。

下面给出洛谷上的题目链接:

P3371 【模板】单源最短路径(弱化版) - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

P4779 【模板】单源最短路径(标准版) - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

题目描述:
给定一个 n 个点,m 条有向边的带非负权图,请你计算从 s 出发,到每个点的距离。数据保证你能从 s 出发到任意点。

输入格式:
第一行为三个正整数 n, m, s。 第二行起 m 行,每行三个非负整数 u_i, v_i, w_i,表示从 u_i到 v_i有一条权值为 w_i的有向边。

输出格式:
输出一行 n 个空格分隔的非负整数,表示 s 到每个点的距离。

输入输出样例:
输入
4 6 1
1 2 2
2 3 2
2 4 1
1 3 5
3 4 3
1 4 4
输出
0 2 4 3

邻接矩阵

使用邻接矩阵来存储整个图,然后每次枚举对应的行或列来找到一个距离最近的。代码也比较简单,这里并不过多描述。

C++实现:

#include <iostream>
#include <cstring>
//注:邻接矩阵效率太低,其实无法通过此题,只是为了帮助理解离散数学

using namespace std;

const int maxn = 10000;
int graph[maxn][maxn]; //有向图的邻接矩阵
int dis[maxn]; //记录某个点到起点的最短距离
int path[maxn]; //记录前驱节点
int vis[maxn]; //记录某个点是否被更新过
int n, m, s; //点数

void dijkstra(int s)
{
memset(path, -1, sizeof(path));
memset(dis, 0x3f, sizeof(dis)); //初始化为无穷大
/*INF使用0x3f3f3f3f的好处:
* 1:满足无穷大加一个有穷的数依然是无穷大(在DijKstra算法松弛操作中避免了溢出而出现负数)
* 2:满足无穷大加无穷大依然是无穷大(两个0x3f3f3f3f相加结果为0x7e7e7e7e,并未溢出)
* 3:初始化时,由于每一个字节为0x3f,所以只需要memset(buf,0x3f,sizeof(buf))即可(memset是以字节为单位初始化的)
*/
memset(vis, 0, sizeof(vis)); //初始化所有节点都没有永久标签
dis[s] = 0; //起点自己到自己的距离为0
while (1)
{
int point = 0;
for (int i = 1; i <= n; i++) //寻找临时标签中到起点距离最小的点
{
if (!vis[i] && dis[i] < dis[point])
point = i;
}
if (!point) //如果没找到,则返回
return;
vis[point] = 1; //把找到的这个点更新为永久标签
for (int i = 1; i <= n; i++)
{
//如果原最短距离比现距离大(现距离即为刚刚找到的点到起点的最短距离+该点到邻接点的距离),则更新
//如果某点无法通过该永久标签点到达,则加上的是一个无穷大,则仍为原距离
if (dis[i] > dis[point] + graph[point][i])
{
dis[i] = dis[point] + graph[point][i]; //更新最短距离
path[i] = point; //路径被改变,重新记录前驱
}
}
}
}

void print(int x) //输出路径,x为终点
{
if (x == -1)
return;
print(path[x]); //小递归
if (x != s)
cout << "->";
cout << x;
}

int main()
{
cin >> n >> m >> s;
memset(graph, 0x3f, sizeof(graph));
for (int i = 0; i < m; i++)
{
int u, v, w;
cin >> u >> v >> w;
graph[u][v] = w;
}
dijkstra(s);
for (int i = 1; i <= n; i++)
cout << dis[i] << " ";
cout << endl;
print(4);
return 0;
}

Python实现:

n, m, s = map(int, input().split())
dis = [float("inf")]*(n+1) # 记录某个点到起点的最短距离,初始化为无穷大
path = [-1]*(n+1) # 记录前驱节点,初始化为-1
vis = [False]*(n+1) # 记录某个点是否被更新过,初始都未被更新


def printPath(x):
# 输出路径,x为终点
if x == -1:
return
printPath(path[x]) # 小递归
if x != s:
print("->", end="")
print(x, end="")


# 构造邻接矩阵
graph = [[float("inf") for i in range(n+1)] for j in range(n+1)]
for i in range(m):
src, des, w = map(int, input().split())
graph[src][des] = w

# 起点自己到自己的距离为0
dis[s] = 0

while True:
point = 0

# 寻找临时标签中到起点距离最小的点
for i in range(1, n+1):
if (not vis[i]) and dis[i] < dis[point]:
point = i

# 如果没找到,则退出循环
if point == 0:
break

# 把找到的这个点更新为永久标签
vis[point] = True

# 如果原最短距离比现距离大(现距离即为刚刚找到的点到起点的最短距离+该点到邻接点的距离),则更新
# 如果某点无法通过该永久标签点到达,则加上的是一个无穷大,则仍为原距离
for i in range(1, n+1):
if dis[i] > dis[point]+graph[point][i]:
dis[i] = dis[point]+graph[point][i]
path[i] = point

print(dis[1:])
printPath(4)

堆优化+链式前向星

堆优化的主要思想就是使用一个优先队列(就是每次弹出的元素一定是整个队列中最小的元素)来代替最近距离的查找,用邻接表代替邻接矩阵,这样可以大幅度节约时间开销。

在这里有几个细节需要处理:

(1)首先来讲,优先队列的数据类型应该是怎样的呢?

​ 我们知道优先队列应该用于快速寻找距离最近的点。由于优先队列只是将最小的那个元素排在前面,因此我们应该定义一种数据类型,使得它包含该节点的编号以及该节点当前与起点的距离。

(2)我们应该在什么时候对队列进行操作呢?
队列操作的地方,首先就是搜索刚开始,要为起点赋初始值,此时必须将起点加入优先队列中。该队列元素的节点编号为起点的编号,该节点当前与起点的距离为0。

(3)那么如果一个节点到起点的最短距离通过其他的运算流程发生了变化,那么如何处理队列中的那个已经存入的元素?
事实上,你不需要理会队列中的元素,而是再存入一个就行了。因为如果要发生变化,只能将节点与起点之间的距离变得更小,而优先队列恰好是先让最小的那个弹出。

因此,轮到某一个队列元素弹出的时候,如果有多个元素的节点编号相同,那么被弹出的一定是节点编号最小的一个。等到后面再遇到这个节点编号的时候,我们只需要将它忽略掉就行了。

C++实现:

#include <iostream>
#include <cstring>
#include <queue>

using namespace std;

const int maxn = 10001;
const int maxm = 500001;
int n, m, s;
int vis[maxn], dis[maxn], path[maxn], head[maxn];
//vis记录某个点是否被更新过,dis记录某个点到起点的最短距离,path记录前驱节点,head[i]记录以第i个点为起点的所有边所构成的链表的首下标

struct Edge
{
//u,v,w,next:起点,终点,权值,下一条边的序号
int u, v, w, next;
} edge[maxm];

struct Point
{
int minDis, now; //该点到起点的最小距离和该点的序号
//重载运算符把最小的元素放在堆顶
bool operator<(const Point &x) const
{
return minDis > x.minDis;
}
/*
1.参数里面那个const是为了不对原来的对象修改,另外这里用引用避免了对实参的拷贝,提高效率
2.函数加上const后缀表示此函数不修改类成员变量,如果在函数里修改了则编译报错
*/
};

priority_queue<Point> q;

void dijkstra(int s)
{
memset(path, -1, sizeof(path));
memset(dis, 0x3f, sizeof(dis)); //初始化为无穷大
memset(vis, 0, sizeof(vis)); //初始化所有节点都没有永久标签
dis[s] = 0; //起点自己到自己的距离为0
Point p = {0, s}; //初始化列表生成结构体
q.push(p); //将起点加入队列,可简写成q.push((Point){0,s});
while (!q.empty()) //堆为空即为所有点都更新
{
Point x = q.top(); //取出首节点
q.pop();
int u = x.now;
if (vis[u])
continue; //如果具有永久标签就下一个
vis[u] = 1; //设置永久标签
for (int i = head[u]; i; i = edge[i].next) //搜索以堆顶节点为起点的所有连边
{
int v = edge[i].v; //获取终点
if (dis[v] > dis[u] + edge[i].w)
{
dis[v] = dis[u] + edge[i].w;
path[v] = u;
q.push((Point){dis[v], v}); //把新遍历到的点加入堆中
}
}
}
}

void print(int x) //输出路径,x为终点
{
if (x == -1)
return;
print(path[x]); //小递归
if (x != s)
cout << "->";
cout << x;
}

int main()
{
cin >> n >> m >> s;
memset(head, 0, sizeof(head));
for (int i = 1; i <= m; i++)
{
int u, v, w;
cin >> u >> v >> w;
edge[i].u = u;
edge[i].v = v;
edge[i].w = w;
edge[i].next = head[u]; //将该边指向下一条边,以某个点为起点的第一个输入的边的下一条边为0,即该边为链表的末尾
head[u] = i; //更新目前该点的最后一条边,即目前链表的头
//以上两行代码是为了将同一起点的边构造成一个链表,即链式前向星
}
dijkstra(s);
for (int i = 1; i <= n; i++)
{
cout << dis[i] << " ";
}
cout << endl;
print(4);
return 0;
}

Python实现:

from heapq import *

n, m, s = map(int, input().split())
dis = [float("inf")]*(n+1) # 记录某个点到起点的最短距离,初始化为无穷大
path = [-1]*(n+1) # 记录前驱节点,初始化为-1
vis = [False]*(n+1) # 记录某个点是否被更新过,初始都未被更新
myheap = [] # 堆


class Point():
# index:该点的标号,dis:该点到起点的最短距离
def __init__(self, index, dis):
self.index = index
self.dis = dis

def __lt__(self, other):
if self.dis < other.dis:
return True
else:
return False


def printPath(x):
# 输出路径,x为终点
if x == -1:
return
printPath(path[x]) # 小递归
if x != s:
print("->", end="")
print(x, end="")


# 链式前向星,用C++实现起来较为复杂,Python的字典可以轻松实现这一功能
# 以每条边的起点为键,以同起点的所有边的终点和权所组成的字典为值,建立链表
graph = {i: {} for i in range(1, n+1)}
for i in range(m):
src, des, w = map(eval, input().split())
graph.get(src)[des] = w

# 起点自己到自己的距离为0
dis[s] = 0
# 将起点加入堆
heappush(myheap, Point(s, 0))

while len(myheap) > 0:
# 取出首节点
x = heappop(myheap)
index = x.index
if vis[index]:
continue
vis[index] = True
# 搜索以堆顶节点为起点的所有连边
for des, w in graph.get(index).items():
if dis[des] > dis[index]+w:
dis[des] = dis[index]+w
path[des] = index
# 把新遍历到的点加入堆中
heappush(myheap, Point(des, dis[des]))

print(dis[1:])
printPath(4)