天天看點

SPFA算法學習筆記

一.理論準備

        為了學習網絡流,先水一道spfa。

        SPFA算法是1994年西南交通大學段凡丁提出,隻要最短路徑存在,SPFA算法必定能求出最小值,SPFA對Bellman-Ford算法優化的關鍵之處在于意識到:隻有那些在前一遍松弛中改變了距離估計值的點,才可能引起他們的鄰接點的距離估計值的改變。為什麼隊列為空就不改變了呢?就是因為要到下一點必須經過它的前一個鄰接點。。SPFA可以處理負權邊。很多時候,給定的圖存在負權邊,這時類似Dijkstra等算法便沒有了用武之地,而Bellman-Ford算法的複雜度又過高,SPFA算法便派上用場了。簡潔起見,我們約定有向權重圖G不存在負權回路,即最短路徑一定存在。當然,我們可以在執行該算法前做一次拓撲排序,以判斷是否存在負權回路。

        初始化: dis數組全部指派為Inf(無窮大,不能是map[s][i]),path數組全部指派為s(即源點),或者指派為-1,表示還沒有知道前驅,然後dis[s]=0;  表示源點不用求最短路徑,或者說最短路就是0。将源點入隊;另外記住在整個算法中有頂點入隊了要記得标記vis數組,有頂點出隊了記得消除那個标記(可能多次入隊)。

        核心:讀取隊頭頂點u,并将隊頭頂點u出隊(記得消除标記);将與點u相連的所有點v進行松弛操作,如果能更新估計值(即令d[v]變小),那麼就更新,另外,如果點v沒有在隊列中,那麼要将點v入隊(記得标記),如果已經在隊列中了,那麼就不用入隊以此循環,直到隊空為止就完成了單源最短路的求解。

        判斷有無負環:如果某個點進入隊列的次數超過N次則存在負環(SPFA無法處理帶負環的圖),假設這個節點的入度是k(無向權則就是這個節點的連接配接的邊)如果進入這個隊列超過k,說明必然有某個邊重複了,即成環;換一種思路:用DFS,假設存在負環a1->a2->…->an->a1。那麼當從a1深搜下去時又遇到了a1,那麼直接可以判斷負環了所有用。當某個節點n次進入隊列,則存在負環,此時時間複雜度為O(n*m),n為節點,m為邊。

        SPFA算法有兩個優化算法 SLF 和 LLL: SLF:Small Label First 政策,設要加入的節點是j,隊首元素為i,若dist(j)<dist(i),則将j插入隊首,否則插入隊尾。 LLL:Large Label Last 政策,設隊首元素為i,隊列中所有dist值的平均值為x,若dist(i)>x則将i插入到隊尾,查找下一進制素,直到找到某一i使得dist(i)<=x,則将i出對進行松弛操作。 SLF 可使速度提高 15 ~ 20%;SLF + LLL 可提高約 50%。 在實際的應用中SPFA的算法時間效率不是很穩定,為了避免最壞情況的出現,通常使用效率更加穩定的Dijkstra算法。個人覺得LLL優化每次要求平均值,不太好,為了簡單,我們可以之間用c++STL裡面的優先隊列來進行SLF優化。

二.算法實作

        直接去把HDU1874AC了吧。

1: import java.util.Comparator;      
2: import java.util.PriorityQueue;      
3: import java.util.Queue;      
4: import java.util.Scanner;      
5: /*      
6:  * 原來一直wa,重寫了一遍,AC了      
7:  * SLF優化      
8:  */      
9: public class HD1874 {      
10:      
11:   static int n,m;      
12:   static int[][] map  = new int[205][205];      
13:   static int[] dis  = new int[205];      
14:   static boolean[] vis  = new boolean[205];      
15:   /*      
16:    * 路徑最大值是10000,不能設定成10005就行,還要考慮和      
17:    * 也不能是整形最大值,否則一加就溢出了      
18:    */      
19:   static final int Inf = 0x3f3f3f3f;      
20:      
21:   public static void main(String[] args) {      
22:     Scanner sc = new Scanner(System.in);      
23:     // 記錄前驅點 。若path[i]=j,表示從s到i的最短路徑中i的前一個點是j      
24:     //int[] path;      
25:     int u,v,w;      
26:     while(sc.hasNext()) {      
27:       n = sc.nextInt();      
28:       m = sc.nextInt();      
29:       for(int i=0; i<205; i++) {      
30:         for(int j=i; j<205; j++) {      
31:           map[i][j] = Inf;      
32:           map[j][i] = Inf;      
33:         }      
34:       }      
35:       for(int i=0; i<m; i++) {      
36:         u = sc.nextInt();      
37:         v = sc.nextInt();      
38:         w = sc.nextInt();      
39:         //多重邊      
40:         if(map[u][v]>w) {      
41:           map[v][u] = w;      
42:           map[u][v] = w;      
43:         }      
44:       }      
45:       int s = sc.nextInt();      
46:       int t = sc.nextInt();      
47:       spfa(s);      
48:       //題目上有st<n,是以不必判斷dis[t]是否越界      
49:       //起點終點相同的話答案是0      
50:       if(Inf==dis[t]) {      
51:         System.out.println(-1);      
52:       }else {      
53:         System.out.println(dis[t]);      
54:       }      
55:     }      
56:   }      
57:      
58:   private static void spfa(int s) {      
59:      
60:     for(int i=0; i<205; i++) {      
61:       vis[i] = false;      
62:       //初始化為map[s][i]第一組資料就錯了      
63:       dis[i] = Inf;      
64:     }      
65:     dis[s] = 0;      
66:     vis[s] = true;      
67:     Comparator<Integer> cmp = new Comparator<Integer>() {      
68:      
69:           public int compare(Integer o1, Integer o2) {      
70:             int i = (int)o1;      
71:             int j = (int)02;      
72:             if(dis[i]>dis[j]) {      
73:               return 1;      
74:             }else if(dis[i]==dis[j]){      
75:               return 0;      
76:             }else {      
77:               return -1;      
78:             }      
79:           }      
80:     };      
81:     //面向接口程式設計;205代表優先隊列(是類)的容量      
82:     Queue<Integer> q = new PriorityQueue<Integer>(205, cmp);      
83:     q.clear();      
84:     q.offer(s);      
85:     while(!q.isEmpty()) {      
86:       int head = q.poll();      
87:       //該注意的是有些點可能重複入隊,是以出隊的點也要重新置未标記      
88:       vis[head] = false;      
89:       for(int i=0; i<n; i++) {      
90:         //dis[head]不可能是INF,map[head][i]可能是INF      
91:         int temp = dis[head] + map[head][i];      
92:         if(temp<dis[i]) {      
93:           //path[i] = head      
94:           dis[i] = temp;      
95:           if(!vis[i]) {      
96:             //用一個數組在此記錄入隊次數,大于n就存在負環;如何事先判斷      
97:             q.offer(i);      
98:             vis[i] = true;      
99:           }      
100:         }      
101:       }      
102:     }      
103:   }      
104:      
105: }      
106: