ACM北大暑期課培訓第六天
今天講了DFA,最小生成樹以及最短路
DFA(接著昨天講)
如何高效的構造前綴指針:
步驟為:根據深度一一求出每一個節點的前綴指針。對於當前節點,設他的父節點與他的邊上的字符為Ch,如果他的父節點的前綴指針所指向的節點的兒子中,有通過Ch字符指向的兒子,那麽當前節點的前綴指針指向該兒子節點,否則通過當前節點的父節點的前綴指針所指向點的前綴指針,繼續向上查找,直到到達根節點為止。
ps:構造前綴指針時在最前面加一個0號節點。
對於一個插入了n個模式串的單詞 前綴樹構造其前綴指針的時間復雜 度為:O(∑len(i)) (i=1..n)
如何在建立好的Trie圖上遍歷
遍歷的方法如下:從ROOT出發,按照當前串的下一 個字符ch來進行在樹上的移動。若當前點P不存在通過ch連接的兒子,那麽考慮P的前綴指針指向的節點Q,如果還無法找到通過ch連接的兒子節點,再考慮Q的前綴指針… 直到找到通過ch連接的兒子,再繼續遍歷。如果遍歷過程中經過了某個終止節點,則說明S包含該終止節點代表的模式串. 如果遍歷過程中經過了某個非終止節點的危險節點, 則可以斷定S包含某個模式串。要找出是哪個,沿著危險節點的前綴指針鏈走,碰到終止節點即可。
ps: 危險節點:1) 終止節點是危險節點 2) 如果一個節點的前綴指針指向危險節點,那麽它也是危險節點。
這樣遍歷一個串S的時間復雜度是O(len(S))
最純粹的Trie圖題目:
給N個模式串,每個不超過個字符,再給M個句子,句子長度< 100 判斷每個句子裏是否包含模式串 N < 10, M < 10 ,字符都是小寫字母 5 8 abcde defg cdke ab f abcdkef abkef bcd bca add ab qab f題目
1 #include <iostream> 2 #include <cstdio> 3 #include <cstring> 4 #include <vector> 5 #include <queue> 6代碼using namespace std; 7 #define LETTERS 26 8 int nNodesCount = 0; 9 struct CNode 10 { 11 CNode * pChilds[LETTERS]; 12 CNode * pPrev; //前綴指針 13 bool bBadNode; //是否是危險節點 14 void Init() 15 { 16 memset(pChilds,0,sizeof(pChilds)); 17 bBadNode = false; 18 pPrev = NULL; 19 } 20 }; 21 CNode Tree[200]; //10個模式串,每個10個字符,每個字符一個節點,也只要100個節點 22 23 void Insert( CNode * pRoot, char * s) 24 { 25 //將模式串s插入trie樹 26 for( int i = 0; s[i]; i ++ ) 27 { 28 if( pRoot->pChilds[s[i]-‘a‘] == NULL) 29 { 30 pRoot->pChilds[s[i]-‘a‘] =Tree + nNodesCount; 31 nNodesCount ++; 32 } 33 pRoot = pRoot->pChilds[s[i]-‘a‘]; 34 } 35 pRoot-> bBadNode = true; 36 } 37 void BuildDfa( ) 38 { 39 //在trie樹上加前綴指針 40 for( int i = 0; i < LETTERS ; i ++ ) 41 Tree[0].pChilds[i] = Tree + 1; 42 Tree[0].pPrev = NULL; 43 Tree[1].pPrev = Tree; 44 deque<CNode * > q; 45 q.push_back(Tree+1); 46 while( ! q.empty() ) 47 { 48 CNode * pRoot = q.front(); 49 q.pop_front(); 50 for( int i = 0; i < LETTERS ; i ++ ) 51 { 52 CNode * p = pRoot->pChilds[i]; 53 if( p) 54 { 55 CNode * pPrev = pRoot->pPrev; 56 while( pPrev ) 57 { 58 if( pPrev->pChilds[i] ) 59 { 60 p->pPrev = pPrev->pChilds[i]; 61 if( p->pPrev-> bBadNode) 62 p-> bBadNode = true; 63 //自己的pPrev指向的節點是危險節點,則自己也是危險節點 64 break; 65 } 66 else 67 pPrev = pPrev->pPrev; 68 } 69 q.push_back(p); 70 } 71 } 72 } //對應於while( ! q.empty() ) 73 } 74 bool SearchDfa(char * s) 75 { 76 //返回值為true則說明包含模式串 77 CNode * p = Tree + 1; 78 for( int i = 0; s[i] ; i ++ ) 79 { 80 while(true) 81 { 82 if( p->pChilds[s[i]-‘a‘]) 83 { 84 p = p->pChilds[s[i]-‘a‘]; 85 if( p-> bBadNode) 86 return true; 87 break; 88 } 89 else 90 p = p->pPrev; 91 } 92 } 93 return false; 94 } 95 int main() 96 { 97 nNodesCount = 2; 98 int M,N; 99 scanf("%d%d",&N,&M); //N個模式串,M個句子 100 for( int i = 0; i < N; i ++ ) 101 { 102 char s[20]; 103 scanf("%s",s); 104 Insert(Tree + 1,s); 105 } 106 BuildDfa(); 107 for( int i = 0 ; i < M; i ++ ) 108 { 109 char s[200]; 110 scanf("%s",s); 111 cout << SearchDfa(s) << endl; 112 } 113 return 0; 114 }
PS:有可能模式串A是另一模式串B的子串,此情況下可能只能得出匹配B的結論而忽略也匹配A,所以不能只看終止節點,還要看危險節點:
對每個節點設置一個“是否計算過”的標記,當標記一個危險節點為“已匹配”時,沿該節點對應的S的所有後綴指針一直到根節點全標記為“已匹配”。
例題:1.POJ3987 Computer Virus on Planet Pandora 2010 福州賽區題目
2.POI #7 題:病毒
3.POJ 3691 DNA repair
4.POJ 1625 Censored!
5.POJ2778 DNA Sequence
最小生成樹(MST)問題
生成樹:
1.無向連通圖的邊的集合
2.無回路
3.連接所有的點
最小: 所有邊的權值之和最小
有n個頂點,n-1條邊
Prim算法
假設G=(V,E)是一個具有n個頂點的連通網, T=(U,TE)是G的最小生成樹,U,TE初值均為空集。
首先從V中任取一個頂點(假定取v1),將它並入U中,此時U={v1},然後只要U是V的真子集(U∈V), 就從那些一個端點已在T中,另一個端點仍在T外 的所有邊中,找一條最短邊,設為(vi ,vj ),其中 vi∈U,vj∈V-U,並把該邊(vi , vj )和頂點vj分別並入T 的邊集TE和頂點集U,如此進行下去,每次往生成樹裏並入一個頂點和一條邊,直到n-1次後得到最小生成樹。
關鍵問題:每次如何從連接T中和T外頂點的所有邊中,找 到一條最短的
1) 如果用鄰接矩陣存放圖,而且選取最短邊的時候遍歷所有點進行選取,則總時間復雜度為 O(V2 ), V為頂點個數
2)用鄰接表存放圖,並使用堆來選取最短邊,則總時間復雜度為O(ElogV)
不加堆優化的Prim 算法適用於密集圖,加堆優化的適用於稀疏圖
Kruskal算法
假設G=(V,E)是一個具有n個頂點的連通網, T=(U,TE)是G的最小生成樹,U=V,TE初值為 空。
將圖G中的邊按權值從小到大依次選取,若選取的邊使生成樹不形成回路,則把它並入TE中,若形成回路則將其舍棄,直到TE 中包含N-1條邊為止,此時T為最小生成樹。
關鍵問題:如何判斷欲加入的一條邊是否與生成樹 中邊構成回路。
利用並查集!
Kruskal 和 Prim 比較
Kruskal:將所有邊從小到大加入,在此過程中 判斷是否構成回路
– 使用數據結構:並查集
– 時間復雜度:O(ElogE)
– 適用於稀疏圖
Prim:從任一節點出發,不斷擴展
– 使用數據結構:堆
– 時間復雜度:O(ElogV) 或 O(VlogV+E)(斐波那契堆)
– 適用於密集圖
– 若不用堆則時間復雜度為O(V2)
例題:1.POJ 1258 Agri-Net
2.POJ 2349 Arctic Network
3. 2011 ACM/ICPC亞洲區預選賽北京賽站
Problem A. Qin Shi Huang’s National Road System
最短路算法
Dijkstra 算法 解決無負權邊的帶權有向圖 或 無向圖的單源最短路問題
用鄰接表,不優化,時間復雜度O(V2+E)
Dijkstra+堆的時間復雜度 o(ElgV)
用斐波那契堆可以做到O(VlogV+E)
若要輸出路徑,則設置prev數組記錄每個節點的前趨點,在d[i] 更新時更新prev[i]
Dijkstra算法實現:
已經求出到V0點的最短路的點的集合為T
維護Dist數組,Dist[i]表示目前Vi到V0的“距離”
開始Dist[0] = 0, 其他Dist[i] = 無窮大, T為空集
1) 若|T| = N,算法完成,Dist數組就是解。否則取Dist[i]最 小的不在T中的點Vi, 將其加入T,Dist[i]就是Vi到V0的最短 路長度。
2) 更新所有與Vi有邊相連且不在T中的點Vj的Dist值: Dist[j] = min(Dist[j],Dist[i]+W(Vi,Vj))
3) 轉到1)
例題:1.POJ 3159 Candies
Bellman-Ford算法
解決含負權邊的帶權有向圖的單源最短路徑問題
不能處理帶負權邊的無向圖(因可以來回走一條負權邊)
限制條件: 要求圖中不能包含權值總和為負值回路(負權值回路),如下圖所示。
Bellman-Ford算法思想:
構造一個最短路徑長度數組序列dist 1 [u], dist 2 [u], …, dist n-1 [u] (u = 0,1…n-1,n為點數)
dist n-1 [u]為從源點v出發最多經過不構成負權值回路的n-1條邊到達終點u的 最短路徑長度;
算法的最終目的是計算出dist n-1 [u],為源點v到頂點u的最短路徑長度。
遞推公式(求頂點u到源點v的最短路徑):
dist 1 [u] = Edge[v][u]
dist k [u] = min{ dist k-1 [u], min{ dist k-1 [j] + Edge[j][u] } }, j=0,1,…,n-1,j≠u
若存在dist n [u] < dist n-1 [u],則說明存在從源點可達的負權值回路
在求出distn-1[ ]之後,再對每條邊<u,k>判斷一下:加入這條邊是否會使得頂點k的最短路徑值再縮短,即判斷:dist[u]+w(u,k)<dist[k]否成立,如果成立,則說明存在從源點可達的負權值回路。
存在負權回路就一定能導致該式成立的證明:
如果成立,則說明找到了一條經過了n條邊的從 s 到k的路徑,且 其比任何少於n條邊的從s到k的路徑都短。
一共n個頂點,路徑卻經過了n條邊,則必有一個頂點m經過了至少 兩次。則m是一個回路的起點和終點。走這個回路比不走這個回路 路徑更短,只能說明這個回路是負權回路。
Bellman-Ford算法改進:
Bellman-Ford算法不一定要循環n-1次,n為頂點個數,只要在某次循環過程中,考慮每條邊後,源點到所有頂點的最短路徑 長度都沒有變,那麽Bellman-Ford算法就可以提前結束了
Dijkstra算法與Bellman-Ford算法的區別
Dijkstra算法和Bellman算法思想有很大的區別:
Dijkstra算法在求解過程中,源點到集合S內各頂點的最短路徑一旦求出,則之後不變了,修改的僅僅是源點到S外各頂點的最短路徑長度。
Bellman-Ford算法在求解過程中,每次循環都要修改所有頂點的dist[ ],也就是說源點到各頂點最短路徑長度一 直要到算法結束才確定下來。
例題:1.POJ 3259 Wormholes
要求判斷任意兩點都能僅通過正邊就互相可達的有向圖(圖中有 重邊)中是否存在負權環 Sample Input 2 3 3 1 1 2 2 1 3 4 2 3 1 3 1 3 3 2 1 1 2 3 2 3 4 3 1 8 Sample Output NO YES 2個test case 每個test case 第一行: N M W (N<=500,M<=2500,W<=200) N個點 M條雙向正權邊 W條單向負權邊 第一個test case 最後一行 3 1 3 是單向負權邊,3->1的邊權值是-3題目
1 //by guo wei 2 #include <iostream> 3 #include <vector> 4 using namespace std; 5 int F,N,M,W; 6 const int INF = 1 << 30; 7 struct Edge 8 { 9 int s,e,w; 10 Edge(int ss,int ee,int ww):s(ss),e(ee),w(ww) { } 11 Edge() { } 12 }; 13 vector<Edge> edges; //所有的邊 14 int dist[1000]; 15 int Bellman_ford(int v) 16 { 17 for( int i = 1; i <= N; ++i) 18 dist[i] = INF; 19 dist[v] = 0; 20 for( int k = 1; k < N; ++k) //經過不超過k條邊 21 { 22 for( int i = 0; i < edges.size(); ++i) 23 { 24 int s = edges[i].s; 25 int e = edges[i].e; 26 if( dist[s] + edges[i].w < dist[e]) 27 dist[e] = dist[s] + edges[i].w; 28 } 29 } 30 for( int i = 0; i < edges.size(); ++ i) 31 { 32 int s = edges[i].s; 33 int e = edges[i].e; 34 if( dist[s] + edges[i].w < dist[e]) 35 return true; 36 } 37 return false; 38 } 39 int main() 40 { 41 cin >> F; 42 while( F--) 43 { 44 edges.clear(); 45 cin >> N >> M >> W; 46 for( int i = 0; i < M; ++ i) 47 { 48 int s,e,t; 49 cin >> s >> e >> t; 50 edges.push_back(Edge(s,e,t)); //雙向邊等於兩條邊 51 edges.push_back(Edge(e,s,t)); 52 } 53 for( int i = 0; i < W; ++i) 54 { 55 int s,e,t; 56 cin >> s >> e >> t; 57 edges.push_back(Edge(s,e,-t)); 58 } 59 if( Bellman_ford(1))//從1可達所有點 60 cout << "YES" <<endl; 61 else cout << "NO" <<endl; 62 } 63 }View Code
for( int k = 1; k < N; ++k) { //經過不超過k條邊 for( int i = 0;i < edges.size(); ++i) { int s = edges[i].s; int e = edges[i].e; if( dist[s] + edges[i].w < dist[e]) dist[e] = dist[s] + edges[i].w; } } 會導致在一次內層循環中,更新了某個 dist[x]後,以後又用dist[x]去更新dist[y],這樣dist[y]就是經過最多不超過k+1條邊的情況了 出現這種情況沒有關系,因為整個 for( int k = 1; k < N; ++k) 循環的目的是要確保,對任意點u,如果從源s到u的最短路是經過不超過n-1條邊的,則這條最短路不會被忽略。至於計算過程中對某些點 v 計算出了從s->v的經過超過N-1條邊的最短路的情況,也不影響結果正確性。若是從s->v的經過超過N-1條邊的結果比經過最多N-1條邊的結果更小,那一定就有負權回路。有負權回路的情況下,再多做任意多次循環,每次都會發現到有些點的最短路變得更短了。問題
2.POJ 1860
3.POJ 3259
4.POJ 2240
SPFA算法
快速求解含負權邊的帶權有向圖的單源最短路徑問題
是Bellman-Ford算法的改進版,利用隊列動態更新dist[]
維護一個隊列,裏面存放所有需要進行叠代的點。初始時隊列中只有一個 源點S。用一個布爾數組記錄每個點是否處在隊列中。
每次叠代,取出隊頭的點v,依次枚舉從v出發的邊v->u,若 Dist[v]+len(v->u) 小於Dist[u],則改進Dist[u](可同時將u前驅記為v)。 此時由於S到u的最短距離變小了,有可能u可以改進其它的點,所以若u不在隊列中,就將它放入隊尾。這樣一直叠代下去直到隊列變空,也就是S到所有節點的最短距離都確定下來,結束算法。若一個點最短路被改進的次數達到n ,則有負權環(原因同B-F算法。可以用spfa算法判斷圖有無負權環
在平均情況下,SPFA算法的期望時間復雜度為O(E)。
例題:1.POJ 3259 Wormholes
要求判斷任意兩點都能僅通過正邊就互相可達的有向圖(圖中有 重邊)中是否存在負權環 Sample Input 2 3 3 1 1 2 2 1 3 4 2 3 1 3 1 3 3 2 1 1 2 3 2 3 4 3 1 8 Sample Output NO YES 2個test case 每個test case 第一行: N M W (N<=500,M<=2500,W<=200) N個點 M條雙向正權邊 W條單向負權邊 第一個test case 最後一行 3 1 3 是單向負權邊,3->1的邊權值是-3題目
1 ///POJ3259 Wormholes 判斷有沒有負權環spfa 2 //by guo wei 3 #include <iostream> 4 #include <vector> 5 #include <queue> 6 #include <cstring> 7 using namespace std; 8 int F,N,M,W; 9 const int INF = 1 << 30; 10 struct Edge 11 { 12 int e,w; 13 Edge(int ee,int ww):e(ee),w(ww) { } 14 Edge() { } 15 }; 16 vector<Edge> G[1000]; //整個有向圖 17 int updateTimes[1000]; //最短路的改進次數 18 int dist[1000]; //dist[i]是源到i的目前最短路長度 19 int Spfa(int v) 20 { 21 for( int i = 1; i <= N; ++i) 22 dist[i] = INF; 23 dist[v] = 0; 24 queue<int> que; 25 que.push(v); 26 memset(updateTimes,0,sizeof(updateTimes)); 27 while( !que.empty()) 28 { 29 int s = que.front(); 30 que.pop(); 31 for( int i = 0; i < G[s].size(); ++i) 32 { 33 int e = G[s][i].e; 34 if( dist[e] > dist[s] + G[s][i].w ) 35 { 36 dist[e] = dist[s] + G[s][i].w; 37 que.push(e); //沒判隊列裏是否已經有e,可能會慢一些 38 ++updateTimes[e]; 39 if( updateTimes[e] >= N) return true; 40 } 41 } 42 } 43 return false; 44 } 45 int main() 46 { 47 cin >> F; 48 while( F--) 49 { 50 cin >> N >> M >> W; 51 for( int i = 1; i <1000; ++i) 52 G[i].clear(); 53 int s,e,t; 54 for( int i = 0; i < M; ++ i) 55 { 56 cin >> s >> e >> t; 57 G[s].push_back(Edge(e,t)); 58 G[e].push_back(Edge(s,t)); 59 } 60 for( int i = 0; i < W; ++i) 61 { 62 cin >> s >> e >> t; 63 G[s].push_back(Edge(e,-t)); 64 } 65 if( Spfa(1)) 66 cout << "YES" <<endl; 67 else cout << "NO" <<endl; 68 } 69 }POJ 3259
2.POJ 2387
3.POJ 3256
ACM北大暑期課培訓第六天