最短路徑
最短路徑是在加權有向圖中,找到從一個頂點到達另一個頂點的成本最小的路徑
1.加權有向圖的數據結構

//加權有向邊
class DirectedEdge
{
private:
int vertax_from; //邊的起點
int vertax_to; //邊的終點
double weight; //邊的權值
public:
DirectedEdge(int v = 0, int w = 0, double weight = 0.0)
: vertax_from(v), vertax_to(w), weight(weight) {}
//返回邊的起點
int from() const { return vertax_from; }
//返回邊的終點
int to() const { return vertax_to; }
//返回邊的權值
double getWeight() const { return weight; }
//字符串輸出加權有向邊
string toString()
{
string s = "[ " + to_string(vertax_from) + "->" + to_string(vertax_to) + " , weight: " + to_string(weight) + " ]";
return s;
}
//重載運算符
friend bool operator<(const DirectedEdge &a, const DirectedEdge &b)
{
return a.getWeight() < b.getWeight();
}
friend bool operator>(const DirectedEdge &a, const DirectedEdge &b)
{
return a.getWeight() < b.getWeight();
}
friend bool operator==(const DirectedEdge &a, const DirectedEdge &b)
{
return a.getWeight() == b.getWeight() && a.from() == b.from() && a.to() == b.to();
}
friend bool operator!=(const DirectedEdge &a, const DirectedEdge &b)
{
return !(a.getWeight() == b.getWeight() && a.from() == b.from() && a.to() == b.to());
}
};

