用五种最短路算法———(南昌理工ACM集训队)
Dijkstra 算法
朴素版
Dijkstra 算法只适用无负权值的单源点最短路。这个算法跟最小生成树的 Prim算法 类似。
首先找到一个没有确定最短路且距离起点最近的点 t,并通过这个点将其他点的最短距离进行更新。每做一次这个步骤,都能确定一个点的最短路,所以需要重复此步骤 n 次,找出 n 个点的最短路。
dist[j] = min(dist[j] , dist[t] + g[i][j] ) 这样更新 dist。
int Dijkstra()
{
memset(dist, 0x3f,sizeof dist); //初始化距离
dist[1]=0; //第一个点到自身的距离为0
for(int i=0;i<n;i++) //有n个点所以要进行n次 迭代
{
int t=-1; //t存储当前访问的点
for(int j=1;j<=n;j++) //这里的j代表的是从1号点开始,,,
if(!st[j]&&(t==-1||dist[t]>dist[j])) //未被标记,即没有确定最短路,或t不是最短距离
t=j; // t 相当于 i ,找出并标记 t
st[t]=true;
for(int j=1;j<=n;j++) //依次更新每个点所到起点的路径值 ,, 找出该点的 最小出边
dist[j]=min(dist[j],dist[t]+g[t][j]);
}
if(dist[n]==0x3f3f3f3f) return -1; // 不相通
return dist[n];
}
例题:Acwing ——849
堆优化版
朴素Dijkstra算法的时间复杂度时O(n^2)的,当 n 过大时就选用堆优化后的Dijkstra, 它的时间复杂度是O(mlongn)。
优化版的Dijkstra算法是通过小根堆来找到当前堆中距离起点最短且没有确定最短路的那个点 t。
优化时可以用手写堆方便插入查询元素,但更多的是用STL库里的优先队列这样代码更简洁且便于维护堆。
// n较大,m趋于n时为稀疏图,用邻接表存储图
void add(int a, int b, int c)
{
e[idx] = b;
w[idx] = c;
ne[idx] = h[a];
h[a] = idx++;
}
int dijkstra()
{
memset(dist, 0x3f, sizeof dist);
dist[1] = 0;
priority_queue<PII, vector<PII>, greater<PII>> heap; // 建立小根堆,每插入一个元素会自动维护,堆顶元素即为最小值
heap.push({1, 0});
while (heap.size())
{
auto t = heap.top(); //距离 已确定的点 最小的点
heap.pop();
int ver = t.first, distance = t.second; //取出节点编号和节点距离
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] 大于从t过来的距离
{
dist[j] = distance + w[i];
heap.push({dist[j], j});
}
}
}
if (dist[n] == 0x3f3f3f3f)return -1;
return dist[n];
}
例题:Acwing——850
Bellman_ford 算法
Bellman-Ford算法是通过循环 n 次,每次循环都遍历每条边,进而更新结点的距离,每一次的循环至少可以确定一个点的最短路,所以循环 n次,就可以求出 n 个点的最短路。
当判断图中是否有负环时Bellman_ford算法时间复杂度为O(nm)比SPFA算法要慢,所以通常Bellman_ford算法用来求有边数限制时的最短路径。
假设我们限制边数 k 为 1,那么外层循环只需要进行一次,肉眼可以看出,我们只能求出 1 和 2 和 3号结点的最短路,4号结点最短路是不存在的,可是当在枚举所有条边时,假如我们先枚举的1——>2边时,那么2号结点最短路被更新为2,可是当我们再枚举到2——>4边时,4号结点最短路会被更新为2+1=3,如果4后面还有结点的话,后面的所有结点都会被更新,可实际上,4结点是不存在最短路的,因为我们限制了 k。那么怎么解决呢,其实很简单,我们只需要用上一次的dist来更新即可,而我们把上一次更新后的dist放到备份里存起来以备下一次更新用。
有了备份,当枚举1——>2到时,2结点被更新为2,而枚举到2——>4时,4号结点是用2号结点上一次的dist来更新的,而2号结点上一次dist是 +∞,而 +∞ + 1 > +∞,所以说4号结点是不会被更新的。
struct Edge {
int a;
int b;
int w;
} e[M]; //把每个边保存下来即可
int dist[N];
int back[N]; //备份数组防止串联
int n, m, k; //k代表最短路径最多包涵k条边
int bellman_ford()
{
memset(dist, 0x3f, sizeof dist);
dist[1] = 0;
for (int i = 0; i < k; i++) //k次循环
{
memcpy(back, dist, sizeof dist); // 将dist 的 前 sizeof dist 个元素复制给back
for (int j = 0; j < m; j++) //遍历所有边
{
int a = e[j].a, b = e[j].b, w = e[j].w;
dist[b] = min(dist[b], back[a] + w); //使用上一次遍历时的 dist[a]
}
}
if (dist[n] > 0x3f3f3f3f / 2) return -1; // k条边走不到 n
else return dist[n];
}
例题:Acwing——853
SPFA 算法
spfa 算法用法很广,很多只有正权边的图也可以用spfa,所以在图论中它是一个很重要的算法。
spfa的算法思想(动态逼近法):
设立一个先进先出的队列q用来保存待优化的结点,优化时每次取出队首结点u,并且用u点当前的最短路径估计值对离开u点所指向的结点v进行松弛操作,如果 v点的最短路径被前面的结点更新,且v点不在当前的队列中,就将v点放入队尾。这样不断从队列中取出结点来进行松弛操作,直至队列空为止。(就是要拿更新过后的点去更新其他的点,因为只有用被更新过的点更新其他结点x,x的距离才可能变小。)
void add(int a, int b, int c)
{
e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx ++ ;
}
int spfa() // 队列只存变小的点
{
memset(dist, 0x3f, sizeof dist);
dist[1] = 0;
queue<int> q;
q.push(1);
st[1] = true; // 放入队列
while (q.size()) // 队列里存 遍历时 dist 被更新的点
{
int t = q.front();
q.pop();
st[t] = false; //踢出队列
for (int i = h[t]; i != -1; i = ne[i])
{
int j = e[i]; // t 出度的每个点 i是前面的点,j 表示 i 连接的点
if (dist[j] > dist[t] + w[i]) //w[i] 代表 i - j 的权重
{
dist[j] = dist[t] + w[i];
if (!st[j]) // 如果未被标记说明不在队列中
{
q.push(j); // 把被 t 更新的点存入队列
st[j] = true;
}
}
}
}
return dist[n];
}
例题:Acwing——851
Floyd 算法
Floyd算法又称为插点法,是一种利用动态规划的思想寻找给定的加权图中多源点之间最短路径的算法,与Dijkstra算法类似。
邻接矩阵 d[i][j] 表示的是从 i 点到 j 点的权值,现在在 i 到 j 之间插一个 k 点,那么状态转移方程为 d[i][j] = min(d[i][k] + d[k][j], d[i][j]);
这算法缺点是时间复杂度大,为O(n^3)。
核心代码:
void floyd()
{
for(int k = 1; k <= n; k++) // 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]);
}
例题:Acwing——3556