最短路

文章目录

  • 【单源最短路】
    • 1.Dijkstra算法
      • 1) 算法思想
      • 2) 算法图解
      • 3) 模板
    • 2.Bellman-Ford算法
      • 1) 概述
      • 2) 算法思想
      • 3) 模板
    • 3.SPFA算法
      • 1) 算法思想
      • 2) 模板
      • 3) spfa判断图中是否存在负环
  • 【全源最短路】
    • 1.Floyd 算法
      • 1) 概述
      • 2) 算法思想
      • 3) 模版
  • 【例题】

【单源最短路】

单源最短路径问题 (Single Source Shortest Path, SSSP问题) 是说,给定一张有向图 G = ( V , E ) G=(V,E) G=(V,E) V V V 是点集, E E E 是边集, ∣ V ∣ = n |V|=n V=n ∣ E ∣ = m |E|=m E=m,节点以 [ 1 , n ] [1,n] [1,n] 之间的连续整数编号, ( x , y , z ) (x,y,z) (x,y,z) 描述一条从 x x x 出发,到达 y y y,长度为 z z z 的有向边。设 1 1 1 号点为起点,求长度为 n n n 的数组 d i s t dist dist,其中 d i s t [ i ] dist[i] dist[i] 表示从起点 1 1 1 到节点 i i i 的最短路径的长度。

1.Dijkstra算法

1) 算法思想

Dijkstra 算法(可读作迪杰斯特拉算法)只能应对所有边权都是非负数的情况,如果边权出现负数,那么 Dijkstra 算法很可能会出错,这时最好使用SPFA算法。

Dijkstra算法的流程如下:

  1. 初始化 d i s t [ 1 ] = 0 dist[1]=0 dist[1]=0,其余节点的 d i s t dist dist 值为正无穷大。
  2. 找出一个未被标记的、 d i s t [ x ] dist[x] dist[x] 最小的节点 x x x,然后标记节点 x x x
  3. 扫描节点 x x x 的所有出边 ( x , y , z ) (x,y,z) (x,y,z),若 d i s t [ y ] > d i s t [ x ] + z dist[y] > dist[x] +z dist[y]>dist[x]+z,则使用 d i s t [ x ] + z dist[x] + z dist[x]+z 更新 d i s t [ y ] dist[y] dist[y]
  4. 重复上述两个步骤,直到所有节点都被标记。

Dijkstra 算法基于贪心思想,它只适用于所有边的长度都是非负数的图。当边长 z z z 都是非负数时,全局最小值不可能再被其他节点更新,故在第 2 2 2 步中选出的节点 x x x 必然满足: d i s t [ x ] dist[x] dist[x] 已经是起点到 x x x 的最短路径。我们不断选择全局最小值进行标记和扩展,最终可得到起点 1 1 1 到每个节点的最短路径的长度。

2) 算法图解

将点分为两类,一类是已确定最短路径的点,称为:白点;一类是未确定最短路径的点,称为:蓝点。

求一个点的最短路径,就是把这个点由蓝点变为白点,从起点到蓝点的最短路径上的中转点在这个时刻只能是白点。

Dijkstra 算法的思想,就是一开始将起点到终点的距离标记为 0 0 0,而后进行 n n n 次循环,每次找出一个到起点距离 d i s [ u ] dis[u] dis[u] 最短的点 u u u ,将它从蓝点变为白点,随后枚举所有白点 V i V_i Vi,如果以此白点为中转到达蓝点 V i V_i Vi 的路径 d i s [ u ] + w [ u ] [ v i ] dis[u]+w[u][v_i] dis[u]+w[u][vi] 更短的话,这将它作为 V i V_i Vi 的更短路径(此时还不能确定是不是 V i V_i Vi 的最短路径)。

以此类推,每找到一个白点,就尝试用它修改其他所有蓝点,中转点先于终点变成白点,故每一个终点一定能被它的最后一个中转点所修改,从而求得最短路径。

以下图为例

最短路_第1张图片

算法开始时,作为起点的 d i s [ 1 ] = 0 dis[1]=0 dis[1]=0,其他的点 d i s [ i ] = 0 x 3 f 3 f 3 f 3 f dis[i]=0x3f3f3f3f dis[i]=0x3f3f3f3f

