已知源点st,有向图G(无向图的每条边被视为两条有向边),求st到图中各个点的最短距离。
利用dis数组表示源点st到各个点的最短距离,初始时dis[st] = 0,其他为INF。
边的松弛:边a->b长度为c,如果dis[b]>dis[a] + c那么可以将dis[b]更新为dis[a] + c。
点的松弛:对以点v为起点的所有边进行边的松弛。
证明: 当每一条边都无法使用松弛操作更新dis数组时,dis数组达到最优。
其实所有的单源最短路算法都是以不同的顺序进行松弛操作,松弛操作的顺序也决定了算法的速度,如:Dijkstra算法的松弛顺序优于Bellman-Ford,也就是说Dijkstra算法可以以更少的松弛操作次数求得最优的dis数组。
需要证明:下列三种路径长度表达方式都能满足:“松弛的不能再松弛”的状态 与 dis数组达到最优互为充要条件
// 边起点为a 终点为b 权值为c
定义放松操作为:
if (dis[b] > dis[a] * c)
dis[b] = dis[a] * c
证明:当放松到无法放松时,dis数组最优
必要性:当dis数组最优时,如果还可以执行放松操作那么dis数组的值会变小,
这与dis数组最优矛盾。
// V为点V(i-1)->Vi的权值为Ci
// V1为源点
充分性:对于任意一条最短路:V1->V2->V3-> .... V(k-1)->Vk有:
dis[Vk] <= dis[V(k-1)] * Ck
dis[V(k-1)] <= dis[V(k-2)] * C(k-1)
:
:
dis[V3] <= dis[V2] * C3
dis[V2] <= dis[V1] * C2
将上述不等式相乘有:dis[Vk] <= Ck * C(k-1) * ... * C2 = 最优路径
已知权值路径为最短路所以:dis[Vk] >= 最优路径
所以dis[Vk] = 最优
得证。
最大值不等于最长路,最小值也不是最长路,这只是路径长度的一种表示方式,换句话说我们求得是某种路径长度表示下的最短路(例如:路径长度为权值最大值时,我们求得是让最大值最小)
// 边起点为a 终点为b 权值为c 不要求权值C > 0
定义放松操作为:
if (dis[b] > max(dis[a], c))
dis[b] = max(dis[a], c)
证明:当放松到无法放松时,dis数组最优
必要性:当dis数组最优时,如果还可以执行放松操作那么dis数组的值会变小,
这与dis数组最优矛盾。
// V1, V2, ..... , V(k-1), Vk为点
// V(i-1)->Vi的权值为Ci
// V1为源点
充分性:对于任意一条最短路:V1->V2->V3-> .... V(k-1)->Vk有:
dis[Vk] <= max(dis[V(k-1)], Ck)
dis[V(k-1)] <= max(dis[V(k-2)], C(k-1))
:
:
dis[V3] <= max(dis[V2], C3)
dis[V2] <= max(dis[V1], C2)
将上述不等式进行变量的替换有:
dis[Vk] <= max(Ck, C(k-1), ..... , C2) = 最优路径
已知权值路径为最短路所以:dis[Vk] >= 最优路径
所以dis[Vk] = 最优
得证。
同理不在证明
假设一个图G含有n个点,那么最短路所含有的边数最多为n - 1。
因为最短路不可能包含重复的点,所以最短路中的每条边都连接两个不同的点,所以最多包含n - 1条边,不然的话最短路就有大于n个的点了(除了一种情况那就是负环,存在负环的图的最短路是无限的,所以可以用Bellman-Ford算法在循环第n次dis数组还是发生了更新的时候说明存在负环)。
如果能在第i次循环找到包含i条边的最短路,那么只需要n - 1次循环就能找到所有的最短路(因为最短路所含有的路径数最长为n - 1)
// 定义边结构体
struct edge
{
int a;
int b;
int c;
};
// 初始化dis数组
memset(dis, 0x3f, sizeof dis);
dis[st] = 0;
// 不断松弛
// n为图的点数
// m为图的边数
for (int i = 0; i < n - 1; ++ i)
{
for (int j = 0; j < m; ++ j)
{
// a -> b
// 放松操作
int a = edges[j].a;
int b = edges[j].b;
int c = edges[j].c;
if (dis[b] > dis[a] + c)
dis[b] = dis[a] + c;
}
}
可以利用数学归纳法证明上述代码能在第i次循环找到包含i条边的最短路。
第一次循环能正确求出包含一条路径的最短路
假设第k次能求出所有包含k条路径的最短路
设包含k+1条路径的最短路V1->V2->V3-> … -> V(k+1) 那么V1->V2->V3-> … Vk 一定是最短路,并且已经在第k次循环中被求出来了,所以只要在利用Vk -> V(k+1) 这条边放松一下,这个包含k+1条路径的最短路就求得了。
在上述流程中可以发现大部分松弛是无意义的,比如在第一次循环中松弛能更新的只有和源点st相邻的点。更加一般的:在某一次循环中,只有dis[u]被更新过,那么以u为起点的边才可能进行有效松弛(如果上一次循环中没有更新dis[u],那么发现对以u为起点的边的放松操作在上一次循环和本次循环中是完全重复的),所以可以将更新的点u加入队列,不断更新。
code:
#include
#include
#include
#include
using namespace std;
const int N = 2510, INF = 0x3f3f3f3f;
struct edge
{
int to;
int val;
};
vector<edge> g[N]; // 邻接表
int dis[N]; // 出发点到各个点的距离
int n, m, st, ed; // 自左向右以此为:点个数,边个数,出发点,结束点
void spfa()
{
// 记录每个点是否在队列中,如果在需要加入点时发现队列中存在这个点那么无需加入。
bool vis[N] = {0};
queue<int> q;
q.push(st);
vis[st] = true;
while(q.size())
{
int u = q.front();
q.pop();
vis[u] = false;
int sz = g[u].size();
for (int i = 0; i < sz; ++ i)
{
edge temp = g[u][i];
if (dis[temp.to] > dis[u] + temp.val)
{
dis[temp.to] = dis[u] + temp.val;
if (!vis[temp.to])
q.push(temp.to);
}
}
}
}
int main(void)
{
cin >> n >> m >> st >> ed;
for (int i = 0; i < m; ++ i)
{
int a, b, c;
cin >> a >> b >> c; //边起点,边终点,边长度
g[a].push_back({b, c}); // 有向图只加入一个
g[b].push_back({a, c}); // 无向图加入两个
}
memset(dis, INF, sizeof dis);
dis[st] = 0;
// 队列优化的弗洛伊德算法:可以应对重边自环负边但无法应对负环
spfa();
// 应对出现负边的情况
if (dis[ed] <= INF/2) cout << dis[ed] << endl;
// 无法到达
else cout << -1 << endl;
return 0;
}
code:
#include
#include
using namespace std;
const int N = 100010, INF = 0x3f3f3f3f;
struct edge
{
int i, j, val;
};
edge e[N];
int n, m, k;
int dist1[550], dist2[550];
void bellman_ford()
{
memset(dist1, INF, sizeof dist1);
dist1[1] = 0;
for (int i = 0; i < k; ++ i)
{
memcpy(dist2, dist1, sizeof(dist1));
for (int j = 0; j < m; ++ j)
{
int a = e[j].i;
int b = e[j].j;
int val = e[j].val;
if (dist1[b] > dist2[a] + val) dist1[b] = dist2[a] + val;
}
}
}
int main(void)
{
cin >> n >> m >> k;
for (int i = 0; i < m; ++ i)
{
int x, y, z;
cin >> x >> y >> z;
e[i] = {x, y, z};
}
bellman_ford();
if (dist1[n] <= INF/2) cout << dist1[n];
else cout << "impossible";
return 0;
}
code:
如何完整的解决第三部分中不同路径长度表示方法下的最短路问题?(拓展到不同形式的最短路问题)
证明松弛到无法松弛时,dis数组最优
证明算法能松弛到无法松弛
关于SPFA算法的好处: 第二点特别好证明,因为只要满足:在某一次循环中,只有dis[u]被更新过,那么以u为起点的边才可能进行有效松弛,SPFA算法结束时一定会达到无法松弛的状态。对比Bellman-Ford算法固定的循环n-1次,SPFA算法在队列为空时结束,此时一定无法靠松弛操作更新dis数组。