最短路径
找到从一个顶点到另一个顶点的成本最小的路径。
定义 :在一幅加权有向图中,从顶点S到顶点t的最短路径是所有从s到t的路径中的权重最小者。
最短路径的性质:
1.路径是有方向的。
2.权重不一定等价于距离。
3.并不是所有顶点都是可达的。
4.负权重会使问题更复杂。
5.最短路径一般哦都是简单的。(忽略0权重边,即没有环)
6.最短路径不一定是唯一的。
7.可能存在平行边和自环。(平行边中的权重最小者才会被选中,最短路径也不可能包含自环)
最短路径树:以S为根结点,树的每条路径都是有向图中的一条最短路径。
加权有向图的数据结构:
1.加权有向边:
代码:
#ifndef DEDGE_H
#define DEDGE_H
#include<sstream>
#include<string>
using namespace::std;
class DEdge
{
private:
int v;//起点
int w;//终点
double weight;//权重
public:
DEdge(){}
DEdge(int v, int w, double weight) :v(v), w(w), weight(weight){}
bool operator<(const DEdge&itr)const
{
return weight < itr.weight;
}
bool operator>(const DEdge&itr)const
{
return weight > itr.weight;
}
bool operator==(const DEdge&itr)const
{
return weight == itr.weight;
}
bool operator!=(const DEdge&itr)const
{
return weight != itr.weight;
}
bool operator<=(const DEdge&itr)const
{
return !(weight > itr.weight);
}
bool operator>=(const DEdge&itr)const
{
return !(weight < itr.weight);
}
double getweight(){ return weight; }
int from(){ return v; }
int to(){ return w; }
string tostring()
{
string s1, s2;
stringstream stream;
stream << v << "-" << w << ":";
stream >> s1;
stream.clear();
stream << weight << endl;
stream >> s2;
return s1 + s2;
}
};
#endif
2.加权有向图:
代码:
#ifndef EDGEDIGTAPH_H
#define EDGEDIGRAPH_H
#include<vector>
using std::vector;
#include"DEdge.h"
#include"Bag.h"
class EdgeDigraph
{
private:
int V;//顶点的总数
int E;//边的总数
vector<Bag<DEdge>> adj;//;邻接表
public:
EdgeDigraph(int V) :V(V), E(0), adj(V){}
EdgeDigraph(int V, int E) :V(V), E(0), adj(V)
{
int v, w;
double weight;
for (int i = 0; i < E; i++)
{
cout << "输入第" << i << "条边的两个顶点和权重:" << endl;
cin >> v >> w >> weight;
DEdge e(v, w, weight);
addEdge(e);
}
}
int getV(){ return V; }
int getE(){ return E; }
void addEdge(DEdge e)
{
adj[e.from()].add(e);
E++;
}
Bag<DEdge> getadj(int V){ return adj[V]; }
Bag<DEdge> edges()
{
Bag<DEdge> b;
for (int v = 0; v < V; v++)
for (auto e : adj[v])
b.add(e);
return b;
}
};
#endif
测试用例:
#include<iostream>
#include"EdgeDigraph.h"
using namespace std;
int main()
{
int n, m;
cout << "输入图的顶点数和边数:" << endl;
cin >> n >> m;
EdgeDigraph G(n, m);
Bag<DEdge> b=G.edges();
for (auto w : b)
cout << w.tostring() << endl;
cout << endl;
system("pause");
return 0;
}
最短路径算法:
1.最短路径需要的工具
1)edgeTo[ ]:最短路径树中的边;(每个结点都储存他来时的路径)
2)distTo[ ]:到达起点的距离(权重和);
3)约定:s为起点,edgeTo[s]=NULL,disTo[s]=0,且起点到不可达顶点为DBL_MAX;
4)松弛:放松边v->w意味着检查从s到w的最短路径是否是先从s到v,然后再由v到w。
点的松弛:
void relax(EdgeDigraph G, int v)
{
for (auto e : G.getadj(v))
{
int w = e.to();
if (distTo[w] > distTo[v] + e.getweight())//e这条边,是不是已知到w的边中最短的
{
distTo[w] = distTo[v] + e.getweight();
edgeTo[w] = e;
}
}
}
2.Dijkstra算法:(类似Prim)
与算法prim大至相似,只是prim每次都添加的都是离树最近的非树顶点,Dijkstra每次添加的都是里起点最近的非树顶点。他们都不需要marked[ ]数组,因为条件!marked[w]等价于distTo[w]为无穷大。
代码:
#ifndef DIJKSTRASP_H
#define DIJKSTRASP_H
#include<vector>
using std::vector;
#include"EdgeDigraph.h"
#include"DEdge.h"
#include"IndexMinPQ.h"
#include"stack.h"
class DijkstraSP
{
private:
vector<DEdge> edgeTo;//最短路径树的边
vector<double> distTo;//到底起点的距离
IndexMinPQ<double> pq;
int s;//起点
void relax(EdgeDigraph G, int v)
{
for (auto e : G.getadj(v))
{
int w = e.to();
if (distTo[w] > distTo[v] + e.getweight())//e这条边,是不是已知到w的边中最短的
{
distTo[w] = distTo[v] + e.getweight();
edgeTo[w] = e;
if (pq.contains(w))pq.change(w, distTo[w]);
else pq.insert(w, distTo[w]);
}
}
}
public:
DijkstraSP(EdgeDigraph G, int s) :edgeTo(G.getV()),
distTo(G.getV(), DBL_MAX), pq(G.getV()), s(s)
{
distTo[s] = 0.0;
pq.insert(s, 0.0);
while (!pq.Empty())
relax(G, pq.delMin());
}
double getdistTo(int v){ return distTo[v]; }//查询顶点到v的最小距离
bool hasPathTo(int v){ return distTo[v] < DBL_MAX; }//顶点与v是否可达
stack<DEdge> pathTo(int v)//输出到v的路径
{
stack<DEdge> path;
if (!hasPathTo(v)||v==s)return path;
DEdge e;
for ( e = edgeTo[v]; e.from() != s; e = edgeTo[e.from()])
path.push(e);
path.push(e);
return path;
}
};
#endif
测试用例:
#include<iostream>
using namespace std;
#include"DijkstraSP.h"
int main()
{
int n, m;
cout << "输入图的顶点数和边数:" << endl;
cin >> n >> m;
EdgeDigraph G(n, m);
DijkstraSP Sp(G, 0);
for (int t = 0; t < G.getV(); t++)
{
stack<DEdge> b = Sp.pathTo(t);
cout << 0 << " to " << t << "(" << Sp.getdistTo(t) << "): " << endl;
for (auto w : b)
cout << w.tostring() << endl;
cout << endl;
}
system("pause");
return 0;
}
3.无环加权有向图的最短路径算法
他是对于拓扑排序的扩展,有以下几个特点:
1)能够在线性时间内解决单点最短路径问题;
2)能够处理负权重问题;
3)能够解决相关问题,(最长路径)
具体步骤就是:先拓扑排序顶点,然后再按照顺序放松顶点。
在拓扑排序中需要使用到DepthFirstOther和DirectedCycle算法,因为边的数据结构有改动所以需要两个代码也需要变动:
#ifndef DC_H
#define DC_H
#include<vector>
using std::vector;
#include"stack.h"
class DirectedCycle
{
private:
vector<bool> marked;
vector<DEdge> edgeTo;//来时的路径
stack<DEdge> cycle;//有向环中的边
vector<bool> onstack;//递归调用所有栈上的顶点
public:
DirectedCycle(EdgeDigraph G) :
marked(G.getV()), edgeTo(G.getV()), onstack(G.getV())
{
for (int v = 0; v < G.getV(); v++)
if (!marked[v])dfs(G, v);
}
void dfs(EdgeDigraph G,int v)
{
onstack[v] = true;
marked[v] = true;
for (auto e : G.getadj(v))
{
int w = e.to();
if (hasCycle())return;
else if (!marked[w])
{
edgeTo[w] = e;//记录来时的路径
dfs(G, w);
}
else if (onstack[w])//在次出现该结点,说明有该结点的环
{//逆序追踪到上一次出现该结点的地点
while (e.from() != w)
{
cycle.push(e);
e = edgeTo[e.from()];
}
cycle.push(e);
}
}
onstack[v] = false;
}
bool hasCycle(){ return !cycle.isEmpty(); }
stack<DEdge> getcycle(){return cycle;}
};
#endif
#ifndef DFO_H
#define DFO_H
#include<vector>
using std::vector;
#include"stack.h"
#include"queue.h"
class DepthFirstOther
{
private:
vector<bool> marked;
queue<int> pre;//先序
queue<int> post;//后序
stack<int> reversepost;//逆后序
public:
DepthFirstOther(EdgeDigraph G) :marked(G.getV())
{
for (int v = 0; v < G.getV(); v++)
if(!marked[v])dfs(G, v);
}
void dfs(EdgeDigraph G, int v)
{
pre.enqueue(v);
marked[v] = true;
for (auto e : G.getadj(v))
{
int w = e.to();
if (!marked[w])dfs(G, w);
}
post.enqueue(v);
reversepost.push(v);
}
queue<int> getpre(){ return pre; }
queue<int> getpost(){ return post; }
stack<int> getreversepsot(){ return reversepost; }
};
#endif
代码:
#ifndef ACYCLICSP_H
#define ACYCLICSP_H
#include<vector>
using std::vector;
#include"Topological.h"
class AcyclicSP
{
private:
vector<DEdge> edgeTo;//来时的路径
vector<double> distTo;//到起点的距离
int s;//起点
void relax(EdgeDigraph G, int v)
{
for (auto e : G.getadj(v))
{
int w = e.to();
if (distTo[w] > distTo[v] + e.getweight())
{
distTo[w] = distTo[v] + e.getweight();
edgeTo[w] = e;
}
}
}
public:
AcyclicSP(EdgeDigraph G, int s) :edgeTo(G.getV()), distTo(G.getV(),DBL_MAX), s(s)
{
distTo[s] = 0.0;//初始化
Topological top(G);
for (int v : top.getorder())
relax(G, v);
}
double getdistTo(int v){ return distTo[v]; }
bool hasPathTo(int v){ return distTo[v] < DBL_MAX; }
stack<DEdge> pathTo(int v)//输出到v的路径
{
stack<DEdge> path;
if (!hasPathTo(v) || v == s)return path;
DEdge e;
for (e = edgeTo[v]; e.from() != s; e = edgeTo[e.from()])
path.push(e);
path.push(e);
return path;
}
};
#endif
测试用例:
#include<iostream>
using namespace std;
#include"EdgeDigraph.h"
#include"Acyclicsp.h"
//#include"DijkstraSP.h"
int main()
{
int n, m;
cout << "输入图的顶点数和边数:" << endl;
cin >> n >> m;
EdgeDigraph G(n, m);
AcyclicSP Sp(G, 0);
for (int t = 0; t < G.getV(); t++)
{
stack<DEdge> b = Sp.pathTo(t);
cout << 0 << " to " << t << "(" << Sp.getdistTo(t) << "): " << endl;
for (auto w : b)
cout << w.tostring() << endl;
cout << endl;
}
system("pause");
return 0;
}
而求最长路径就十分简单了,只需要把代码中DBL_MAX改为DBL_MIN,以及relax()中不等式的方向改变即可。
4.基于队列的Bellman-Ford算法(不存在负权重环)
一般加权有向图中的最短路径问题,也就是可能含有负权重和环的图。
负权重环:是一个总权重为负的有向环。
当且仅当加权有向图中至少存在一条从s到v的有向路径且所有从s到v的有向路径上的任意顶点都是不存在于任何负权重环中时,s到v的最短路径才是存在的。
也就是说我们需要在Dijkstra算法的基础上确定是否负权重环。
需要的数据结构:
1)一条用来保存即将被放松的顶点的队列queue;
2)一个由顶点索引的bool数组onQ[ ],用来指示顶点是否已经在队列中。
代码:
#ifndef BfSP_H
#define BFSP_H
#include<vector>
using std::vector;
#include"DEdge.h"
#include"queue.h"
#include"stack.h"
#include"DirectedCycle.h"
class BellmanFordSP
{
private:
vector<double> distTo;//到起点的距离
vector<DEdge> edgeTo;//来时的路径
vector<bool> onQ;//顶点是否存在于队列中
queue<int> queue;//正在被放松的顶点
int cost;//relax()的调用次数
stack<DEdge> cycle;//负权重环
int s;//起点
public:
BellmanFordSP(EdgeDigraph G, int s) :distTo(G.getV(),DBL_MAX),
edgeTo(G.getV()), onQ(G.getV()), s(s),cost(1)
{
distTo[s] = 0.0;
queue.enqueue(s);
onQ[s] = true;
while (!queue.Empty() && !hasNegativeCycle())//放松队列不为空且没有负权重环
{
int v = queue.dequeue();
onQ[v] = false;
relax(G, v);
}
}
double getdistTo(int v){ return distTo[v]; }
bool hasPathTo(int v){ return distTo[v] < DBL_MAX; }
stack<DEdge> pathTo(int v)//输出到v的路径
{
stack<DEdge> path;
if (!hasPathTo(v) || v == s)return path;
DEdge e;
for (e = edgeTo[v]; e.from() != s; e = edgeTo[e.from()])
path.push(e);
path.push(e);
return path;
}
private:
void relax(EdgeDigraph G, int v)//放松
{
for (auto e : G.getadj(v))
{
int w = e.to();
if (distTo[w] > distTo[v] + e.getweight())
{
distTo[w] = distTo[v] + e.getweight();
edgeTo[w] = e;
cout << edgeTo[w].tostring() << endl;
if (!onQ[w])
{
queue.enqueue(w);
onQ[w] = true;
}
if (cost++ %G.getV() == 0)findNegativeCycle();//当cost走过一轮后,查看路径中是否有负权重环
}
}
}
void findNegativeCycle()//检测路径中是否有负权重环
{
cout << "1" << endl;
int V = edgeTo.size();
EdgeDigraph spt(V);
for (int v = 0; v < V; v++)
if (v != s)spt.addEdge(edgeTo[v]);
DirectedCycle cy(spt);//检测存储路径中是否有环
double ws=0;
cout << "3" << endl;
for (auto e : cy.getcycle())
ws += e.getweight();//计算环的权重
if (ws < 0)cycle = cy.getcycle();
cout << "4" << endl;
}
public:
bool hasNegativeCycle(){ return !cycle.isEmpty(); }
stack<DEdge> negativeCycle(){ return cycle; }
};
#endif
测试用例:
#include<iostream>
using namespace std;
#include"EdgeDigraph.h"
#include"BellmanFordSP.h"
int main()
{
int n, m;
cout << "输入图的顶点数和边数:" << endl;
cin >> n >> m;
EdgeDigraph G(n, m);
BellmanFordSP Sp(G, 0);
if (Sp.hasNegativeCycle())cout << "存在负权重环" << endl;
cout << "不存在负权重环" << endl;
for (int t = 0; t < G.getV(); t++)
{
stack<DEdge> b = Sp.pathTo(t);
cout << 0 << " to " << t << "(" << Sp.getdistTo(t) << "): " << endl;
for (auto w : b)
cout << w.tostring() << endl;
cout << endl;
}
system("pause");
return 0;
}
比较三种算法
算法 | 局限 | 比较次数(一般) | 比较次数(最坏) | 所需空间 | 优势 |
Dijkstra(即时) | 边的权重必须为正 | ElogV | ElogV | V | 最坏情况下性能也较好 |
拓扑排序 | 只适用于无环加权图 | E+V | E+V | V | 是无环图的最优算法 |
Bellman-Fond | 不能存在负权重环 | E+V | VE | V | 适用领域广泛 |