第一轮循环找到 d i s [ 1 ] dis[1] dis[1] 最小,将 1 1 1 变为白点,对所有蓝点进行修改,使得: d i s [ 2 ] = 2 , d i s [ 3 ] = 4 , d i s [ 4 ] = 7 dis[2]=2,dis[3]=4,dis[4]=7 dis[2]=2dis[3]=4dis[4]=7

最短路_第2张图片

此时, d i s [ 2 ] 、 d i s [ 3 ] 、 d i s [ 4 ] dis[2]、dis[3]、dis[4] dis[2]dis[3]dis[4] 被它的最后一个中转点 1 1 1 修改了最短路径。

第二轮循环找到 d i s [ 2 ] dis[2] dis[2] 最小,将 2 2 2 变成白点,对所有蓝点进行修改,使得: d i s [ 3 ] = 3 、 d i s [ 5 ] = 4 dis[3]=3、dis[5]=4 dis[3]=3dis[5]=4

最短路_第3张图片

此时, d i s [ 3 ] 、 d i s [ 5 ] dis[3]、dis[5] dis[3]dis[5] 被它的最后一个中转点 2 2 2 修改了最短路径。

第三轮循环找到 d i s [ 3 ] dis[3] dis[3] 最小,将 3 3 3 变成白点,对所有蓝点进行修改,使得: d i s [ 4 ] = 4 dis[4]=4 dis[4]=4

最短路_第4张图片

此时, d i s [ 4 ] dis[4] dis[4] 被它的最后一个中转点 3 3 3 修改了最短路径,但发现以 3 3 3 为中转不能修改 5 5 5,说明 3 3 3 不是 5 5 5 的最后一个中转点。

接下来两轮循环将 4 、 5 4、5 45 也变成白点。

最短路_第5张图片

N轮循环结束,所有点的最短路径均可求出。

最短路_第6张图片

3) 模板

对于无向图,可以把无向边看作两条方向相反的有向边,因此,只需重点关注有向图的最短路即可。

(1)朴素Dijkstra算法

  时间复杂度是 O ( n 2 ) O(n^2) O(n2) n n n 表示点数, m m m 表示边数

  适用于稠密图,用邻接矩阵存储。

  注意处理重边。

int g[N][N];  			// 存储每条边
int dist[N];  			// 存储1号点到每个点的最短距离
bool st[N];   			// 存储每个点的最短路是否已经确定

// 求1号点到n号点的最短路,如果不存在则返回-1
int dijkstra()
{
    memset(dist, 0x3f, sizeof dist);
    dist[1] = 0;

    for (int i = 0; i < n - 1; i ++ )
    {
        int t = -1;     // 在还未确定最短路的点中,寻找距离最小的点
        for (int j = 1; j <= n; j ++ )
            if (!st[j] && (t == -1 || dist[t] > dist[j]))
                t = j;

        // 用t更新其他点的距离
        for (int j = 1; j <= n; j ++ )
            dist[j] = min(dist[j], dist[t] + g[t][j]);

        st[t] = true;
    }

    if (dist[n] == 0x3f3f3f3f) return -1;
    return dist[n];
}

(2)堆优化版的 Dijkstra 算法

  外层循环的 O ( V ) O(V) O(V) 时间无法避免,但是寻找最小 d [ u ] d[u] d[u] 的过程使用堆优化来降低复杂度。

  时间复杂度 O ( m l o g n ) O(mlogn) O(mlogn) n n n 表示点数, m m m 表示边数

  适用于稀疏图,用邻接表存储。用邻接表存储就不需要对重边进行特殊处理了

typedef pair<int, int> PII;

int n;      							// 点的数量
int h[N], w[N], e[N], ne[N], idx;       // 邻接表存储所有边
int dist[N];        					// 存储所有点到1号点的距离
bool st[N];     						// 存储每个点的最短距离是否已确定

