从简单到难的最短路径实现方法 测试都是 hdu 2544
DFS-就是深度遍历
这个不会可以自己百度一下原理或者看书,肯定比我讲的好很多(广度遍历 BFS 也是可以的,就不贴了)。直接上代码吧,用模版题检验是否写对,题目是
//
// 深度遍历实现最短路.cpp
// c++ virtureTest
//
// Created by 刘恒 on 2018/8/19.
// Copyright © 2018年 刘恒. All rights reserved.
//
#include <stdio.h>
#include <cstdlib>
#include <cstring>
#include <iostream>
#include <vector>
#include <algorithm>
#include <queue>
using namespace std;
#define inf 0x3f3f3f3f
#define N 333
int map[333][333];
int minDis;
int visit[N];
void dfs(int vertex,int dis,int endNum)
{
if(minDis < dis)
return;
if(vertex == endNum)
{
if(minDis > dis)
minDis = dis;
return;
}
else
{
for(int i = 1 ; i <= endNum ; i++)
{
if(visit[i] == 0 && map[vertex][i] != inf && i != vertex)
{
visit[i] = 1;
dfs(i, dis+map[vertex][i], endNum);
visit[i] = 0;//
}
}
return;
}
}
int main()
{
int n,m;
while (cin>>n) {
cin>>m;
if(n==0 || m==0)
break;
memset(map, inf, sizeof(map));
memset(visit, 0, sizeof(visit));
minDis = inf;
for(int i = 0 ; i< m ; i++)
{
int a,b,dis;
cin>>a>>b>>dis;
map[a][b] = dis;
map[b][a] = dis;
}
visit[1] = 1;
dfs(1, 0, n);
cout<<minDis <<endl;
}
}
弗洛伊德算法-Floyd
这个算法我认为是最好实现的,而且对于顶点少的效果还很好,但是这个算法很简单吗,其实我觉得还是比较难理解的,要知其然而知其所以然话的我觉得比Dijkstra、DFS 要难理解的多,可能你会写,但是问你为什么这样做能保证它是最短路,我觉得有很多人是不知道的。
首先肯定要先初始化,map[N][N] = 无穷大 ,同时输入每个点之间的路径。举个例子,A-F点,首先以A 为中介点,观察图上的任意两点在加入中介点A后距离能不能缩短,如果能,那么最短距离更新为K-A-S(KS为图上任意两点,K也是可以为A 的)实际例子:一开始A-B为无穷大,现在A-B = map[A][B],A-C = map[A][C]等。第二次循环,以B(不一定按顺序,除了A外的任意一个点都可以)为中介点,更新距离,发现map[A][C] > map[A][B] + map[B][C] , 更新。并且以后若以C为中介点更新距离A-C-K,实际路径则是A-B-C-K。
下面是百科的介绍:
1,从任意一条单边路径开始。所有两点之间的距离是边的权,如果两点之间没有边相连,则权为无穷大。
2,对于每一对顶点 u 和 v,看看是否存在一个顶点 w 使得从 u 到 w 再到 v 比已知的路径更短。如果是更新它。
把图用邻接矩阵G表示出来,如果从Vi到Vj有路可达,则G[i][j]=d,d表示该路的长度;否则G[i][j]=无穷大。定义一个矩阵D用来记录所插入点的信息,D[i][j]表示从Vi到Vj需要经过的点,初始化D[i][j]=j。把各个顶点插入图中,比较插点后的距离与原来的距离,G[i][j] = min( G[i][j], G[i][k]+G[k][j] ),如果G[i][j]的值变小,则D[i][j]=k。在G中包含有两点之间最短道路的信息,而在D中则包含了最短通路径的信息。
比如,要寻找从V5到V1的路径。根据D,假如D(5,1)=3则说明从V5到V1经过V3,路径为{V5,V3,V1},如果D(5,3)=3,说明V5与V3直接相连,如果D(3,1)=1,说明V3与V1直接相连。
Floyd算法适用于APSP(All Pairs Shortest Paths,多源最短路径),是一种动态规划算法,稠密图效果最佳,边权可正可负。此算法简单有效,由于三重循环结构紧凑,对于稠密图,效率要高于执行|V|次Dijkstra算法,也要高于执行|V|次SPFA算法。
优点:容易理解,可以算出任意两个节点之间的最短距离,代码编写简单。
缺点:时间复杂度比较高,不适合计算大量数据。(补充一下 复杂度为O(3) )
//
// Floyd.cpp
// c++ virtureTest
//
// Created by 刘恒 on 2018/8/18.
// Copyright © 2018年 刘恒. All rights reserved.
//
#include <stdio.h>
#include <cstdlib>
#include <cstring>
#include <iostream>
#include <vector>
#include <algorithm>
#include <queue>
#include <list>
#include <array>
using namespace std;
#define inf 0x3f3f3f3f
#define N 111
int map[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++)
{
if(map[i][j]>map[i][k]+map[k][j])
{
map[i][j]=map[i][k]+map[k][j];
}
}
}
}
}
int main()
{
int n,m;
while (cin>>n) {
cin>>m;
if(n==0 || m==0)
break;
memset(map, inf, sizeof(map));
for(int i= 1; i<=m ; i++)
{
int a,b,c;
cin>>a>>b>>c;
map[a][b]=c;
map[b][a]=c;
}
Floyd(n);
cout<<map[1][n]<<endl;
}
}
Dijkstra-迪杰斯特算法
这个反是让我觉得最好理解的一个算法,可是写起来却比较麻烦。这个算法用到了一个贪心的思想。先说用邻接矩阵实现Dijkstra。
邻接矩阵实现Dijkstra
建立两个集合S,V。S集合初始只有起点一个点,V集合有除了起点以外的所有点。初始化dis[N],dis[1] = 0;其余的为无穷大。遍历S和V集合,选择V集合当中到S集合中的点距离最小的那个点,让它从V集合中除去加入到S集合。这样做能保证,每一次找到的点,S-K(K为找到的点)dis[K] 都是所有路径中最小的,不知道你们能不能理解,我举个反例:如果存在某个中介点Q,使得S-Q-K < S-K,那么是不是证明S-Q<S-K,既然S-Q<S-K,那么根据我们的添加规则,Q肯定是先与K加入到S集合当中的。以此类推,最终找到终点就能停止了。
下面是百度百科的算法步骤
把顶点集合V分成两组:
(1)S:已求出的顶点的集合(初始时只含有源点V0)
(2)V-S=T:尚未确定的顶点集合
将T中顶点按递增的次序加入到S中,保证:
(1)从源点V0到S中其他各顶点的长度都不大于从V0到T中任何顶点的最短路径长度
(2)每个顶点对应一个距离值
S中顶点:从V0到此顶点的长度
T中顶点:从V0到此顶点的只包括S中顶点作中间顶点的最短路径长度
依据:可以证明V0到T中顶点Vk的,或是从V0到Vk的直接路径的权值;或是从V0经S中顶点到Vk的路径权值之和
(反证法可证)
求最短路径步骤
算法步骤如下:
G={V,E}
1. 初始时令 S={V0},T=V-S={其余顶点},T中顶点对应的距离值
若存在<V0,Vi>,d(V0,Vi)为<V0,Vi>弧上的权值
若不存在<V0,Vi>,d(V0,Vi)为∞
2. 从T中选取一个与S中顶点有关联边且权值最小的顶点W,加入到S中
3. 对其余T中顶点的距离值进行修改:若加进W作中间顶点,从V0到Vi的距离值缩短,则修改此距离值
重复上述步骤2、3,直到S中包含所有顶点,即W=Vi为止。
代码敬上!!
#include <stdio.h>
#include <cstdlib>
#include <cstring>
#include <iostream>
#include <vector>
#include <algorithm>
#include <queue>
using namespace std;
#define inf 0x3f3f3f3f
#define N 333
int map[333][333];
int s[333];
int v[333];
int dis[N];
void Dijkstra(int n)
{
int s_size =1;//S集合的大小
int v_size = n-1;V集合的大小
s[1] = 1;
dis[1] = 0;
for(int i = 2 ; i <= n ; i++)
{
v[i-1] = i;
}
for(int i = 1;i<=s_size ; i++)
{
int min = inf;
int _min = 1;
for(int q = 1 ; q<=s_size ; q++)
{
for(int j = 1;j<=v_size ; j++)
{
int k = map[s[q]][v[j]];
if( k!= -1)
{
int d = dis[s[q]] + k;
if(dis[v[j]] > d)
{
dis[v[j]] = d;
}
if(min > dis[v[j]])
{
min = dis[v[j]];
_min = j;
}
}
}
}
s_size ++;
s[i+1] =v[_min];
for(int kk = _min ; kk<=v_size-1 ; kk++)
{
v[kk] = v[kk+1];
}
v_size --;
if(v_size == 0)
break;
i=0;
}
}
int main()
{
int n,m;
while (cin>>n) {
cin>>m;
if(n==0 || m==0)
break;
memset(map, -1, sizeof(map));
for(int i = 0 ; i< m ; i++)
{
int a,b,dis;
cin>>a>>b>>dis;
map[a][b] = dis;
map[b][a] = dis;
}
for(int i = 2 ; i<=n ; i++)//初始化dis
{
if(map[1][i]!= -1)
{
dis[i] = map[1][i];
}
else
{
dis[i] = inf;
}
}
//上面全是初始化
Dijkstra(n);
cout<<dis[n]<<endl;
}
}
其实这个代码我写的不好,大家就将就一下,因为第一个写的就是这个,思维比较混乱。
接下来讲下下如何用邻接表来实现Dijkstra
用邻接表来实现dijkstra 我觉得可能更加直观点。其实很像BFS上的一种改进。
先定义如下几个基本结构:
class Edge
{
public:
int vertexId;
int length;
Edge *next = nullptr;
};
class Vertex
{
public:
int id;
int rank=-1;
bool isVisit =false;
Edge *first = nullptr;
Edge *last = nullptr;
void AddEdge(Edge *edge);
};
void Vertex::AddEdge(Edge *edge)
{
if(this->first == nullptr)
{
first = edge;
last = edge;
}
else
{
last->next = edge;
last = edge;
}
}
class Map
{
public:
vector<Vertex> vertexs;
Map();
};
Map :: Map()
{
for(int i = 0 ; i< N ; i ++)
{
Vertex v;
v.id = i;
vertexs.push_back(v);
}
}
class Node
{
public:
int id;
int dis;
friend bool operator < (const Node &node1,const Node &node2)
{
return node1.dis < node2.dis;
}
Node(int id,int minDis)
{
this->id = id;
this->dis = minDis;
}
Node(){}
};
邻接表的优点我也就不多说了,省空间。如何创建邻接表。。。。emm看书吧,和DFS,BFS一样,基础内容我都建议看书。
思想其实就是贪心思想,和邻接矩阵是一样的。先说下结构,定义minDis[N] 存储起点到达各个点的最短距离。如果需要将路径输出还能定义一个parent[N]来存储路径。还有记得初始化邻接表。
再创建一个最小堆队列priority_queue<Node> q;Node 是自定义的结构。最小堆队列其实就是根结点永远是最小的,访问方式也只能是q.top()。一开始将起点推入q中。minDis[1] = 0;
例如A-F,遍历与A 相连的结点,将其推入q。由于最小堆,所以我们取出的每个结点都是最短的路径。接下来的思想其实都一样的,不多讲了=-=。下面代码奉上:
//
// 邻接表实现最短路径 Dijkstra.cpp
// c++ virtureTest
//
// Created by 刘恒 on 2018/8/16.
// Copyright © 2018年 刘恒. All rights reserved.
//
#include <stdio.h>
#include <cstdlib>
#include <cstring>
#include <iostream>
#include <vector>
#include <algorithm>
#include <queue>
#include <list>
#include <array>
using namespace std;
#define inf 0x3f3f3f3f
#define N 333
class Edge
{
public:
int vertexId;
int length;
Edge *next = nullptr;
};
class Vertex
{
public:
int id;
int rank=-1;
Edge *first = nullptr;
Edge *last = nullptr;
void AddEdge(Edge *edge);
};
void Vertex::AddEdge(Edge *edge)
{
if(this->first == nullptr)
{
first = edge;
last = edge;
}
else
{
last->next = edge;
last = edge;
}
}
class Map
{
public:
vector<Vertex> vertexs;
Map();
};
Map :: Map()
{
for(int i = 0 ; i< N ; i ++)
{
Vertex v;
v.id = i;
vertexs.push_back(v);
}
}
class Node
{
public:
int id;
int dis;
friend bool operator < (const Node &node1,const Node &node2) //定义最小堆用的,重载运算符
{
return node1.dis < node2.dis;
}
Node(int id,int minDis)
{
this->id = id;
this->dis = minDis;
}
Node(){}
};
Node minDis[N];
int Parent[N];
priority_queue<Node> q; //最小堆和最大堆。 只能访问根节点
void CreatMap(int n, int m, Map &map)
{
for(int i = 1 ; i<= m ; i++)
{
int a,b,dis;
cin>>a>>b>>dis;
Edge *edge1 = new Edge();
edge1->vertexId = b;
edge1->length = dis;
map.vertexs[a].AddEdge(edge1);
Edge *edge2 = new Edge();
edge2->vertexId = a;
edge2->length = dis;
map.vertexs[b].AddEdge(edge2);
}
}
void Dijkstra(Map &map,int start,int n)
{
for(int i=1;i<=n ; i++)
{
minDis[i].id = i;
minDis[i].dis = inf;
Parent[i] = -1;
}
minDis[start].dis = 0;
q.push(minDis[start]);
while (!q.empty()) {
Node node = q.top();
q.pop();
Edge *p = map.vertexs[node.id].first;
while (p!=nullptr) {
if(minDis[p->vertexId].dis > minDis[node.id].dis + p->length)
{
minDis[p->vertexId].dis = minDis[node.id].dis + p->length;
q.push(minDis[p->vertexId]);
Parent[p->vertexId] = node.id;//需要输出路径的时候可以用到
}
p = p->next;
}
}
}
int main()
{
int n,m;
while (cin>>n) {
cin>>m;
if(n==0 || m==0)
break;
Map map;
CreatMap(n, m, map);
Dijkstra(map, 1, n);
cout<<minDis[n].dis<<endl;
}
}
下面是一些优缺点:
优点:O(N*N),加堆优化:O(N*logN)
缺点: 在单源最短路径问题的某些实例中,可能存在权为负的边。
如果图G=(V,E)不包含从源s可达的负权回路,
则对所有v∈V,最短路径的权定义d(s,v)依然正确,
即使它是一个负值也是如此。但如果存在一从s可达的负回路,
最短路径的权的定义就不能成立。S到该回路上的结点就不存在最短路径。
当有向图中出现负权时,则Dijkstra算法失效。当不存在源s可达的负回路时,
我们可用Bellman-Ford算法实现。
这里说一下为什么Dijkstra 不能有负权,我举个例子:我门每次是不是都要保证S集合中的每一个点都是当前的最短距离,假设V集合中存在点Q ,S集合中存在点A,B,C.dis(A-B) = 4,dis(B-Q) = -4;dis(A-C) = 5;dis(B-C) = 1;dis(C-Q) =1;按照我们的做法,Q是不和A直接相连的,所以第一遍Q肯定没有加入S集合,此时minDis[B] = A-B = 4;而实际上,minDis[B] = 5+1-4 = 2;???对吧如果我门要求的就是AB 的最短距离是不是就出现问题了。
BellMan-Ford 贝尔曼-福特
为了解决Dijkstra算法不能解决负权的问题,所以出现了了BellMan-Ford算法。注意点是,Floyd也是能解决负权边的,但是在顶点太多的时候,效率实在是太低了-0-,毕竟O(3),还有BellMan-Ford还能发现负权环。
BellMan-Ford 算法的实现和Floyd一样简单,简而言之,若有n个顶点,那么就对所有的边的进行n-1次松弛操作,面是松弛操作,其实就是
if(map[i][j]>map[i][k]+map[k][j])
{
map[i][j]=map[i][k]+map[k][j];
}
类似于上面的操作,上面的是floyd的松弛操作。
因为没有前提条件,所以能解决负权边的问题。思想我觉得和floyd挺像的。对边进行n-1次松弛操作,实在懒得讲了,直接上代码,我觉得看了代码以后肯定有感觉!!
#include <stdio.h>
#include <cstdlib>
#include <cstring>
#include <iostream>
#include <vector>
#include <algorithm>
#include <queue>
using namespace std;
#define inf 0x3f3f3f3f
#define N 333
int minDis[N];
typedef struct Edge
{
int start = 0;
int end = 0;
int weight = inf;
}Edge;
void BellmanFord(Edge edge[],int v_num,int edge_num)
{
minDis[1] = 0;
for(int i = 0 ; i < v_num -1 ; i ++)
{
for(int j = 0 ; j < edge_num ; j++)
{
if(minDis[edge[j].end] > minDis[edge[j].start] + edge[j].weight)
{
minDis[edge[j].end] = minDis[edge[j].start] + edge[j].weight;
}
}
}
}
int main()
{
int n,m;
while (cin>>n) {
cin>>m;
if(n==0 || m==0)
break;
Edge edge[100001];
memset(minDis, inf, sizeof(minDis));
for(int i= 0 ; i< m ; i++)
{
int a,b,c;
cin>>a>>b>>c;
edge[i].start = a;
edge[i].end = b;
edge[i].weight = c;
}
BellmanFord(edge, n, m);
cout<<minDis[n]<<endl;
}
}
这里说下BellMan-Ford 的时间复杂度是O(VE),V是顶点数量,E是边的数量。所以在边数少,顶点数多的时候计算速度会比Floyd要快很多。
SPFA-Bellman的一种改进
SPFA 算法是 Bellman-Ford算法 的队列优化算法的别称,通常用于求含负权边的单源最短路径,以及判负权环。SPFA 最坏情况下复杂度和朴素 Bellman-Ford 相同,为 O(VE)。(来自百科介绍)
根据上面的BellMan-Ford 我门知道,BellMan-Ford 要对每一条边都进行一次松弛,直到进行n-1次,每一次都能确定一个点的最短路径(我门并不知道是哪个点)。但是用每次都把所有边拿来松弛太浪费了,不难发现,只有那些已经确定了最短路径的点所连出去的边才是有效的,因为新确定的点一定要先通过已知(最短路径的)节点。 详细点的话可以看这两篇博客,我觉得讲的很好https://blog.csdn.net/mmy1996/article/details/52225893 和https://www.sohu.com/a/206351499_479559如果考虑一个随机图(点和边随机生成),除了已确定最短路的顶点与尚未确定最短路的顶点之间的边,其它的边所做的都是无用,因为已经确定的点之间的松弛操作是没有必要的 。具体思想参考一下Dijkstra算法,他们之间其实都是很类似的。
可能讲了那么多还不是很理解,博主我也一样,想了好几天,可能代码都很好实现,一下就写好了,刷刷题什么的都没有什么问题,但我觉得知道算法为什么要这么写还是比较重要的,需要自己多花时间,多看几篇博客。下面就代码奉上了
//
// spfa.cpp
// c++ virtureTest
//
// Created by 刘恒 on 2018/8/20.
// Copyright © 2018年 刘恒. All rights reserved.
//
#include <stdio.h>
#include <cstdlib>
#include <cstring>
#include <iostream>
#include <vector>
#include <algorithm>
#include <queue>
using namespace std;
#define inf 0x3f3f3f3f
#define N 111
int minDis[N];
int map[N][N];
int sign[N];
void SPFA(int nodenum)
{
queue<int> SP;
minDis[1] = 0;
SP.push(1);
sign[1] = 1;
while (!SP.empty()) {
int u = SP.front();
SP.pop();
sign[u] = 0;
for(int i = 1; i <= nodenum ; i++)
{
if(i!= u && minDis[i] > minDis[u] + map[u][i])
{
minDis[i] = minDis[u] + map[u][i];
if(sign[i] == 0)
{
SP.push(i);
sign[i] = 1;
}
}
}
}
}
int main()
{
int n,m;
while (cin>>n) {
cin>>m;
if(n==0 || m==0)
break;
memset(minDis, inf, sizeof(minDis));
memset(map, inf, sizeof(map));
memset(sign, 0, sizeof(sign));
for(int i= 0 ; i< m ; i++)
{
int a,b,c;
cin>>a>>b>>c;
map[a][b] = c;
map[b][a] = c;
}
SPFA(n);
cout<<minDis[n]<<endl;
}
}
总结
求最短路,正常情况下用dijkstra 就够了,速度一般是最快,如果有负权,可以考虑使用Floyd 和Spfa;需要判断负权环的话就用 SPFA,至于BellMan-Ford看看就好,毕竟效率还是太差了,而且它能做的SPFA都可以,而且还更快是吧==。写的不好,但依旧希望能对你们有点帮助吧。