1. 程式人生 > >ACM北大暑期課培訓第六天

ACM北大暑期課培訓第六天

提前 真子集 root dna 端點 -- 後綴 work ios

  今天講了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北大暑期課培訓第六天