// 求1号点到n号点的最短距离,如果不存在,则返回-1
int dijkstra()
{
    memset(dist, 0x3f, sizeof dist);
    dist[1] = 0;
    priority_queue<PII, vector<PII>, greater<PII>> heap;
    heap.push({0, 1});      // first存储距离,second存储节点编号

    while (heap.size())
    {
        auto t = heap.top();
        heap.pop();

        int ver = t.second, distance = t.first;

        if (st[ver]) continue;
        st[ver] = true;

        for (int i = h[ver]; i != -1; i = ne[i])
        {
            int j = e[i];
            if (dist[j] > distance + w[i])
            {
                dist[j] = distance + w[i];
                heap.push({dist[j], j});
            }
        }
    }

    if (dist[n] == 0x3f3f3f3f) return -1;
    return dist[n];
}

(3)输出路径

  “以 u u u 为中介点可以使起点 s s s 到顶点 v v v 的最短距离 d [ v ] d[v] d[v] 更优”,隐含了这样一层意思:使 d [ v ] d[v] d[v] 变得更小的方案是让 u u u 作为从 s s s v v v 最短路径上 v v v 的前一个结点。

  不妨把这个前驱信息记录下来,于是,可以设置数组 p r e [   ] pre[\ ] pre[ ],令 p r e [ v ] pre[v] pre[v] 表示从起点 s s s 到顶点 v v v 的最短路径上 v v v 的前一个顶点的编号。

  以邻接矩阵为例:

int n, G[MAXV][MAXV];  					// n为顶点数,MAXV为最大顶点数
int d[MAXV]; 							// 起点到达各点的最短路径长度
int pre[MAXV]; 							// pre[v]表示从起点到顶点v的最短路径上v的前一个顶点
bool vis[MAXV] = {false}; 				// 标记数组, vis[i]==true表示已访问。初值均为false

void Dijkstra(int s) 					// s为起点
{
	fill(d, d + MAXV, INF); 			// fill函数将整个d数组赋为INF
	for(int i = 0; i < n; i++) 
		pre[i] = i; 					// 初始状态设每个点的前驱为自身(新添加)
	d[s] = 0; 							// 起点s到达自身的距离为0
	for(int i = 0; i < n; i++)			// 循环n次
	{
		int u = -1, MIN = INF;  		// u使d[u]最小,MIN存放该最小的d[u]
		for(int j = 0;j < n; j++) 		// 找到未访间的顶点中d[]最小的
		{
			if(vis[j] == false && d[j] < MIN)
			{
				u = j;
				MIN = d[j];
			}
		}

		if(u == -1)						// 找不到小于INF的d[u],说明剩下的顶点和起点s不连通
			return;
		
		vis[u] = true;					// 标记 u为已访问
		
		for(int v = 0; v < n; v++)
		{ 
			//如果v未访问 && u能到达v &&以u为中介点可以使d[v]更优
			if(vis[v] == false && G[u][v] != INF && d[u] + G[u][v] < d[v])
			{
				d[v] = d[u] + G[u][v]; 			// 优化d[v]
				pre[v] = u; 					// 记录v的前驱顶点是u
			}
		}
	}
}

// 然后用递归不断利用 pre[] 的信息寻找前驱,直至到达起点后从递归深处开始输出

void DFS(int s, int v)	 		// s为起点编号,v为当前访问的顶点编号(从终点开始递归)
{
	if(v == s)	 				// 如果当前已经到达起点s, 则输出起点并返回
	{ 
		printf("%d\n", s);
		return;
	}
	DFS(s, pre[v]); 			// 递归访问v的前驱顶点pre[v]
	printf("%d\n", v); 			// 从最深处return回来之后,输出每一层的顶点号
}

2.Bellman-Ford算法

1) 概述

Bellman-Ford 算法可解决单源最短路径问题,能处理有负权边的情况,但无法处理存在负权回路的情况。

根据环中边的边权之和的正负,可以将环分为零环、正环、负环。

最短路_第7张图片
  1. 显然,图中的零环和正环不会影响最短路径的求解,因为零环和正环的存在不能使最短路径更短;

  2. 而如果图中有负环,且从源点可以到达,那么就会影响最短路径的求解;但如果图中的负环无法从源点出发到达,则最短路径的求解不会受到影响。