代碼實現:
/加權有向圖
class EdgeWeightDigraph
{
private:
int vertax; //頂點數
int edge; //邊數
vector<list<DirectedEdge>> adjList; //鄰接表
vector<DirectedEdge> edges; //所有有向邊
public:
//創建V個頂點的空圖
EdgeWeightDigraph(int V)
{
vertax = V;
edge = 0;
adjList.resize(vertax, list<DirectedEdge>());
}
//從文件讀入加權有向圖
EdgeWeightDigraph(string in)
{
ifstream file(in);
if (!file)
{
printf("can't open this file.\n");
return;
}
string ch;
int i = 0, e = 0;
while (getline(file, ch))
{
istringstream iss(ch);
if (i == 0)
{
iss >> vertax;
edge = 0;
adjList.resize(vertax, list<DirectedEdge>());
}
else if (i == 1)
iss >> e;
else if (i < e + 2)
{
int v, w;
double weight;
iss >> v >> w >> weight;
addEdge(DirectedEdge(v, w, weight));
}
else
break;
i++;
}
file.close();
}
//添加一條加權有向邊
void addEdge(DirectedEdge e)
{
adjList[e.from()].push_back(e);
edges.push_back(e);
edge++;
}
//返回頂點數
int V() { return vertax; }
//返回邊數
int E() { return edge; }
//返回v指出的邊
list<DirectedEdge> &adj(int v)
{
return adjList[v];
}
//返回所有的邊
vector<DirectedEdge> &getEdges()
{
return edges;
}
//字符串輸出加權有向圖
string toString()
{
string s = to_string(vertax) + " vertices, " + to_string(edge) + " edges.\n";
for (int v = 0; v < vertax; v++)
{
s += to_string(v) + ": ";
for (auto e : adjList[v])
s += e.toString();
s += '\n';
}
return s;
}
};
2.Floyd算法
Floyd算法是解決圖中所有點到所有點的最短路徑的一種方法,核心思想是在兩個頂點之間插入一個或一個以上的中轉點,比較經過與不經過中轉點的距離哪個更短。
代碼也十分簡單,對於矩陣map[n][n]
for(int k = 0; i < n; k++)
for(int i = 0; i < n; i++)
for(int j = 0; j < n; j++)
if(map[i][j] > map[i][k]+map[k][j])
map[i][j] = map[i][k]+map[k][j];
3.Dijkstra算法
Dijkstra算法是從未求出最短路徑的頂點中,選出距離源點最近的一個頂點,將其的出邊遍歷一遍,更新其他未求出最短路徑的點的距離,不斷重復這一過程。
要實現Dijkstra算法,我們需要將distTo[s]初始化為0(源點),其他元素初始化為正無窮。然后將源點的出邊遍歷,將更新的新頂點放入優先隊列,然后再更新優先隊列的頂點。直到所有可達頂點都更新過。
代碼實現:
//Dijkstra算法
class Dijkstra
{
private:
vector<DirectedEdge> edgeTo; //到各點的最短路徑
vector<double> distTo; //到各點的最短路徑的權值
map<int, double> pq; //優先隊列
int getMin() //優先隊列的獲取最小值操作
{
double minWeight = numeric_limits<double>::max();
int minKey = 0;
for (auto v : pq)
{
if (v.second < minWeight)
{
minWeight = v.second;
minKey = v.first;
}
}
return minKey;
}
void del(int v) //優先隊列的刪除操作
{
auto it = pq.find(v);
pq.erase(it);
}
public:
Dijkstra(EdgeWeightDigraph G, int s)
{
edgeTo.resize(G.V());
distTo.resize(G.V(), numeric_limits<double>::max());
distTo[s] = 0.0;
pq[s] = distTo[s];
while (!pq.empty())
{
int v = getMin();
del(v);
relax(G, v);
}
}
//更新最短路徑
void relax(EdgeWeightDigraph G, int v)
{
for (auto e : G.adj(v))
{
int w = e.to();
if (distTo[w] > distTo[v] + e.getWeight())
{
distTo[w] = distTo[v] + e.getWeight();
edgeTo[w] = e;
pq[w] = distTo[w];
}
}
}
//獲取到v最短路徑權值的接口
double getDistTo(int v)
{
return distTo[v];
}
//獲取v點可達路徑的接口
bool hasPathTo(int v)
{
return distTo[v] < numeric_limits<double>::max();
}
//獲取到達v的路徑
vector<DirectedEdge> pathTo(int v)
{
vector<DirectedEdge> p;
if (!hasPathTo(v))
return p;
stack<DirectedEdge> pathTemp;
for (DirectedEdge e = edgeTo[v]; e != DirectedEdge(0, 0, 0.0); e = edgeTo[e.from()])
pathTemp.push(e);
while (!pathTemp.empty())
{
auto e = pathTemp.top();
pathTemp.pop();
p.push_back(e);
}
return p;
}
};
4.拓撲排序
對於無環的加權有向圖來說,有一種比Dijkstra算法更快、更簡單的方法。只要將頂點按照拓撲排序的順序更新最短路徑的信息就可以獲取源點到所有點的最短路徑。
代碼實現:
//拓撲排序
class Topologic
{
private:
vector<int> indegree; //入度數組
vector<int> order; //拓撲排序的頂點順序
public:
Topologic(EdgeWeightDigraph G)
{
//初始化入度數組
indegree.resize(G.V(), 0);
for (int v = 0; v < G.V(); v++)
for (auto e : G.adj(v))
indegree[e.to()]++;
queue<int> q;
//將入度為0的頂點放入隊列
for (int v = 0; v < G.V(); v++)
if (indegree[v] == 0)
q.push(v);
int count = 0;
//刪除隊列的結點,將它指向的結點入度-1
//將入度減少到0的頂點放入隊列
while (!q.empty())
{
int v = q.front();
q.pop();
order.push_back(v);
for (auto w : G.adj(v))
if (!(--indegree[w.to()]))
q.push(w.to());
}
}
//獲取拓撲排序的接口
vector<int> &getOrder()
{
return order;
}
};
//無環加權有向圖的最短路徑
class AcyclicSP
{
private:
vector<DirectedEdge> edgeTo; //到各點的最短路徑
vector<double> distTo; //到各點的最短距離
public:
AcyclicSP(EdgeWeightDigraph G, int s)
{
edgeTo.resize(G.V());
distTo.resize(G.V(), numeric_limits<double>::max());
distTo[s] = 0.0;
Topologic topo(G); //拓撲排序
for (int v : topo.getOrder()) //按拓撲排序更新最短路徑信息
relax(G, v);
}
void relax(EdgeWeightDigraph G, int v) //更新結點最短路徑信息
{
for (auto e : G.adj(v))
{
int w = e.to();
if (distTo[w] > distTo[v] + e.getWeight())
{
distTo[w] = distTo[v] + e.getWeight();
edgeTo[w] = e;
}
}
}
//獲取到v最短路徑權值的接口
double getDistTo(int v)
{
return distTo[v];
}
//獲取v點可達路徑的接口
bool hasPathTo(int v)
{
return distTo[v] < numeric_limits<double>::max();
}
//獲取到達v的路徑
vector<DirectedEdge> pathTo(int v)
{
vector<DirectedEdge> p;
if (!hasPathTo(v))
return p;
stack<DirectedEdge> pathTemp;
for (DirectedEdge e = edgeTo[v]; e != DirectedEdge(0, 0, 0.0); e = edgeTo[e.from()])
pathTemp.push(e);
while (!pathTemp.empty())
{
auto e = pathTemp.top();
pathTemp.pop();
p.push_back(e);
}
return p;
}
};
5.Bellman-Ford算法
Bellman-Ford算法可以解決除了含有負權重環的加權有向圖。對於含有負權重環的圖,討論單點最短路徑是沒有意義的。
Bellman-Ford是將distTo[s]初始化為0,其他distTo元素初始化為無窮大,按任意順序更新頂點最短路徑,重復V遍。
代碼實現:
//尋找加權有向環
class EdgeWeightCycleFinder
{
private:
vector<bool> marked; //標記
vector<int> edgeTo;
vector<bool> onStack; //s是否在棧中
stack<int> cycle; //環
public:
EdgeWeightCycleFinder(EdgeWeightDigraph G)
{
marked.resize(G.V(), false);
onStack.resize(G.V(), false);
edgeTo.resize(G.V(), 0);
for (int v = 0; v < G.V(); v++)
if (!marked[v] && cycle.empty())
dfs(G, v);
}
void dfs(EdgeWeightDigraph G, int v)
{
marked[v] = true;
onStack[v] = true;
for (auto e : G.adj(v))
{
int w = e.to();
if (!cycle.empty())
return;
else if (!marked[w])
{
edgeTo[w] = v;
dfs(G, w);
}
else if (onStack[w])
{
for (int x = v; x != w; x = edgeTo[x])
cycle.push(x);
cycle.push(w);
cycle.push(v);
}
}
onStack[v] = false;
}
bool hasCycle()
{
return !cycle.empty();
}
vector<int> cyclePath()
{
vector<int> path;
while (!cycle.empty())
{
path.push_back(cycle.top());
cycle.pop();
}
return path;
}
};
//Bellman-Ford算法
class BellmanFord
{
private:
vector<double> distTo; //最短路徑的長度
vector<DirectedEdge> edgeTo; //各點的最短路徑
vector<bool> onQ; //頂點是否在隊列
queue<int> q;
int cost; //更新函數調用次數
vector<int> cycle; //負權重環
public:
BellmanFord(EdgeWeightDigraph G, int s)
{
//初始化
distTo.resize(G.V(), numeric_limits<double>::max());
edgeTo.resize(G.V());
onQ.resize(G.V(), false);
cost = 0;
//將源點放入隊列
distTo[s] = 0.0;
q.push(s);
onQ[s] = true;
//按任意順序更新
while (!q.empty() && !hasNegativeCycle())
{
int v = q.front();
q.pop();
onQ[v] = false;
relax(G, v);
}
}
//更新最短路徑信息
void relax(EdgeWeightDigraph G, int v)
{
for (auto e : G.adj(v))
{
int w = e.to();
if (distTo[w] > distTo[v] + e.getWeight())
{
distTo[w] = distTo[v] + e.getWeight();
edgeTo[w] = e;
if (!onQ[w])
{
q.push(w);
onQ[w] = true;
}
}
if (cost++ % G.V() == 0)
findNegativeCycle();
}
}
//獲取到v最短路徑權值的接口
double getDistTo(int v)
{
return distTo[v];
}
//獲取v點可達路徑的接口
bool hasPathTo(int v)
{
return distTo[v] < numeric_limits<double>::max();
}
//獲取到達v的路徑
vector<DirectedEdge> pathTo(int v)
{
vector<DirectedEdge> p;
if (!hasPathTo(v))
return p;
stack<DirectedEdge> pathTemp;
for (DirectedEdge e = edgeTo[v]; e != DirectedEdge(0, 0, 0.0); e = edgeTo[e.from()])
pathTemp.push(e);
while (!pathTemp.empty())
{
auto e = pathTemp.top();
pathTemp.pop();
p.push_back(e);
}
return p;
}
//尋找負權重環
void findNegativeCycle()
{
int V = edgeTo.size();
EdgeWeightDigraph spt(V);
for (int v = 0; v < V; v++)
if (edgeTo[v] != DirectedEdge(0, 0, 0.0))
spt.addEdge(edgeTo[v]);
EdgeWeightCycleFinder cf(spt);
cycle = cf.cyclePath();
}
//是否存在負權重環
bool hasNegativeCycle()
{
return !cycle.empty();
}
//負權重環的路徑
vector<int> &negativeCycle()
{
return cycle;
}
};
6.各個算法比較
| 算法 | 局限 | 時間復雜度 | 空間復雜度 |
|---|---|---|---|
| Floyd算法 | 開銷較大 | \(V^3\) | \(V^2\) |
| Dijkstra算法 | 邊的權值必須為正 | \(ElogV\) | \(V\) |
| 拓撲排序 | 只適用於無環加權有向圖 | \(E+V\) | \(V\) |
| Bellman-Ford算法 | 不能存在負權重環 | \(E+V\)~\(EV\) | \(V\) |
