迪杰斯特拉(Dijkstra,单源最短路)算法
基本原理
从起始点出发,重复寻找当前距离起始点最近的且未访问过的结点然后用该结点访问距离数组,直到访问过所有结点为止。最终的距离数组即为起始点到其余个点的最短路径距离。
特点:它不允许有负权值,由于在第一步时就会将与源点最近的点确认为最短,如果这个是可以被缩短的那这个确认就是错误的。
处理框架
(1)在集合V-U中选择距离值最小的顶点vmin加入集合U;
(2)对集合V-U中各顶点的距离值进行修正:如果加入顶点vmin为中间顶点后,使v0到vi的距离值比原来的距离值更小,则修改vi的距离值。
(3)重复(1)(2)操作,直到从v0出发到达的所有顶点都在集合U中为止。
代码
const int inf = 500;
vector<int> dijkstra(vector<vector<int> > G,int source)
{
int n = G.size();
vector<int> dis(n,inf);
vector<int> vis(n,false);
dis[source]=0;
for(int i=0; i<n-1; i++)
{
int node = -1;
for(int j=0;j<n;j++)
{
if(!vis[j] && (node == -1 || dis[j]<dis[node]))
{
node = j;
}
}
for(int j=0; j<n; j++)
{
dis[j]=min(dis[j],dis[node]+G[node][j]);
}
vis[true];
}
return dis;
}
代价
初始化部分的时间复杂度为O(n),求最短路径部分由一个大循环组成,其中外循环进行n-1次,内循环为两个,均进行n次,因此,算法的时间复杂度为O(n2)。
弗洛伊德(Floyd,多源最短路)算法
基本原理
选取某个节点k作为i到j需要经过的中间节点,通过比较d(i,k)和d(k,j)的和与d(i,j) 的大小,将较小值更新为路径长度。对k节点的选取进行遍历,以得到在经过所有节点时i到j的最短路径长度,通过不断加入中间点的方式更新最短路径。
特点:边的权值正值负值均可。
//#include <iostream>
//#include<algorithm>
#include<bits/stdc++.h>
#include<iostream>
#include<cstdio>
using namespace std;
#define INF 0x3f3f3f3f //相当于定义INF为无穷大
int main()
{
int a[21][21];
int m,n,i, j, w;
for (int i = 1; i <= 20; i++) {
for (int j = 1; j <= 20; j++) {
if (i == j) {a[i][j] = 0;}
else a[i][j] = INF;
}
}
scanf("%d%d",&n,&m);
for (int k = 1;k<= m;k++) {
scanf("%d%d%d",&i,&j,&w);
a[i][j] = w;
a[j][i] = w;
}
for (int p = 1; p <= n; p++) {
for (int i = 1; i <= n; i++) {
if (a[i][p] == INF) continue; //如果a[i][p] == INF,结束本次循环,进行下一次循环
for (int j = 1; j <= n; j++) {
if (i==j)continue;
a[i][j] = min(a[i][j], a[i][p] + a[p][j]);
}
}
}
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) {
printf("%d ",a[i][j]);
}
printf("\n");
}
return 0;
}
代价
弗洛伊德算法的时间复杂度一般为O(n3)。
贝尔曼福特(Bellman-ford,单源最短路)算法
基本原理
不断试图对图上的每一条边进行松弛,每进行一轮循环,就对图上所有的边进行一次松弛操作,当一次循环中没有成功的松弛操作时,算法结束。共执行N-1次操作,N为图中的节点数。
特点:松弛操作是指不断地更新最短路径和前驱结点的操作。边权没有要求,可正可负,还可判断负权边,时间复杂度为O(n*m)。
//#include <iostream>
//#include <cstring>
//#include <algorithm>
#include<bits/stdc+++.h>
using namespace std;
const int N=510,M=1e4+10;
int dist[N],backup[N];
int n,m,k;
struct node
{
int a,b,c;
}edg[M];
int bellman_ford()
{
memset(dist,0x3f,sizeof dist);
dist[1]=0;
for(int i=0;i<k;i++)
{
memcpy(backup,dist,sizeof dist);
for(int j=0;j<m;j++)
{
int a=edg[j].a,b=edg[j].b,c=edg[j].c;
dist[b]=min(dist[b],backup[a]+c);
}
}
return dist[n];
}
int main()
{
cin>>n>>m>>k;
for(int i=0;i<m;i++)
{
int a,b,c;
cin>>a>>b>>c;
edg[i]={a,b,c};
}
//这里不用0x3f3f3f3f是因为防止n号点被负权边更新
if(bellman_ford()>0x3f3f3f3f/2) //相当于无穷大
cout<<"impossible";
else
cout<<dist[n];
return 0;
}
SPFA(Bellman-ford的队列优化版)
基本原理
bellman-Ford中有很多重复无用的松弛操作,只有上一次被松弛的节点所连接的边,才有可能引起下一次的松弛操作。所以,我们用STL队列来储存被松弛的节点,这样就可以访问必须访问的边了。
特点:由于SPFA是基于Bellman-ford的优化版本,所以在大多数情况下,SPFA都跑的比较快,但是时间复杂度最优 O(m) ,最差退回bellman-ford 的 O(n * m)。边权没有要求,可正可负,还可以判断负权边。
处理框架
算法大致流程是用一个队列来进行维护。 初始时将源加入队列。 每次从队列中取出一个元素,并对所有与他相邻的点进行松弛,若某个相邻的点松弛成功,如果该点没有在队列中,则将其入队。 直到队列为空时算法结束。
判断有无负环:如果某个点进入队列的次数超过V次则存在负环(SPFA无法处理带负环的图)
1.用dis数组记录点到有向图的任意一点距离,初始化起点距离为0,其余点均为INF,起点入队。
2.判断该点是否存在。(未存在就入队,标记)
3.队首出队,并将该点标记为没有访问过,方便下次入队。
4.遍历以对首为起点的有向边(t,i),如果dis[i]>dis[t]+w(t,i),则更新dis[i]。
5.如果i不在队列中,则入队标记,一直到循环为空。
代码
#include<bits/stdc++.h> //万能
//#include<iostream>
//#include<vector>
//#include<queue>
//#include<cstdio>
using namespace std;
const int INF = 0x3f3f3f3f; //相当于无穷大
const int maxn = 1000;
int dis[maxn];
bool vis[maxn];
struct node {
int s1;//记录结点
int side;//边权
};
vector<node>mp[maxn];//用vector建立邻接表
void Spfa(int s) {
queue<int>v;
vis[s] = 1; v.push(s); dis[s] = 0;
while (!v.empty()) {
int q = v.front();
v.pop(); vis[q] = 0;
for (int i = 0; i < mp[q].size(); i++) {
if (dis[mp[q][i].s1] > dis[q] + mp[q][i].side) {
dis[mp[q][i].s1] = dis[q] + mp[q][i].side;
if (!vis[mp[q][i].s1]) {//是在更新新的值条件里面判断,一定特别注意这点
v.push(mp[q][i].s1);
vis[mp[q][i].s1] = 1;//标记未标记过的点
}
}
}
}
}
算法大致流程是用STL队列来进行维护。 先将源加入队列。 然后每次从队列中取出一个元素,并对所有与他相邻的点进行松弛操作,如果某个相邻的点松弛成功,如果该点没有在队列中,则将其入队。 直到队列为空时算法结束。
引用
参考了一些大佬的思路和博客。