Bellman-Ford算法的时间复杂度是 O ( V E ) O(VE) O(VE),其中 V V V 是顶点个数, E E E 是边数。

2) 算法思想

给定一张有向图, 若对于图中的某一条边 ( x , y , z ) (x,y,z) (x,y,z),有 d i s t [ y ] ≤ d i s t [ x ] + z dist[y] \leq dist[x]+z dist[y]dist[x]+z 成立,则称该边满足三角形不等式。若所有边都满足三角形不等式,则 d i s t dist dist 数组就是所求最短路。

基于迭代思想的Bellman-Ford算法,它的流程如下:

  1. 扫描所有边 ( x , y , z ) (x,y,z) (x,y,z),若 d i s t [ y ] > d i s t [ x ] + z dist[y]> dist[x] +z dist[y]>dist[x]+z,则用 d i s t [ x ] + z dist[x]+z dist[x]+z 更新 d i s t [ y ] dist[y] dist[y]
  2. 重复上述步骤,直到没有更新操作发生。

3) 模板

(1)邻接表

  由于 Bellman-Ford 算法需要遍历所有边,显然使用邻接表会比较方便;如果使用邻接矩阵,则时间复杂度会上升到 O ( V 3 ) O(V^3) O(V3)

struct Node {
	int v, dis;							// v为邻接边的目标顶点,dis为邻接边的边权
};

vector<Node> Adj[MAXV]; 				// 图G的邻接表
int n;  								// n为顶点数, MAXV为最大顶点数
int d[MAXV];  							// 起点到达各点的最短路径长度

bool Bellman(int s) 					// s为源点
{
	fill(d, d + MAXV, INF);  			// fill函数将整个d数组赋为INF (慎用memset)
	d[s] = 0; 							// 起点s到达自身的距离为0
	
	// 以下为求解数组d的部分
	for(int i = 0; i < n - 1; i++)				// 执行n-1轮操作,n为顶点数
	{
		for{int u = 0; u < n; u++)				// 每轮操作都遍历所有边
		{
			for(int j = 0; j < Adj[u].size(); j++)
			{
				int v = Adj[u][j].v;  			// 邻接边的顶点
				int dis = Adj[u][j].dis;		// 邻接边的边权
				if(d[u] + dis < d[v])			// 以u为中介点可以使d[v]更小
				{
					d[v] = d[u] + dis;			// 松弛操作
				}
			}
		}
	}

	// 以下为判断负环的代码
	for(int u = 0; u < n; u++)					// 对每条边进行判断
	{
		for(int j = 0; j < Adj[u].size(); j++)
		{
			int v = Adj[u][j].v;  				// 邻接边的顶点
			int dis = Adj[u][j].dis;  			// 邻接边的边权
			if(d[u] + dis < d[v])  				// 如果仍可以被松驰
			{
				return false;  					// 说明图中有从源点可达的负环
			}
		}
	}
	return true; 								// 数组d的所有值都已经达到最优
}

(2)不需要用邻接表存储,开一个结构体数组存储所有的边即可。

  时间复杂度 O ( n m ) O(nm) O(nm) n n n 表示点数, m m m 表示边数

int n, m;       	// n表示点数,m表示边数
int dist[N];        // dist[x]存储1到x的最短路距离

struct Edge     	// 边,a表示出点,b表示入点,w表示边的权重
{
    int a, b, w;
}edges[M];

// 求1到n的最短路距离,如果无法从1走到n,则返回-1。
int bellman_ford()
{
    memset(dist, 0x3f, sizeof dist);
    dist[1] = 0;

    // 如果第n次迭代仍然会松弛三角不等式,就说明存在一条长度是n+1的最短路径,
    // 由抽屉原理,路径中至少存在两个相同的点,说明图中存在负权回路。
    for (int i = 0; i < n; i ++ )
    {
        for (int j = 0; j < m; j ++ )
        {
            int a = edges[j].a, b = edges[j].b, w = edges[j].w;
            if (dist[b] > dist[a] + w)
                dist[b] = dist[a] + w;
        }
    }

    if (dist[n] > 0x3f3f3f3f / 2) return -1;
    return dist[n];
}

