解决最短路常用的有三种算法Floyd、Dijkstra、SPFA。三种方法各有优劣
Floyd算法是多源最短路算法,复杂度最高(n^3),通常用在点比较少的起点不固定的问题中。能解决负边(负权)但不能解决负环。
Dijkstra算法是单源最短路算法,最常用时间复杂度(n^2)优化后可以达到(nlogn),不能解决负边问题,稀疏图(点的范围很大但是边不多,边的条数|E|远小于|V|²)需要耗费比较多的空间。
SPFA算法适合稀疏图,可以解决带有负权边,负环的问题,但是在稠密图中效率比Dijkstra要低。
Floyd算法
floyd算法是解决除带负环外(可以计算任意两个点之间的最短路)
有向图或者无向图的多源最短路问题(可带负边权),图常用邻接矩阵来存储。
算法实现过程的结果也在矩阵中产生,即在算法实现前矩阵为两个顶点之间的权值,实现后矩阵为两点之间的最短路
时间复杂度为O(V^3),空间复杂度为O(V^2)算法思想
很容易理解,我们从一个点i到另一个点j, 无非就两种走法
- 直接从i到j
- 通过中间点k中转,i->k->j
所以我们就遍历所有情况,如果通过某个中转点距离小于直接到达的距离,就更新这两点间的距离。
#include<iostream>
#include<cstring>
#include<algorithm>
#include<cstdio>
#include<cstdlib>
#include<queue>
#include<cmath>
#include<cctype>
#include<stack>
#include<map>
#include<string>
#include<cstdlib>
#define ll long long
using namespace std;
const int N = 101;
int g[N][N];
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) {
g[i][j] = min(g[i][j], g[i][k] + g[k][j]);
}
}
}
}
int main(){
int n, m;
cin >> n >> m;
int u, v, w;
memset(g, 0x3f, sizeof g);
for (int i = 0; i <= n; ++i) {
g[i][i] = 0;
}
while (m--) {
cin >> u >> v >> w;
g[v][u] = g[u][v] = w;
}
floyd(n);
for (int i = 1; i <= n; ++i) {
for (int j = 1; j <= n; ++j) {
cout << g[i][j] << " ";
}
cout << endl;
}
system("pause");
return 0;
}
Dijkstra算法
Dijkstra算法采用的是一种贪心的策略,声明一个数组dis来保存源点到各个顶点的最短距离和一个保存已经找到了最短路径的顶点的集合:T,初始时,原点 s 的路径权重被赋为 0 (dis[s] = 0)。若对于顶点 s 存在能直接到达的边(s,m),则把dis[m]设为w(s, m),同时把所有其他(s不能直接到达的)顶点的路径长度设为无穷大。初始时,集合T只有顶点s。
然后,从dis数组选择最小值,则该值就是源点s到该值对应的顶点的最短路径,并且把该点加入到T中,OK,此时完成一个顶点,
然后,我们需要看看新加入的顶点是否可以到达其他顶点并且看看通过该顶点到达其他点的路径长度是否比源点直接到达短,如果是,那么就替换这些顶点在dis中的值。
然后,又从dis中找出最小值,重复上述动作,直到T中包含了图的所有顶点。算法思想
有两个数组,dis和vis含义参见上面,初始时vis中只有起点,更新dis中的起点到所有点的距离.
遍历所有节点,找到距离起点最近的一个点K,将这个点加入vis中标记
进行松弛操作,遍历没有在vis数组中的其他所有点,比较起点——>该点和起点——>K点——>该点的距离,
重复2-3操作,直到所有的点遍历完
#include<iostream>
#include<cstring>
using namespace std;
const int N = 1001;
const int M = 1001;
int n, m;
struct edge {
int v, w, next;
edge() {}
edge(int vv, int ww, int _next) {
v = vv;
w = ww;
next = _next;
}
}e[2 * M];
int head[N], num;
void Init() {
memset(head, -1, sizeof head);
num = 0;
}
//无向边插入
void insert(int u, int v, int w) {
e[num] = edge(v, w, head[u]); //相当于链表头插法
head[u] = num++;
//对称边插入
e[num] = edge(u, w, head[v]); //相当于链表头插法
head[v] = num++;
}
bool vis[N];
int dis[N];
void dijkstra(int u) {
memset(dis, 0x3f, sizeof dis); //每个点到源点距离初始化为最大
dis[u] = 0; //源点到自身的距离为0
for (int i = 0; i < n; ++i) { //每一层标记一个结点,共n个结点,n层
int mind = 0x3f3f3f3f, minv = -1; //最小的距离和点
for (int j = 1; j <= n; ++j) { //在n个结点中找一个未被标记且到达源点路径最短的结点
if (!vis[j] && dis[j] < mind) {
mind = dis[j];
minv = j;
}
}
if (minv == -1) { //全部结点已标记,退出
return;
}
vis[minv] = true; //标记该节点
/*~j代表j != -1*/
for (int j = head[minv]; ~j; j = e[j].next) { //从该结点出发,依次遍历相邻且未标记结点,看看是否可以更新其相邻结点到源点之间的距离
int v = e[j].v;
int w = e[j].w;
if (!vis[v] && dis[v] > w + dis[minv]) {
dis[v] = w + dis[minv]; //更新距离
}
}
}
}
//打印邻接表
void print() {
for (int i = 1; i <= n; ++i) {
cout << i << "->";
int next = head[i];
while (next != -1) {
cout << e[next].v << "->";
next = e[next].next;
}
cout << endl;
}
}
int main() {
Init();
cin >> n >> m;
while (m--) {
int u, v, w;
cin >> u >> v >> w;
insert(u, v, w);
}
dijkstra(1); //从源点出发
print();
for (int i = 1; i <= n; ++i) {
cout << dis[i] << " ";
}
cout << endl;
return 0;
}
dijkstra的堆优化?
观察dijkstra的流程,发现步骤在n个结点中找一个未被标记且到达源点路径最短的结点可以优化
怎么优化呢?
我会zkw线段树!我会斐波那契堆!我会堆!
我们可以用堆对dis数组进行维护,用O(log2n)的时间取出堆顶元素并删除,用O(log2n)遍历每条边,总复杂度O((n+m)log2n)
这个堆怎么实现呢,我们可以借助STL库内的priority_queue优先队列实现对 dis 的小顶堆,每次从堆顶访问的元素的dis都是未标记最小的
/****
堆优化版dijkstra
****/
#include<bits/stdc++.h>
#include<cstring>
using namespace std;
typedef pair<int, int> PII;
const int N = 100005;
const int M = 200005;
struct edge{
int v,w,next;
}e[M];
int head[N];
int pointer;
void init_list(){
memset(head, -1, sizeof head);
pointer = 0;
}
void insert(int u,int v,int w){
e[pointer].v = v;
e[pointer].w = w;
e[pointer].next = head[u];
head[u] = pointer++;
}
int n, m,s;
int dis[N];
bool vis[N]; //判读每个顶点最短路是否已经确定
void dijkstra(){
memset(dis,0x3f,sizeof dis);
dis[s] = 0;
priority_queue<PII, vector<PII>, greater<PII> > heap; //借助优先队列找出未标记点的最小dis值
heap.push({0,s}); //first存单源最短路,second存顶点编号
while(! heap.empty()){
int now = heap.top().second;
int now_dis = heap.top().first;
heap.pop();
if(vis[now]) continue;
vis[now] = true; //确定v到1的最短路
for(int j = head[now]; ~j; j = e[j].next){
int v = e[j].v;
int w = e[j].w;
if(dis[v] > now_dis + w){
dis[v] = now_dis + w;
heap.push({dis[v],v});
}
}
}
}
int main(){
init_list();
scanf("%d %d %d", &n, &m, &s);
for(int i = 0; i < m; ++i){
int u, v, w;
scanf("%d %d %d", &u, &v, &w);
insert(u, v, w);
}
dijkstra();
for(int i = 1; i<= n; ++i){
printf("%d ",dis[i]);
}
return 0;
}
SPFA算法
SPFA是一种用队列优化的B-F算法,看上去和BFS很像,B-F效率较低就不介绍了,还有一种用DFS优化B-F的SPFA但是往往这种方法比平时更加劣化没有队列优化的好用,平时用SPFA就够用了。可以解决负边问题,可以判断负环是否存在。在稀疏图中,采用类似邻接链表储存比较节省空间。
算法思想
1、初始时,只有把起点放入队列中。
2、遍历与起点相连的边,如果可以松弛就更新距离dis[],然后判断如果这个点没有在队列中就入队标记。
3、出队队首,取消标记,循环2-3步,直至队为空。
4、所有能更新的点都更新完毕,dis[]数组中的距离就是,起点到其他点的最短距离。
为什么SPFA可以处理负边:因为在SPFA中每一个点松弛过后说明这个点距离更近了,所以有可能通过这个点会再次优化其他点,所以将这个点入队再判断一次,而Dijkstra中是贪心的策略,每个点选择之后就不再更新,如果碰到了负边的存在就会破坏这个贪心的策略就无法处理了。
如何判断成环:
在储存边时,记录下每个点的入度,每个点入队的时候记录一次,如果入队的次数大于这个点的入度,说明从某一条路进入了两次,即该点处成环。
#include<iostream>
#include<cstring>
#include<algorithm>
#include<cstdio>
#include<cstdlib>
#include<queue>
#include<cmath>
#include<cctype>
#include<stack>
#include<map>
#include<string>
#include<cstdlib>
#define ll long long
using namespace std;
const int N = 1001;
const int M = 1001;
int n, m;
struct edge {
int v, w, next;
edge() {}
edge(int vv, int ww, int _next) {
v = vv;
w = ww;
next = _next;
}
}e[2 * M];
int head[N];
int cnt;
void Init() {
memset(head, -1, sizeof head);
cnt = 0;
}
//有向边插入
void insert(int u, int v, int w) { //用于数组模拟链表表示邻接表边与点的关系要搞清楚
e[cnt] = edge(v, w, head[u]); //相当于链表头插法
head[u] = cnt++;
}
bool vis[N];
int dis[N];
queue<int> q;
/*SPFA算法判负环*/
//用一个数组in保存每个顶点的入队次数,如果入队次数大于n,则说明存在负环,那个点的最短路判断无解
int in[N];
void SPFA(int u) {
memset(dis, 0x3f, sizeof dis); //每个点到源点距离初始化为最大
memset(vis, false, sizeof vis);
dis[u] = 0;
q.push(u);
//in[u]++; //用于判负环
vis[u] = true; //代表进过队列
while (!q.empty()) {
int curr_v = q.front();
q.pop();
vis[curr_v] = false; //代表出过队列
for (int j = head[curr_v]; ~j; j = e[j].next) {
int v = e[j].v; //当前点的邻接点
int w = e[j].w;
if (dis[v] > dis[curr_v] + w) {
dis[v] = dis[curr_v] + w;
if (!vis[v]) {
q.push(v);
vis[v] = true; //代表进过队列
/*in[v]++;
if (in[v] > n) {
return false; //如果存在负环,则return false,需要判断负环时应把SPFA改成bool类型函数
}*/
}
}
}
}
}
//打印邻接表
void print() {
for (int i = 0; i < n; ++i) {
cout << i << "->";
int next = head[i];
while (next != -1) {
cout << e[next].v << "->";
next = e[next].next;
}
cout << endl;
}
}
int main() {
Init();
cin >> n >> m;
while (m--) {
int u, v, w;
cin >> u >> v >> w;
insert(u, v, w);
}
print();
cout << endl;
SPFA(0);
for (int i = 0; i < n; ++i) {
cout << dis[i] << " ";
}
cout << endl;
system("pause");
return 0;
}