在學習圖論的過程中,最短論問題是比較常見且又具有代表性的一類問題。最短路是給定兩個定點,在以這兩個點作爲起點和終點的路徑中,邊的權值和最小的路徑。在實際生活中,最常見的最短路問題,就是在地圖導航上應用。比如我們把權值作爲距離,那麼我們就可以求得A到B的最短路徑。如果時間作爲權值,那麼我們就可以得到A到B的最短時間。
1、Bellman-Ford算法
單源最短路問題就是將起點固定,求該起點到其他所有點的最短路問題。貝爾曼-福特算法(Bellman-Ford)是由理查德·貝爾曼(Richard Bellman) 和 萊斯特·福特(Lester Ford) 創立的,求解單源最短路徑問題的一種算法。有時候這種算法也被稱爲 Moore-Bellman-Ford 算法,因爲 Edward F. Moore 也爲這個算法的發展做出了貢獻。它的原理是對圖進行|V|-1次鬆弛操作,得到所有可能的最短路徑。其優於Dijkstra算法的方面是邊的權值可以爲負數、實現簡單,缺點是時間複雜度過高,高達O(VE)。但算法可以進行若干種優化,提高了效率。
①算法描述
設dist[v]表示從源點s到v的最短路徑長度。對於與v相鄰的任意頂點u,dist[v]滿足三角不等式:
dist[v] ≤ dist[u] + w(u, v), (其中w(u,v)爲邊(u, v)的權值)
我們設d[v]爲s到v的最短路權值上界(可能爲無窮大,即不連通),稱爲最短路估計。
如果 d[v] > d[u] + w(u, v),即說明d[v]還可以變得更小。於是我們就使 d[v] = d[u] + w(u, v),我們稱這個操作爲鬆弛操作。
顯然每次通過鬆弛操作我們都可以使得d[v]減小,直到d[v]的值不在變化(即當d[v]等於dist[v])。
我們容易知道,在一個沒有負權環的圖中。每一個頂點至多與其它|V|-1個頂點進行鬆弛操作,若大於|V|-1,則必然存在負權環。
對於圖G(V,E),下面給出Bellman-Ford的算法流程:
輸入:圖G和起點S
輸出:s到每一個點的最短路徑,以及圖中是否存在負權環
具體流程:
1、初始化d數組,d[s] = 0, d[i] = ∞ (i ≠ s)
2、枚舉每一條邊,進行鬆弛操作
3、將操作2重複執行|V|-1次
4、枚舉每一條邊,看是否能夠進行鬆弛操作,若能,這說明原圖存在負權環
②時間複雜度
對於Bellman-Ford,由於每次操作需要枚舉|E|條邊,總共需要重複|V|-1次操作,則我們容易得出其時間複雜度爲O(VE)。如果我們使用隊列進行優化,則時間複雜度可下降爲O(kE),k是個比較小的係數(並且在絕大多數的圖中,k<=2,然而在一些精心構造的圖中可能會上升到很高)。
③代碼實現
<1>未優化版
#include <cstdio>
#include <cstring>
#define INF 0xfffffff
#define MAXN (100 + 10)
using namespace std;
struct edge{
int from, to;
edge(int f = 0, int t = 0)
: from(f), to(t){}
};
edge es[MAXN*MAXN];
int cost[MAXN];
bool graph[MAXN][MAXN];
int d[MAXN];
//判斷圖是否聯通
void Floyd(int n){
for(int i = 1; i <= n; i++){
for(int k = 1; k <= n; k++){
for(int j = 1; j <= n; j++){
if(!graph[i][j])
graph[i][j] = graph[i][k] && graph[k][j];
}
}
}
}
bool bellman_ford(int s, int V, int E){
for(int i = 0; i <= V; i++)
d[i] = -INF;
d[s] = 100;
//重複對每一條邊進行鬆弛操作
for(int k = 0; k < V-1; k++){
for(int i = 0; i < E; i++){
edge e = es[i];
//鬆弛操作
if(d[e.to] < d[e.from] + cost[e.to] && d[e.from] + cost[e.to] > 0){
d[e.to] = d[e.from] + cost[e.to];
}
}
}
//檢查負權環
for(int i = 0; i < E; i++){
edge e = es[i];
if(d[e.to] < d[e.from] + cost[e.to] && graph[e.to][V] && d[e.from] + cost[e.to] > 0)
return true;
}
return d[V] > 0;
}
int main(){
int n, m, cnt, vex;
while(scanf("%d", &n), n != -1){
memset(graph, false, sizeof(graph));
cnt = 0;
for(int i = 1; i <= n; i++){
scanf("%d%d", &cost[i], &m);
for(int j = 0; j < m; j++){
scanf("%d", &vex);
es[cnt++] = edge(i, vex);
graph[i][vex] = true;
}
}
Floyd(n);
if(!graph[1][n] || !bellman_ford(1, n, cnt)){
printf("hopeless\n");
}
else{
printf("winnable\n");
}
}
return 0;
}
<2>隊列優化SPFA
單源最短路的SPFA算法的全稱是:Shortest Path Faster Algorithm。 SPFA算法是西南交通大學段凡丁於1994年發表的。鬆弛操作必定只會發生在最短路徑前導節點鬆弛成功過的節點上,用一個隊列記錄鬆弛過的節點,可以避免了冗餘計算。我們還是以hdu1317xyzzy爲例,代碼如下:
#include <cstdio>
#include <cstring>
#include <queue>
#define MAXN (100 + 10)
using namespace std;
//d表示s到各點的所經過路徑的權值之和
//cost表示各點的權值
//cnt表示進入隊列的次數
int d[MAXN], cost[MAXN], cnt[MAXN];
//reach表示兩點之間是否聯通,即可達
//graph記錄兩點之間是否有邊
bool reach[MAXN][MAXN], graph[MAXN][MAXN];
void Init(){
memset(d, 0, sizeof(d));
memset(cnt, 0, sizeof(cnt));
memset(graph, false, sizeof(graph));
memset(reach, false, sizeof(reach));
}
//判斷圖是否聯通
void Floyd(int n){
for(int i = 1; i <= n; i++){
for(int k = 1; k <= n; k++){
for(int j = 1; j <= n; j++){
if(!reach[i][j])
reach[i][j] = reach[i][k] && reach[k][j];
}
}
}
}
bool SPFA(int s, int n){
queue<int> Q;
d[s] = 100;
Q.push(s);
while(!Q.empty()){
int now = Q.front();
Q.pop();
cnt[now]++;
//如果不存在負權環(PS:在本題中爲正權環),即每個點進入隊列的次數至多爲n-1
//若大於n-1,即表明必然存在負權環
if(cnt[now] >= n) return reach[now][n];
//依次枚舉每條邊
for(int next = 1; next <= n; next++){
if(graph[now][next] && d[now] + cost[next] > d[next] && d[now] + cost[next] > 0){
Q.push(next);
d[next] = d[now] + cost[next];
}
}
}
return d[n] > 0;
}
int main(){
int n, m, vex;
while(scanf("%d", &n), n != -1){
Init();
for(int i = 1; i <= n; i++){
scanf("%d%d", &cost[i], &m);
for(int j = 0; j < m; j++){
scanf("%d", &vex);
reach[i][vex] = true;
graph[i][vex] = true;
}
}
Floyd(n);
if(!reach[1][n] || !SPFA(1, n)){
printf("hopeless\n");
}
else{
printf("winnable\n");
}
}
return 0;
}
2、Dijkstra算法
我們容易發現,如果圖中沒有負邊的情況。在Bellman-Ford算法中,如果d[u]還不是最短距離的話,那麼即便我們進行了鬆弛操作,那麼d[v]也不會變爲最短距離。而且即便d[v]沒有變化,那麼他還是需要檢查一次所有的邊。顯然這些操作很浪費時間,於是乎,我們就提出了以下改進:
(1)從最短距離已經確定的頂點出發更新與之相鄰頂點的最短距離。
(2)對於最短距離已經確定的頂點,我們直接無視。
通過這樣的修改我們就得到了Dijkstra算法。Dijkstra算法是用來解決只含非負權值邊的圖的單源最短路問題。換而言之,Dijkstra無法處理含有負權邊的圖。
①算法描述
對於圖G(V,E),下面給出Dijkstra的算法流程:
輸入:圖G和起點S
輸出:s到每一個點的最短路徑
具體流程:
1、初始化d數組,d[s] = 0, d[i] = ∞ (i ≠ s)
2、設置所有點未訪問過(即設置一個標記數組,並將其置空)
3、找出所有未訪問過的點中距離值最小的點,將其標記爲訪問過
4、對3中找到的點的相鄰邊進行鬆弛操作
5、重複3和4直到所有點都訪問過
下邊給出一幅圖來模擬Dijkstra的過程:
②時間複雜度
因爲操作更新|V|次,每次操作需要找最小值,掃描一個點連接的所有邊,如果我們使用堆來實現尋找和維護,則時間複雜度爲O( (|E|+|V|) log|V| )。若只用普通的方法掃描,時間複雜度爲O(|V|² + |E|)。
③代碼實現
我們以hdu 1874暢通工程續 來說明Dijkstra算法:
<1>未優化版
#include <cstdio>
#include <vector>
#include <algorithm>
#define MAXN 200 + 10
#define INF 0xffffff
using namespace std;
struct Vex{
int v, weight;
Vex(int tv, int tw):v(tv), weight(tw){}
};
//graph用來記錄圖的信息
vector<Vex> graph[MAXN];
//判斷是否已經找到最短路
bool inTree[MAXN];
//源點s到各頂點最短路的值
int mindist[MAXN];
//初始化
void Init(int n){
for(int i = 0; i < n; i++){
inTree[i] = false;
graph[i].clear();
mindist[i] = INF;
}
}
//s表示源點,t表示終點,n表示頂點數目
int Dijkstra(int s, int t, int n){
int tempMin, tempVex, addNode;
//初始化s
mindist[s] = 0;
//將源點s標記爲訪問過
inTree[s] = true;
//題目中可能有重邊,我們去除重邊
for(unsigned int i = 0; i < graph[s].size(); i++)
mindist[graph[s][i].v] = min(mindist[graph[s][i].v], graph[s][i].weight);
//從剩下的n-1個點逐個枚舉
for(int nNode = 1; nNode <= n-1; nNode++){
tempMin = INF;
//尋找所有未訪問過點中,有最小距離的點
for(int i = 0; i < n; i++){
if(!inTree[i] && mindist[i] < tempMin){
tempMin = mindist[i];
addNode = i;
}
}
//將該點標記爲訪問過
inTree[addNode] = true;
//將與該點相鄰的點進行鬆弛操作
for(unsigned int i = 0; i < graph[addNode].size(); i++){
tempVex = graph[addNode][i].v;
if(!inTree[tempVex] && tempMin + graph[addNode][i].weight < mindist[tempVex]){
mindist[tempVex] = tempMin + graph[addNode][i].weight;
}
}
}
return mindist[t];
}
int main(){
int n, m;
int v1, v2, x, s, t;
while(scanf("%d%d", &n, &m) != EOF){
Init(n);
for(int i = 0; i < m; i++){
scanf("%d%d%d", &v1, &v2, &x);
graph[v1].push_back(Vex(v2, x));
graph[v2].push_back(Vex(v1, x));
}
scanf("%d%d", &s, &t);
int ans = Dijkstra(s, t, n);
if(ans == INF)
printf("-1\n");
else
printf("%d\n", ans);
}
return 0;
}
<2>堆優化版
#include <cstdio>
#include <vector>
#include <queue>
#include <algorithm>
#define MAXN 200 + 10
#define INF 0xffffff
using namespace std;
struct edge{
int to, cost;
};
typedef pair<int, int> P;
vector<edge> graph[MAXN];
int mindist[MAXN];
void Init(int n){
for(int i = 0; i < n; i++){
graph[i].clear();
mindist[i] = INF;
}
}
int Dijkstra_heap(int s, int t, int n){
//pair的first存放s->v的距離
//second存放頂點v
priority_queue<P, vector<P>, greater<P> > Q;
//初始化源點s的信息
mindist[s] = 0;
Q.push(P(0, s));
while(!Q.empty()){
//每次從堆中取出最小值
P p = Q.top(); Q.pop();
int v = p.second;
//當取出的值不是當前最短距離的話,就丟棄這個值
if(mindist[v] < p.first) continue;
//將與其相鄰的點,進行鬆弛操作
for(unsigned int i = 0; i < graph[v].size(); i++){
edge e = graph[v][i];
if(mindist[e.to] > mindist[v] + e.cost){
mindist[e.to] = mindist[v] + e.cost;
//將滿足條件的點重新加入堆中
Q.push(P(mindist[e.to], e.to));
}
}
}
return mindist[t];
}
int main()
{
int n, m;
int v1, v2, x, s, t;
while(scanf("%d%d", &n, &m) != EOF){
Init(n);
for(int i = 0; i < m; i++){
scanf("%d%d%d", &v1, &v2, &x);
graph[v1].push_back({v2, x});
graph[v2].push_back({v1, x});
}
scanf("%d%d", &s, &t);
int ans = Dijkstra_heap(s, t, n);
if(ans == INF)
printf("-1\n");
else
printf("%d\n", ans);
}
return 0;
}
PS:如果不懂優先隊列的,請移步:傳送門
PPS:在附贈一個大禮包,圖論500題