3.SPFA算法

1) 算法思想

SPFA (Shortest Path Faster Algrithm) 算法在国际上通称为“队列优化的Bellman-Ford算法”,仅在中国大陆地区流行“SPFA算法”的称谓。

基本原理:由 Bellman-Ford 算法中 d i s t [ b ] = m i n ( d i s t [ b ] , d i s t [ a ] + w [ i ] ) dist[b] = min(dist[b], dist[a]+w[i]) dist[b]=min(dist[b],dist[a]+w[i]) 可知,只有 a a a 变小了, b b b 才能变小,因此可以用一个队列存储变小了的节点。

SPFA 算法的流程如下:

  1. 建立一个队列,最初队列中只含有起点 1 1 1
  2. 取出队头节点 x x x,扫描它的所有出边 ( x , y , z ) (x,y,z) (x,y,z),若 d i s t [ y ] > d i s t [ x ] + z dist[y]> dist[x] +z dist[y]>dist[x]+z,则使用 d i s t [ x ] + z dist[x]+z dist[x]+z 更新 d i s t [ y ] dist[y] dist[y]。同时,若 y y y 不在队列中,则把 y y y 入队。
  3. 重复上述步骤,直到队列为空。

在任意时刻,该算法的队列都保存了待扩展的节点。每次入队相当于完成一次 d i s t dist dist 数组的更新操作,使其满足三角形不等式。一个节点可能会入队、出队多次。最终,图中节点收敛到全部满足三角形不等式的状态。

这个队列避免了 Bellman-Ford 算法中对不需要扩展的节点的冗余扫描,在稀疏图上运行效率较高,为 O ( k m ) O(km) O(km)级别,其中 k k k 是一个较小的常数。但在稠密图或特殊构造的网格图上,该算法仍可能退化为 O ( n m ) O(nm) O(nm)

2) 模板

(1)邻接表

// 如果事先知道图中不会有环,那么 num 数组的部分可以去掉

vector<Node> Adj[MAXV];						// 图G的邻接表 
int n, d[MAXV], num[MAXV];  				// num数组记录顶点的入队次数
bool inq[MAXV];								// 顶点是否在队列中 

bool SPFA(int s)
{
	memset(inq, false, sizeof(inq));		// 初始化部分
	memset(num, 0, sizeof(num));
	fill(d, d + MAXV, INF);
	
	// 源点入队部分
	queue<int> Q;
	Q.push(s);								// 源点入队
	inq[s] = true;							// 源点已入队 
	num[s]++;  								// 源点入队次数加1
	d[s] = 0;  								// 源点的d值为0
	
	// 主体部分
	while(!Q.empty())
	{
		int u = Q.front();					// 队首顶点编号为u 
		Q.pop();  							// 出队
		inq[u] = false;						// 设置u为不在队列中 
		
		// 遍历u的所有邻接边v
		for(int j = 0; j < Adj[u].size(); j++)
		{
			int v = Adj[u][j].v;
			int dis = Adj[u][j].dis;
			if(d[u] + dis < d[v])			// 松弛操作
			{
				d[v] = d[u] + dis;
				if(!inq[v])  				// 如果v不在队列中
				{
					Q.push(v); 				// v入队
					inq[v] = true;  		// 设置v为在队列中
					num[v]++;  				// v的入队次数加1
					if(num[v] >= n) return false;  	// 有可达负环
				}
			}
		}
	}
	return true;  									//无可达负环
}

(2)数组模拟邻接表

int n;      							// 总点数
int h[N], w[N], e[N], ne[N], idx;       // 邻接表存储所有边
int dist[N];        					// 存储每个点到1号点的最短距离
bool st[N];     						// 存储每个点是否在队列中

// 求1号点到n号点的最短路距离,如果从1号点无法走到n号点则返回-1
int spfa()
{
    memset(dist, 0x3f, sizeof dist);
    dist[1] = 0;

    queue<int> q;
    q.push(1);
    st[1] = true;

    while (q.size())
    {
        auto t = q.front();
        q.pop();

        st[t] = false;

        for (int i = h[t]; i != -1; i = ne[i])
        {
            int j = e[i];
            if (dist[j] > dist[t] + w[i])
            {
                dist[j] = dist[t] + w[i];
                if (!st[j])     		// 如果队列中已存在j,则不需要将j重复插入
                {
                    q.push(j);
                    st[j] = true;
                }
            }
        }
    }

    if (dist[n] == 0x3f3f3f3f) return -1;
    return dist[n];
}

3) spfa判断图中是否存在负环

Bellman-Ford也可以判负环,但时间复杂度有点高,常用spfa判负环。

时间复杂度是 O ( n m ) O(nm) O(nm) n n n 表示点数, m m m 表示边数

基本原理:如果某条最短路径上有 n n n 个点(除了自己),那么加上自己之后一共有 n + 1 n+1 n+1 个点,由抽屉原理一定有两个点相同,所以存在环。

注意:判断图中是否存在负权回路,并不是判断是否存在从 1 1 1 开始的负环,所以,初始时我们要把所有的点都放到队列里。

int n;      							// 总点数
int h[N], w[N], e[N], ne[N], idx;       // 邻接表存储所有边
bool st[N];     						// 存储每个点是否在队列中
int dist[N], cnt[N];
// dist[x]存储1号点到x的最短距离,cnt[x]存储1到x的最短路中经过的点数

// 如果存在负环,则返回true,否则返回false
bool spfa()
{
    // 不需要初始化dist数组
    
    queue<int> q;
    for (int i = 1; i <= n; i ++ )
    {
        q.push(i);
        st[i] = true;
    }

    while (q.size())
    {
        auto t = q.front();
        q.pop();

        st[t] = false;

        for (int i = h[t]; i != -1; i = ne[i])
        {
            int j = e[i];
            if (dist[j] > dist[t] + w[i])
            {
                dist[j] = dist[t] + w[i];
                cnt[j] = cnt[t] + 1;
                // 如果从1号点到x的最短路中包含至少n个点(不包括自己),则说明存在环
                if (cnt[j] >= n) return true;
                
                if (!st[j])		// 如果队列中已存在j,则不需要将j重复插入
                {
                    q.push(j);
                    st[j] = true;
                }
            }
        }
    }

    return false;
}

【全源最短路】

1.Floyd 算法

1) 概述

Floyd 算法 (可读作“弗洛伊德算法”),用来解决全源最短路问题,即对给定的图 G ( V , E ) G(V, E) G(V,E),求任意两点 u , v u, v u,v 之间的最短路径长度。

为了求出图中任意两点间的最短路径,当然可以把每个点作为起点,求解 N N N 次单源最短路径问题。不过,在任意两点间最短路问题中,图一般比较稠密。使用 Floyd 算法可以在 O ( N 3 ) O(N^3) O(N3) 的时间内完成求解,并且程序实现非常简单。

由于 n 3 n^3 n3 的复杂度决定了顶点数 n n n 的限制约在 200 200 200 以内,因此使用邻接矩阵来实现 Floyd 算法是非常合适且方便的。

2) 算法思想

D [ k , i , j ] D[k,i,j] D[k,i,j] 表示“经过若干个编号不超过 k k k 的节点”从 i i i j j j 的最短路长度。

该问题可划分为两个子问题,经过编号不超过 k − 1 k-1 k1 的节点从 i i i j j j,或者从 i i i 先到 k k k 再到 j j j。于是:
D [ k , i , j ] = m i n ( D [ k − 1 , i , j ] ,   D [ k − 1 , i , k ] + D [ k − 1 , k , j ] ) D[k,i,j] = min(D[k- 1,i,j],\ D[k- 1,i,k] + D[k- 1,k,j]) D[k,i,j]=min(D[k1,i,j], D[k1,i,k]+D[k1,k,j])
初值为 D [ 0 , i , j ] = A [ i , j ] D[0,i,j]= A[i,j] D[0,i,j]=A[i,j],其中 A A A 为邻接矩阵。

可以看到,Floyd 算法的本质是动态规划。 k k k 是阶段,所以必须置于最外层循环中 i i i j j j 是附加状态,所以应该置于内层循环。

与背包问题的状态转移方程类似, k k k 这一维可被省略。最初,我们可以直接用 D D D 保存邻接矩阵,然后执行动态规划的过程。当最外层循环到 k k k 时,内层有状态转移:

D [ i , j ] = m i n ( D [ i , j ] ,   D [ i , k ] + D [ k , j ] ) D[i,j] = min(D[i,j],\ D[i,k] + D[k,j]) D[i,j]=min(D[i,j], D[i,k]+D[k,j])

最终 D [ i , j ] D[i,j] D[i,j] 就保存了 i i i j j j 的最短路长度。

3) 模版

(1)求最短路

  时间复杂度是 O ( n 3 ) O(n^3) O(n3) n n n 表示点数

  注意的是:不能将最外层的 k k k 循坏放到内层,这会导致最后结果出错。

// 初始化
    for (int i = 1; i <= n; i ++ )
        for (int j = 1; j <= n; j ++ )
            if (i == j) d[i][j] = 0;
            else d[i][j] = INF;

// 算法结束后,d[a][b]表示a到b的最短距离
void floyd()
{
    for (int k = 1; k <= n; k ++ )
        for (int i = 1; i <= n; i ++ )
            for (int j = 1; j <= n; j ++ )
                d[i][j] = min(d[i][j], d[i][k] + d[k][j]);
}

(2)求字典序最小的最短路径

int G[N][N];
int path[N][N];		// path[i][j]=x表示i到j的路径上除i外的第一个点是x

void init(int n)
{
    for(int i = 1; i <= n; i++)
	{
        for(int j = 1; j <= n; j++)
		{
            if(i == j)
                G[i][j] = 0;
            else
                G[i][j] = INF;
            path[i][j] = j;
        }
    }
}

void floyd(int n)
{
    for(int k = 1; k <= n; k++)			 					// 枚举中间点
	{
        for(int i = 1; i <= n; i++)							// 枚举起点
		{  
            for(int j = 1; j <= n; j++)						// 枚举终点
			{ 
                if(G[i][k] < INF && G[k][j] < INF)
				{
                    if(G[i][j] > G[i][k] + G[k][j])			// 松弛操作
					{ 
                        G[i][j] = G[i][k] + G[k][j];
                        path[i][j] = path[i][k]; 			// 更新路径
                    }
                    // 在最短路相同的情况下,更新字典序最小的路径
                    else if (G[i][j] == G[i][k] + G[k][j] && path[i][j] > path[i][k])
					{
                        path[i][j] = path[i][k]; 		// 最终path中存的是字典序最小的路径
                    }
                }
            }
        }
    }
}

int main()
{
    int n, m;
    while(scanf("%d%d", &n, &m) != EOF)
	{
        init(n);
        for(int i = 1; i <= m; i++)
		{
            int x, y, dis;
            scanf("%d%d%d", &x, &y, &dis);
            // 无向图添边一次,有向图添边两次
            G[x][y] = dis;
            G[y][x] = dis;
        }
 
        floyd(n);
 
        for(int i = 1; i <= n; i++)
		{
            for(int j = 1; j <= n; j++)
                printf("%d ", G[i][j]);
            printf("\n");
        }
    }
    return 0;
}

【例题】

  • AcWing 849. 朴素Dijkstra算法(模板):点击这里
  • AcWing 850. 堆优化版的Dijkstra算法(模板):点击这里
  • PTA A1003 Emergency(新增点权 + 求最短路径条数):点击这里
  • PTA A1030 Travel Plan(新增边权 + 输出路径):点击这里
  • AcWing 853. 有边数限制的最短路(Bellman-Ford模板):点击这里
  • AcWing 851. spfa求最短路(模板):点击这里
  • AcWing 852. spfa判断负环 (模板):点击这里
  • AcWing 854. Floyd求最短路(模板):点击这里
  • POJ 2387 Til the Cows Come Home(最短路裸题):点击这里
  • POJ 1797 Heavy Transportation(最小边权最大化):点击这里
  • POJ 2253 Frogger(最大边权最小化):点击这里
  • POJ 3268 Silver Cow Party(两次Dijkstra):点击这里

你可能感兴趣的:(最短路)