國慶七天樂——第六天
20171006
【【圖論】】
**********************定義*****************************
在講這個問題之前,首先我們需要了解圖論中的圖是什麽東西。
定義:圖G是一個有序二元組(V,E),其中V稱為頂集(Vertices Set),其中的元素稱為頂點,E稱為邊集(Edges set),其中的元素稱為邊。E與V不相交。它們亦可寫成V(G)和E(G)。
邊都是二元組,用(x,y)表示,其中x,y∈V。
圖G可以理解為對若幹個元素之間的關系的抽象表示,其中邊代表著對應的頂點之間的關系。
由於圖G包含兩個集合,頂集和邊集,因此圖G的規模有兩個向度:V集的大小(頂點的數量)和E集的大小(邊的數量),前者又稱為圖G的階。
如果圖G所表述的頂點之間的關系是不能互換的,那麽對於x和y之間的關系來說,x和y的相對順序是必須考慮的,此時邊(x,y)可以用有序對(x,y)表示,而圖G是有向圖。
************************************路徑*******************
令圖G=(V,E),u到w的一條路徑指的是這樣的一個序列:
u,e1,v1,e2,v2,..,ek,w
其中頂點和邊相間,而且邊表示從左邊頂點到右邊頂點的關系
如果這條路徑除了u和w以外其余頂點都各不相等,那麽稱這條路徑為簡單路徑。
如果u=w,也就是起點和終點相等,那麽這條路徑稱為環(回路)
路徑的長度定義為路徑中所有邊的權值的和(可能為負)
如果u到v存在路徑,v到w存在路徑,那u到w也存在路徑。
對於無向圖,u到v的路徑可以通過反轉(排列反轉,邊變為反向邊)得到v到u的路徑。
*******************************分類*******************
對於圖G來說,如果它所表述的頂點之間的關系都是可以互換的,也就是說這個關系不關心兩個頂點的相對順序,那麽邊(x,y)可以用無序對(x,y)表示,此時圖G是無向圖。
令無向圖G=(V,E),如果?x,y∈V,x和y之間都存在路徑,那麽稱圖G為連通圖。
令有向圖G=(V,E),如果?x,y∈V,x到y都存在路徑,那麽稱圖G為強連通圖。
令圖G=(V,E),如果對於任意的頂點x和y,x到y之間的關系都可以用最多一條邊表述,也就是說V中不含兩個或以上的(x,y),那麽圖G稱為簡單圖。簡單圖可以用矩陣表示。
令無向圖G=(V,E),如果圖G連通,且不包含簡單環。那麽稱圖G為無向樹,此時|V|-1=|E|。
令有向圖G=(V,E),如果圖G在把邊無向化後變為無向樹,那麽稱圖G為有向樹。
令有向樹G=(V,E),如果存在一個頂點x使得從x能夠到達其余所有頂點,那麽有向樹G=(V,E)稱為根在x的樹形圖。
令圖G=(V,E),取集合V’=V,然後令E’?E,則圖G’=(V’,E’)稱為圖G的生成子圖。特別地,若此時G’為無向樹,那麽稱圖G’為生成樹。
令圖G=(V,E),取集合V’?V,然後令E’={ (x,y)∈E | x,y∈V’ },則圖G’=(V’,E’)稱為圖G的導出子圖。
令圖G=(V,E),分別取集合V’?V,E’?E,且E’中的元素(x,y)均滿足x,y∈V’,則G’=(V’,E’)稱為圖G的子圖。
可以看到,圖G的任何子圖,都可以看作是圖G的某個導出子圖的生成子圖。
************************存儲***************************
1.直接列表法
直接開三個數組,記錄圖的每條邊的起點、終點和權值。
比如右圖,我們可以開三個數組u[ ],v[ ],w[ ],並存下如表所示的數值。
讀入的時候就能夠建立列表。
但是需要轉化為其他形式才能解決和路徑有關的問題。
時間復雜度O(m)
我們運用數組的靜態空間,來作為鏈表的實際存儲位置
鏈表和鏈表元素的定義
struct Edge{ int to; int data; Edge* next; } edge[max_Esize],*fir[max_Vsize]; int edges;
加入邊
for (i=0,edges=0;i<m;i++) { //第i條邊起點為u[i]終點為v[i]信息為w[i]
edge[edges]=Edge(v[i],w[i],fir[u[i]]); fir[u[i]]=edge+edges; edges++; }
2.矩陣形式(鄰接矩陣)
給定一個簡單圖G,我們可以建立一個矩陣A,使得任意兩個頂點x到y之間邊的有無(有時候還會有權值)都能用矩陣A的對應元素Axy表示。
這樣的話,存儲一個階為n的圖G,所用的時間和空間復雜度為O(n2)
優點:操作簡單,在需要求所有頂點對之間最短路徑的時候,或者需要使用矩陣乘法的時候有奇效(這點在講最短路的時候詳細介紹)
缺點:在面對較為稀疏的圖的時候造成時間空間的大量浪費。
矩陣的定義
int edge_data[max_Vsize][max_Vsize];
加入邊
for (i=0;i<m;i++) edge_data[u[i]][v[i]]=w[i];
3.鏈表形式(鄰接鏈表)
對於每個頂點,我們可以用一個鏈表儲存從它出去的邊。
采取將邊逐條加入鏈表的形式(無向邊拆成2條有向邊),存儲具有m條邊的圖G所用的時間和空間復雜度為O(m)
優點:節省空間,能夠應對階更大的圖
對於鄰接矩陣,我們想要對一條無向邊(x,y)進行操作,只要找到對應頂點的下標Axy和Ayx進行對應操作就可以了。
但是對於鄰接鏈表,同樣為了對一條無向邊(x,y)進行操作,我們需要在兩個鏈表中進行操作,如何更快地找到鏈表中代表這條無向邊的元素呢?
最簡單的方法是直接在兩個鏈表中尋找。
但是我們也可以選擇建立這兩條邊位置之間的數學聯系,這樣當找到其中一條邊時,通過對位置進行數學運算就能找到反向邊。
反向邊的位置關系一般有2種:一種是下標±m,另一種是下標^1
******************圖的遍歷*****************************
1.深度優先搜索(DFS)
原則是建立一個棧,只要棧頂結點u還有相鄰的點v未入過棧,就把v入棧,遍歷v,繼續遞歸地搜索,當棧頂結點u的相鄰結點都入過棧時,將u出棧。
深度優先搜索
int dfs(int nd) {
for (each v adjacent to nd) if (visited[v]==0) { visit(v); dfs(v); }
}
2. 廣度優先搜索(BFS)
建立一個隊列,每次從隊頭取出一個結點,然後將相鄰的沒進過隊列的結點入隊並遍歷,然後再取出一個結點如此做。
深度優先搜索
int dfs(int nd) {
for (each v adjacent to nd) if (visited[v]==0) { visit(v); dfs(v); }
}
【最短路】
問題1(單源最短路徑問題)
性質1:給定s和t(且從s出發能夠到達t),如果不存在負權回路,那麽必定存在一條簡單路徑作為最短路。
性質2:如果從s到t的最短路se1...vk-1ekt滿足k>1,那麽取除最後一條邊得到的se1...vk-1是從s到vk-1的最短路。(最優子結構)
性質3:若對於圖G=(V,E)的起點s,s能夠達到圖G所有頂點,那麽圖G存在一個生成子圖G’,並且G’是以s為根的樹形圖,使得對任意結點x,從s沿該樹形圖到x的路徑均為從s到x的最短路徑。
性質4:對於圖G=(V,E)的任意兩個頂點u,v,設從u到v的最短路徑長度為duv。那麽對任意三個頂點u,v,w,均有duw≤duv+dvw。(三角形不等式)
******************************實際操作********************
1.Dijkstra
每次取出未固定的結點當中,dist最小的結點nd
由前面的性質可知,nd無法被已固定的結點更新,而其余未固定的結點u也無法通過更新dist[u]使dist[u]<dist[nd],同樣無法更新dist[nd],這時候dist[nd]就固定了。
固定了dist[nd]以後,為了維持前面的性質,我們拿dist[nd]去松弛與nd相鄰的結點
此時對於未固定的結點v,它的臨時最短路的前驅par[v]要麽是nd,要麽是先前固定的結點,而且那條最短路不比從其他已固定結點的最短路接續的長,這時候前面的性質又一次得到滿足。
重復以上操作直到固定了dist[t]為止。
時間主要耗費在兩個地方:松弛結點,找最小值
這算法的時間主要耗費在兩個地方:松弛結點,找最小值。
對於找最小值的部分,如果直接枚舉的話,對於固定完所有結點才能結束的最壞情況,時間復雜度可達到O(|V|2),此時松弛操作的時間復雜度為O(|E|)。總的時間復雜度為O(|V|2+|E|)。
如果我們用一個高效支持單元素修改,詢問全體最小值的數據結構來記錄(線段樹/堆/優先隊列),那麽在單次修改復雜度變為O(log|V|)的前提下,單次詢問的時間復雜度為O(1)。因此總的時間復雜度為O(|E|log|V|+|V|)。
不加堆優化的程序實現(使用鄰接矩陣w[ ][ ])
for (i=1;i<=n;i++) dist[i]=+inf,closed[i]=0;
dist[s]=0; closed[s]=1; nd=s;
for ( ; nd!=t ; ) {
for (i=1;i<=n;i++)
if (dist[i]>dist[nd]+w[nd][i])
dist[i]=dist[nd]+w[nd][i];
nd=t;
for (i=1;i<=n;i++)
if (!closed[i]&&dist[i]<dist[nd]) nd=i;
closed[nd]=1;
}
不加堆優化的程序實現(使用鄰接鏈表)
for (i=1;i<=n;i++) dist[i]=+inf,closed[i]=0;
dist[s]=0; closed[s]=1; nd=s;
for ( ; nd!=t ; ) {
for (p=fir[nd];p!=0;p=p->next)
if (dist[p->to]>dist[nd]+p->w)
dist[p->to]=dist[nd]+p->w;
nd=t;
for (i=1;i<=n;i++)
if (!closed[i]&&dist[i]<dist[nd]) nd=i;
closed[nd]=1;
}
加堆優化的程序實現(使用鄰接鏈表,優先隊列)
priority_queue<pair<int,int>> que; while (!que.empty()) que.pop();
for (i=1;i<=n;i++) dist[i]=+inf; dist[s]=0; que.push(make_pair(0,s));
while (!que.empty()) {
int nd=que.top().second; que.pop();
if (closed[nd]) continue; closed[nd]=1;
for (p=fir[nd];p!=0;p=p->next)
if (distc[p->to]>dist[nd]+p->w) {
dist[p->to]=dist[nd]+p->w;
que.push( make_pair(-dist[p->to],p->to) );
}
}
這個算法無法處理負權邊
2.bellman-ford
用一個數組d[ ]記錄從s出發到所有結點當前的最短路徑。
一開始設d[s]=0,然後枚舉所有的邊,利用三角形不等式更新最短距離。
不斷枚舉直到某一次枚舉中沒有結點被更新。
如果沒有負權回路,算法應當在第|V|次枚舉之前就求出所有結點的最短路徑。
如此做的時間復雜度為O(nm)
但我們一般都不用這個算法,我們用他的兒子——SPFA
3.SPFA
用一個隊列存儲被更新的結點
從s出發,利用bfs每次從隊列中取出一個結點對周圍的結點進行松弛操作,因此而被更新d值的結點加入隊列。
如此直到隊列中沒有元素為止。(即沒有元素能夠被更新時)
當隊列中沒有元素時,圖中所有結點都找到了從起點到自身的最短路。
實現 //鏈表struct Edge{ int to; int dist; Edge* next; } edge[max_Esize],*fir[max_Vsize]; int edges;
for (i=1;i<=n;i++) dist[i]=+inf, inq[i]=0; // 以1為起點,先假設不存在路徑
q[0]=1; dist[1]=0; inq[1]=1; //inq表示該結點是否在隊列中
for (h=0;h<t;h++) { //在n較大的時候宜改成循環隊列
u=q[h]; inq[u]++; //inq為奇數表示在隊內,偶數表示在隊外
for (p=fir[u];p!=0;p=p->next) if (dist[v=p->to]>dist[u]+p->dist) {dist[v]=dist[u]+p->dist;
back[v]=u;
if (!(inq[v]&1)) { inq[v]++; q[t++]=v; } }
}
如果圖中存在負權回路,前面的程序會死循環,這個問題後面解決。
如果圖中不存在負權回路,那麽任何的最短路都會是簡單路徑。而一個階為n的圖,到一個點的最短路所經過的路徑數量最多為n-1。
我們可以證明,每求出所有某一長度的最短路徑,每個點最多入隊一次。
時間復雜度O(k(n+m)),其中k和最短路經過的路徑條數有關。
在實際運行中,隨機數據較難達到這個時間。(人品算法)
問題2(多源最短路徑問題)
1floyd
這個算法的思路建立在性質1的正確性的基礎之上,它考慮兩個結點的最短路所經過的其它結點。
它運用動態規劃的思想,從直連的邊出發,逐步加入中間點,首先處理中間點只有1的最短路,然後令2也成為中間點……
最後當所有的結點都被允許成為中間點以後,任何起點和終點之間的最短路也就求出來了。
當枚舉第k個中間點的時候,我們用三角形不等式,將只含有前k-1個中間點的路徑u-k和v-k拼接起來,和原來的只有前k-1個中間點的u-v進行比較,記錄更優的結果。
由於性質1的正確性,每個最短路不可能包含2個或以上的結點k,通過拼接可以由k-1個中間點的最短路求出k個中間點的最短路。
這個算法需要依次添加|V|個中間點,每次需要比較|V|2個結點對,因此時間復雜度為O(|V|3)。
下面是程序實現
for (mid=1;mid<=n;mid++) //每循環一次,給最短路加入一個中間點
for (le=1;le<=n;le++) //枚舉拼接路徑的起點和終點
for (ri=1;ri<=n;ri++)
if (dist[le][ri]>dist[le][mid]+dist[mid][ri])
dist[le][ri]=dist[le][mid]+dist[mid][ri];
******************************例題*************************
- 缺水的村子
floyd算法求出任意兩間房子之間的最短路徑。
- 郵遞員(luogu1629)
這道題我們求出1->2,1->3,…,1->n,2->1,3->1,…,n->1的最短路徑的和
對於前n-1條最短路徑,直接進行dijkstra算法來求。
對於後n-1條,要求的是以1為終點的最短路徑,我們可以把圖中的邊變為反向邊,這樣問題就轉化為求以1為起點的最短路徑了。
- poj3259
這道題只需要判斷是否存在一個回路,繞著走一圈以後回到過去。
也就是是否存在負權回路的問題,可以用bellman-ford或者spfa。
【最小生成樹】
- kruskal
首先證明,整個圖G權值最小的邊一定在最小生成樹裏面。
我們在將權值最小的邊加入了最小生成樹以後,可以將這條邊所連接的兩個點合成一個點考慮,然後再找下一個權值最小的連接兩個不同點的邊。
以此類推,我們可以把所有的邊按照邊權排序,先插入邊權較小的邊,當某條邊插入時兩端已經在同一個連通塊,就舍棄這條邊,否則就插入這條邊並合並對應的連通塊。
如何判斷兩個點所在的連通塊是否相等,用並查集。
時間復雜度:排序O(|E|log|E|),並查集維護O(|E|)
程序實現(用struct Edge{ int u,v,weight; }表示邊)
sort(edge,edge+m,cmp); // 按邊權從小到大排序
for (i=1;i<=n;i++) fa[i]=i,rk[i]=0;
for (i=0;i<m;i++)
{
tu=top(edge[i].u);
tv=top(edge[i].v);
if (tu==tv) continue;
if (rk[tu]<rk[tv]) swap(tu,tv);
if (rk[tu]==rk[tv]) rk[tu]++;
fa[tv]=tu; ans+=weight;
}
- prim
首先證明,對於某個結點來說,以其為端點的邊當中,權值最小的一條邊一定在最小生成樹中。(當權值最小的有多條的時候,每一條都存在某棵最小生成樹包含之)
也就是說,我們可以從一個結點出發,在相鄰的邊當中選擇一條權值最小的,加入最小生成樹,然後將整個連通塊當成一個結點,對外再選下一條權值最小的邊。
以此類推,我們可以仿照dijkstra中對結點最短距離的維護,只是我們這次維護的是當前連通塊連到這個結點的邊中權值最小的一條。
不使用堆優化,時間復雜度O(|V|2)
使用堆優化,時間復雜度O(|E|log|V|+|V|)
Prim算法不加堆優化(使用鄰接矩陣w[ ][ ])
for (i=1;i<=n;i++) dist[i]=+inf,closed[i]=0;
dist[s]=0; closed[s]=1; nd=s;
for ( ; nd!=t ; ) {
for (i=1;i<=n;i++)
if (dist[i]>w[nd][i])
dist[i]=w[nd][i];
nd=t;
for (i=1;i<=n;i++)
if (!closed[i]&&dist[i]<dist[nd]) nd=i;
closed[nd]=1; ans+=dist[nd];
}
Prim算法加堆優化
priority_queue<pair<int,int>> que; while (!que.empty()) que.pop();
for (i=1;i<=n;i++) dist[i]=+inf; dist[s]=0; que.push(make_pair(0,s));
while (!que.empty()) {
int nd=que.top().second; que.pop();
if (closed[nd]) continue; closed[nd]=1; ans+=dist[nd];
for (p=fir[nd];p!=0;p=p->next)
if (dist[p->to]>p->w) {
dist[p->to]=p->w;
que.push( make_pair(-dist[p->to],p->to) );
}
}
***********************應用*******************************
1poj1287
這一題,最小生成樹水題,對最小生成樹不熟悉的同學可以用這道題目練手。
2.在二維坐標系上有n個點,現在要給這些點之間連上線段,使得所有n個點連通的情況下,線段的長度和最小
Ans: 將每個點對之間的距離全部求出來以後,問題轉換為最小生成樹。
【拓撲排序】
只有有向無環圖(DAG)有拓撲排序
*****************建拓撲排序的方法*********************
1.檢查入度法
如果一個活動入度為0,那就表示這個活動沒有前置活動,可以放在序列的最前面。將這個活動放入拓撲序列中,並且將其出度全部刪除,以找到一個新的入度為0 的結點。
檢查入度法的時間復雜度O(n+m)
檢查入度法 //通過鄰接鏈表組織起來的邊edge[1..m]
for (i=1;i<=m;i++) rd[edge[i].to]++;
t=0; for (i=1;i<=n;i++) if (rd[i]==0) que[t++]=i;
for (h=0;h<t;h++) { //que表示拓撲順序
u=que[h];
for (p=fir[u];p!=0;p=p->next) {
rd[p->to]--;
if (rd[p->to]==0) que[t++]=p->to;
}
}
- 深度搜索法
********************補充定義內容***********************
深度優先搜索
在深度優先搜索的過程中,所有的結點分為3類,用vis值表示
vis=0表示該結點還沒訪問過,
vis=1表示該結點訪問過,但還沒從該點返回(子孫結點還沒訪問完)
vis=2表示該結點已經返回了
此外,每個結點賦予一個dfn值,表示結點的入棧順序
對於邊來說,邊分為樹邊,前向邊,後向邊和橫叉邊,其中
樹邊指的是從父節點找到子結點所用的邊
前向邊指的是從祖先結點指向其子孫結點的非樹邊
後向邊指的是從子孫結點往回指向祖先結點的邊
橫叉邊指的是沒有祖孫關系的兩個結點之間的邊,起點的dfn值排在終點後面
在進行dfs的時候,我們對這些邊有不同的處理方式
****************************補充完畢******************。
我們從任意的結點出發,進行深度搜索,找任意一個vis值<2的相鄰結點遞歸下去,如果我們找到了一個vis值=1的結點,就表示我們找到一個環。
當某個結點不存在相鄰結點,或者相鄰結點全部vis值為2的時候,這個結點為拓撲序列的最後一位,然後返回。
說人話:先遞歸到最後一層,如果最後的點沒有出度了,這個點就是拓撲序的最後一個,存入,然後倒敘輸出。
如果出發的結點返回後仍未加入所有結點,找一個未加入的結點進行上面的操作,直到發現環或者加入所有結點為止。時間復雜度O(n+m)。
int dfs(int nd)
{
vis[nd]=1;
for(each v adjacent to nd)
{
if(vis[v]==1) return false;
if(vis[v]==0)
if(!dfs(v)) return false;
}
vis[nd]=2;
que[t++]=nd;
return true;
for(i=1;i<=n;i++)
if(vis[i]==0)
if(!dfs(i)) break;
}
************************應用***************************
1.給出一個只含有負權邊的圖G(|V|<2000000,|E|<3000000),求所有以任意頂點為起點的最短路徑長度的最小值。
Ans:
這一題用前面提到的最短路徑一定會超時。
首先如果這圖有環,那麽就存在負權回路,答案為-∞。
否則可以對這圖進行拓撲排序,用dist[v]表示以任意起點以結點v結尾的最短路長度。
然後在進行了拓撲排序的前提下,我們發現,松弛操作只可能是前面的結點對後面的結點進行松弛。所以我們按照這個序列的先後順序,一個個往後松弛,最後得到以所有結點結尾的最短路長度。
時間復雜度O(|V|+|E|)
2.給出一個有向無環圖(|V|<2000000,|E|<3000000),頂點v處有kv元錢,現在,任意選定出發的頂點,並在任意終點結束,求最多能從該圖收集到多少錢?
Ans:
首先對這一題進行拓撲排序,按照所得序列的順序,計算到達每個頂點時能收集到的最多的錢。
用dp[v]表示走到v最多能夠得多少錢,然後我們列出狀態轉移方程:
dp[v]=max(dp[v],dp[u]+k[v])
時間復雜度O(|V|+|E|)
3.給出一個有向圖(|V|<2000000,|E|<3000000),頂點v處有kv元錢,現在,任意選定出發的頂點,並在任意終點結束,求最多能從該圖收集到多少錢?
Ans:
這一題需要學習強連通分量才能解答,我們可以把這個有向圖的強連通分量縮成一個點,這個圖就變成DAG了。
總的時間復雜度O(|V|+|E|)。
【連通分量】
無向圖G=(V,E)是連通的,當且僅當其中任意兩個結點能互相到達,如果G是有向圖,這種情況稱G是強連通的。
對於一個連通圖G來說,如果刪掉了邊(u,v)以後,會使得圖不再連通,那麽稱這條邊(u,v)為橋(也稱割邊)
如果一個連通圖不包含橋的話,就意味著這個連通圖無法僅靠刪除一條邊變為不連通圖,此時稱這個連通圖為雙連通圖。
對於圖G的子圖G’來說,如果G’連通,那麽稱G’為G的連通子圖。
如果圖G的某個連通子圖G’的頂集不是其余連通子圖G’’的點集的真子集,那麽稱G’為G的一個連通分量。
對於強連通圖和雙連通圖,也有類似的性質。
【tarjan】求強聯通分量
tarjan算法借助dfs過程中產生的各種類型的邊,旨在為每一個結點通過後向邊找到dfs樹下的子結點最多能夠追溯到高度多低的祖先,從而確定強連通分量的範圍。
我們記錄dfn,表示結點的訪問順序,然後額外地記錄low表示以自身為根的子樹能夠到達的最小高度的祖先。
如左圖所示,2號結點有一個後代有直接
20171006
【【圖論】】
**********************定義*****************************
在講這個問題之前,首先我們需要了解圖論中的圖是什麽東西。
定義:圖G是一個有序二元組(V,E),其中V稱為頂集(Vertices Set),其中的元素稱為頂點,E稱為邊集(Edges set),其中的元素稱為邊。E與V不相交。它們亦可寫成V(G)和E(G)。
邊都是二元組,用(x,y)表示,其中x,y∈V。
圖G可以理解為對若幹個元素之間的關系的抽象表示,其中邊代表著對應的頂點之間的關系。
由於圖G包含兩個集合,頂集和邊集,因此圖G的規模有兩個向度:V集的大小(頂點的數量)和E集的大小(邊的數量),前者又稱為圖G的階。
如果圖G所表述的頂點之間的關系是不能互換的,那麽對於x和y之間的關系來說,x和y的相對順序是必須考慮的,此時邊(x,y)可以用有序對(x,y)表示,而圖G是有向圖。
************************************路徑*******************
令圖G=(V,E),u到w的一條路徑指的是這樣的一個序列:
u,e1,v1,e2,v2,..,ek,w
其中頂點和邊相間,而且邊表示從左邊頂點到右邊頂點的關系
如果這條路徑除了u和w以外其余頂點都各不相等,那麽稱這條路徑為簡單路徑。
如果u=w,也就是起點和終點相等,那麽這條路徑稱為環(回路)
路徑的長度定義為路徑中所有邊的權值的和(可能為負)
如果u到v存在路徑,v到w存在路徑,那u到w也存在路徑。
對於無向圖,u到v的路徑可以通過反轉(排列反轉,邊變為反向邊)得到v到u的路徑。
*******************************分類*******************
對於圖G來說,如果它所表述的頂點之間的關系都是可以互換的,也就是說這個關系不關心兩個頂點的相對順序,那麽邊(x,y)可以用無序對(x,y)表示,此時圖G是無向圖。
令無向圖G=(V,E),如果?x,y∈V,x和y之間都存在路徑,那麽稱圖G為連通圖。
令有向圖G=(V,E),如果?x,y∈V,x到y都存在路徑,那麽稱圖G為強連通圖。
令圖G=(V,E),如果對於任意的頂點x和y,x到y之間的關系都可以用最多一條邊表述,也就是說V中不含兩個或以上的(x,y),那麽圖G稱為簡單圖。簡單圖可以用矩陣表示。
令無向圖G=(V,E),如果圖G連通,且不包含簡單環。那麽稱圖G為無向樹,此時|V|-1=|E|。
令有向圖G=(V,E),如果圖G在把邊無向化後變為無向樹,那麽稱圖G為有向樹。
令有向樹G=(V,E),如果存在一個頂點x使得從x能夠到達其余所有頂點,那麽有向樹G=(V,E)稱為根在x的樹形圖。
令圖G=(V,E),取集合V’=V,然後令E’?E,則圖G’=(V’,E’)稱為圖G的生成子圖。特別地,若此時G’為無向樹,那麽稱圖G’為生成樹。
令圖G=(V,E),取集合V’?V,然後令E’={ (x,y)∈E | x,y∈V’ },則圖G’=(V’,E’)稱為圖G的導出子圖。
令圖G=(V,E),分別取集合V’?V,E’?E,且E’中的元素(x,y)均滿足x,y∈V’,則G’=(V’,E’)稱為圖G的子圖。
可以看到,圖G的任何子圖,都可以看作是圖G的某個導出子圖的生成子圖。
************************存儲***************************
1.直接列表法
直接開三個數組,記錄圖的每條邊的起點、終點和權值。
比如右圖,我們可以開三個數組u[ ],v[ ],w[ ],並存下如表所示的數值。
讀入的時候就能夠建立列表。
但是需要轉化為其他形式才能解決和路徑有關的問題。
時間復雜度O(m)
我們運用數組的靜態空間,來作為鏈表的實際存儲位置
鏈表和鏈表元素的定義
struct Edge{ int to; int data; Edge* next; } edge[max_Esize],*fir[max_Vsize]; int edges;
加入邊
for (i=0,edges=0;i<m;i++) { //第i條邊起點為u[i]終點為v[i]信息為w[i]
edge[edges]=Edge(v[i],w[i],fir[u[i]]); fir[u[i]]=edge+edges; edges++; }
2.矩陣形式(鄰接矩陣)
給定一個簡單圖G,我們可以建立一個矩陣A,使得任意兩個頂點x到y之間邊的有無(有時候還會有權值)都能用矩陣A的對應元素Axy表示。
這樣的話,存儲一個階為n的圖G,所用的時間和空間復雜度為O(n2)
優點:操作簡單,在需要求所有頂點對之間最短路徑的時候,或者需要使用矩陣乘法的時候有奇效(這點在講最短路的時候詳細介紹)
缺點:在面對較為稀疏的圖的時候造成時間空間的大量浪費。
矩陣的定義
int edge_data[max_Vsize][max_Vsize];
加入邊
for (i=0;i<m;i++) edge_data[u[i]][v[i]]=w[i];
3.鏈表形式(鄰接鏈表)
對於每個頂點,我們可以用一個鏈表儲存從它出去的邊。
采取將邊逐條加入鏈表的形式(無向邊拆成2條有向邊),存儲具有m條邊的圖G所用的時間和空間復雜度為O(m)
優點:節省空間,能夠應對階更大的圖
對於鄰接矩陣,我們想要對一條無向邊(x,y)進行操作,只要找到對應頂點的下標Axy和Ayx進行對應操作就可以了。
但是對於鄰接鏈表,同樣為了對一條無向邊(x,y)進行操作,我們需要在兩個鏈表中進行操作,如何更快地找到鏈表中代表這條無向邊的元素呢?
最簡單的方法是直接在兩個鏈表中尋找。
但是我們也可以選擇建立這兩條邊位置之間的數學聯系,這樣當找到其中一條邊時,通過對位置進行數學運算就能找到反向邊。
反向邊的位置關系一般有2種:一種是下標±m,另一種是下標^1
******************圖的遍歷*****************************
1.深度優先搜索(DFS)
原則是建立一個棧,只要棧頂結點u還有相鄰的點v未入過棧,就把v入棧,遍歷v,繼續遞歸地搜索,當棧頂結點u的相鄰結點都入過棧時,將u出棧。
深度優先搜索
int dfs(int nd) {
for (each v adjacent to nd) if (visited[v]==0) { visit(v); dfs(v); }
}
2. 廣度優先搜索(BFS)
建立一個隊列,每次從隊頭取出一個結點,然後將相鄰的沒進過隊列的結點入隊並遍歷,然後再取出一個結點如此做。
深度優先搜索
int dfs(int nd) {
for (each v adjacent to nd) if (visited[v]==0) { visit(v); dfs(v); }
}
【最短路】
問題1(單源最短路徑問題)
性質1:給定s和t(且從s出發能夠到達t),如果不存在負權回路,那麽必定存在一條簡單路徑作為最短路。
性質2:如果從s到t的最短路se1...vk-1ekt滿足k>1,那麽取除最後一條邊得到的se1...vk-1是從s到vk-1的最短路。(最優子結構)
性質3:若對於圖G=(V,E)的起點s,s能夠達到圖G所有頂點,那麽圖G存在一個生成子圖G’,並且G’是以s為根的樹形圖,使得對任意結點x,從s沿該樹形圖到x的路徑均為從s到x的最短路徑。
性質4:對於圖G=(V,E)的任意兩個頂點u,v,設從u到v的最短路徑長度為duv。那麽對任意三個頂點u,v,w,均有duw≤duv+dvw。(三角形不等式)
******************************實際操作********************
1.Dijkstra
每次取出未固定的結點當中,dist最小的結點nd
由前面的性質可知,nd無法被已固定的結點更新,而其余未固定的結點u也無法通過更新dist[u]使dist[u]<dist[nd],同樣無法更新dist[nd],這時候dist[nd]就固定了。
固定了dist[nd]以後,為了維持前面的性質,我們拿dist[nd]去松弛與nd相鄰的結點
此時對於未固定的結點v,它的臨時最短路的前驅par[v]要麽是nd,要麽是先前固定的結點,而且那條最短路不比從其他已固定結點的最短路接續的長,這時候前面的性質又一次得到滿足。
重復以上操作直到固定了dist[t]為止。
時間主要耗費在兩個地方:松弛結點,找最小值
這算法的時間主要耗費在兩個地方:松弛結點,找最小值。
對於找最小值的部分,如果直接枚舉的話,對於固定完所有結點才能結束的最壞情況,時間復雜度可達到O(|V|2),此時松弛操作的時間復雜度為O(|E|)。總的時間復雜度為O(|V|2+|E|)。
如果我們用一個高效支持單元素修改,詢問全體最小值的數據結構來記錄(線段樹/堆/優先隊列),那麽在單次修改復雜度變為O(log|V|)的前提下,單次詢問的時間復雜度為O(1)。因此總的時間復雜度為O(|E|log|V|+|V|)。
不加堆優化的程序實現(使用鄰接矩陣w[ ][ ])
for (i=1;i<=n;i++) dist[i]=+inf,closed[i]=0;
dist[s]=0; closed[s]=1; nd=s;
for ( ; nd!=t ; ) {
for (i=1;i<=n;i++)
if (dist[i]>dist[nd]+w[nd][i])
dist[i]=dist[nd]+w[nd][i];
nd=t;
for (i=1;i<=n;i++)
if (!closed[i]&&dist[i]<dist[nd]) nd=i;
closed[nd]=1;
}
不加堆優化的程序實現(使用鄰接鏈表)
for (i=1;i<=n;i++) dist[i]=+inf,closed[i]=0;
dist[s]=0; closed[s]=1; nd=s;
for ( ; nd!=t ; ) {
for (p=fir[nd];p!=0;p=p->next)
if (dist[p->to]>dist[nd]+p->w)
dist[p->to]=dist[nd]+p->w;
nd=t;
for (i=1;i<=n;i++)
if (!closed[i]&&dist[i]<dist[nd]) nd=i;
closed[nd]=1;
}
加堆優化的程序實現(使用鄰接鏈表,優先隊列)
priority_queue<pair<int,int>> que; while (!que.empty()) que.pop();
for (i=1;i<=n;i++) dist[i]=+inf; dist[s]=0; que.push(make_pair(0,s));
while (!que.empty()) {
int nd=que.top().second; que.pop();
if (closed[nd]) continue; closed[nd]=1;
for (p=fir[nd];p!=0;p=p->next)
if (distc[p->to]>dist[nd]+p->w) {
dist[p->to]=dist[nd]+p->w;
que.push( make_pair(-dist[p->to],p->to) );
}
}
這個算法無法處理負權邊
2.bellman-ford
用一個數組d[ ]記錄從s出發到所有結點當前的最短路徑。
一開始設d[s]=0,然後枚舉所有的邊,利用三角形不等式更新最短距離。
不斷枚舉直到某一次枚舉中沒有結點被更新。
如果沒有負權回路,算法應當在第|V|次枚舉之前就求出所有結點的最短路徑。
如此做的時間復雜度為O(nm)
但我們一般都不用這個算法,我們用他的兒子——SPFA
3.SPFA
用一個隊列存儲被更新的結點
從s出發,利用bfs每次從隊列中取出一個結點對周圍的結點進行松弛操作,因此而被更新d值的結點加入隊列。
如此直到隊列中沒有元素為止。(即沒有元素能夠被更新時)
當隊列中沒有元素時,圖中所有結點都找到了從起點到自身的最短路。
實現 //鏈表struct Edge{ int to; int dist; Edge* next; } edge[max_Esize],*fir[max_Vsize]; int edges;
for (i=1;i<=n;i++) dist[i]=+inf, inq[i]=0; // 以1為起點,先假設不存在路徑
q[0]=1; dist[1]=0; inq[1]=1; //inq表示該結點是否在隊列中
for (h=0;h<t;h++) { //在n較大的時候宜改成循環隊列
u=q[h]; inq[u]++; //inq為奇數表示在隊內,偶數表示在隊外
for (p=fir[u];p!=0;p=p->next) if (dist[v=p->to]>dist[u]+p->dist) {dist[v]=dist[u]+p->dist;
back[v]=u;
if (!(inq[v]&1)) { inq[v]++; q[t++]=v; } }
}
如果圖中存在負權回路,前面的程序會死循環,這個問題後面解決。
如果圖中不存在負權回路,那麽任何的最短路都會是簡單路徑。而一個階為n的圖,到一個點的最短路所經過的路徑數量最多為n-1。
我們可以證明,每求出所有某一長度的最短路徑,每個點最多入隊一次。
時間復雜度O(k(n+m)),其中k和最短路經過的路徑條數有關。
在實際運行中,隨機數據較難達到這個時間。(人品算法)
問題2(多源最短路徑問題)
1floyd
這個算法的思路建立在性質1的正確性的基礎之上,它考慮兩個結點的最短路所經過的其它結點。
它運用動態規劃的思想,從直連的邊出發,逐步加入中間點,首先處理中間點只有1的最短路,然後令2也成為中間點……
最後當所有的結點都被允許成為中間點以後,任何起點和終點之間的最短路也就求出來了。
當枚舉第k個中間點的時候,我們用三角形不等式,將只含有前k-1個中間點的路徑u-k和v-k拼接起來,和原來的只有前k-1個中間點的u-v進行比較,記錄更優的結果。
由於性質1的正確性,每個最短路不可能包含2個或以上的結點k,通過拼接可以由k-1個中間點的最短路求出k個中間點的最短路。
這個算法需要依次添加|V|個中間點,每次需要比較|V|2個結點對,因此時間復雜度為O(|V|3)。
下面是程序實現
for (mid=1;mid<=n;mid++) //每循環一次,給最短路加入一個中間點
for (le=1;le<=n;le++) //枚舉拼接路徑的起點和終點
for (ri=1;ri<=n;ri++)
if (dist[le][ri]>dist[le][mid]+dist[mid][ri])
dist[le][ri]=dist[le][mid]+dist[mid][ri];
******************************例題*************************
- 缺水的村子
floyd算法求出任意兩間房子之間的最短路徑。
- 郵遞員(luogu1629)
這道題我們求出1->2,1->3,…,1->n,2->1,3->1,…,n->1的最短路徑的和
對於前n-1條最短路徑,直接進行dijkstra算法來求。
對於後n-1條,要求的是以1為終點的最短路徑,我們可以把圖中的邊變為反向邊,這樣問題就轉化為求以1為起點的最短路徑了。
- poj3259
這道題只需要判斷是否存在一個回路,繞著走一圈以後回到過去。
也就是是否存在負權回路的問題,可以用bellman-ford或者spfa。
【最小生成樹】
- kruskal
首先證明,整個圖G權值最小的邊一定在最小生成樹裏面。
我們在將權值最小的邊加入了最小生成樹以後,可以將這條邊所連接的兩個點合成一個點考慮,然後再找下一個權值最小的連接兩個不同點的邊。
以此類推,我們可以把所有的邊按照邊權排序,先插入邊權較小的邊,當某條邊插入時兩端已經在同一個連通塊,就舍棄這條邊,否則就插入這條邊並合並對應的連通塊。
如何判斷兩個點所在的連通塊是否相等,用並查集。
時間復雜度:排序O(|E|log|E|),並查集維護O(|E|)
程序實現(用struct Edge{ int u,v,weight; }表示邊)
sort(edge,edge+m,cmp); // 按邊權從小到大排序
for (i=1;i<=n;i++) fa[i]=i,rk[i]=0;
for (i=0;i<m;i++)
{
tu=top(edge[i].u);
tv=top(edge[i].v);
if (tu==tv) continue;
if (rk[tu]<rk[tv]) swap(tu,tv);
if (rk[tu]==rk[tv]) rk[tu]++;
fa[tv]=tu; ans+=weight;
}
- prim
首先證明,對於某個結點來說,以其為端點的邊當中,權值最小的一條邊一定在最小生成樹中。(當權值最小的有多條的時候,每一條都存在某棵最小生成樹包含之)
也就是說,我們可以從一個結點出發,在相鄰的邊當中選擇一條權值最小的,加入最小生成樹,然後將整個連通塊當成一個結點,對外再選下一條權值最小的邊。
以此類推,我們可以仿照dijkstra中對結點最短距離的維護,只是我們這次維護的是當前連通塊連到這個結點的邊中權值最小的一條。
不使用堆優化,時間復雜度O(|V|2)
使用堆優化,時間復雜度O(|E|log|V|+|V|)
Prim算法不加堆優化(使用鄰接矩陣w[ ][ ])
for (i=1;i<=n;i++) dist[i]=+inf,closed[i]=0;
dist[s]=0; closed[s]=1; nd=s;
for ( ; nd!=t ; ) {
for (i=1;i<=n;i++)
if (dist[i]>w[nd][i])
dist[i]=w[nd][i];
nd=t;
for (i=1;i<=n;i++)
if (!closed[i]&&dist[i]<dist[nd]) nd=i;
closed[nd]=1; ans+=dist[nd];
}
Prim算法加堆優化
priority_queue<pair<int,int>> que; while (!que.empty()) que.pop();
for (i=1;i<=n;i++) dist[i]=+inf; dist[s]=0; que.push(make_pair(0,s));
while (!que.empty()) {
int nd=que.top().second; que.pop();
if (closed[nd]) continue; closed[nd]=1; ans+=dist[nd];
for (p=fir[nd];p!=0;p=p->next)
if (dist[p->to]>p->w) {
dist[p->to]=p->w;
que.push( make_pair(-dist[p->to],p->to) );
}
}
***********************應用*******************************
1poj1287
這一題,最小生成樹水題,對最小生成樹不熟悉的同學可以用這道題目練手。
2.在二維坐標系上有n個點,現在要給這些點之間連上線段,使得所有n個點連通的情況下,線段的長度和最小
Ans: 將每個點對之間的距離全部求出來以後,問題轉換為最小生成樹。
【拓撲排序】
只有有向無環圖(DAG)有拓撲排序
*****************建拓撲排序的方法*********************
1.檢查入度法
如果一個活動入度為0,那就表示這個活動沒有前置活動,可以放在序列的最前面。將這個活動放入拓撲序列中,並且將其出度全部刪除,以找到一個新的入度為0 的結點。
檢查入度法的時間復雜度O(n+m)
檢查入度法 //通過鄰接鏈表組織起來的邊edge[1..m]
for (i=1;i<=m;i++) rd[edge[i].to]++;
t=0; for (i=1;i<=n;i++) if (rd[i]==0) que[t++]=i;
for (h=0;h<t;h++) { //que表示拓撲順序
u=que[h];
for (p=fir[u];p!=0;p=p->next) {
rd[p->to]--;
if (rd[p->to]==0) que[t++]=p->to;
}
}
- 深度搜索法
********************補充定義內容***********************
深度優先搜索
在深度優先搜索的過程中,所有的結點分為3類,用vis值表示
vis=0表示該結點還沒訪問過,
vis=1表示該結點訪問過,但還沒從該點返回(子孫結點還沒訪問完)
vis=2表示該結點已經返回了
此外,每個結點賦予一個dfn值,表示結點的入棧順序
對於邊來說,邊分為樹邊,前向邊,後向邊和橫叉邊,其中
樹邊指的是從父節點找到子結點所用的邊
前向邊指的是從祖先結點指向其子孫結點的非樹邊
後向邊指的是從子孫結點往回指向祖先結點的邊
橫叉邊指的是沒有祖孫關系的兩個結點之間的邊,起點的dfn值排在終點後面
在進行dfs的時候,我們對這些邊有不同的處理方式
****************************補充完畢******************。
我們從任意的結點出發,進行深度搜索,找任意一個vis值<2的相鄰結點遞歸下去,如果我們找到了一個vis值=1的結點,就表示我們找到一個環。
當某個結點不存在相鄰結點,或者相鄰結點全部vis值為2的時候,這個結點為拓撲序列的最後一位,然後返回。
說人話:先遞歸到最後一層,如果最後的點沒有出度了,這個點就是拓撲序的最後一個,存入,然後倒敘輸出。
如果出發的結點返回後仍未加入所有結點,找一個未加入的結點進行上面的操作,直到發現環或者加入所有結點為止。時間復雜度O(n+m)。
int dfs(int nd)
{
vis[nd]=1;
for(each v adjacent to nd)
{
if(vis[v]==1) return false;
if(vis[v]==0)
if(!dfs(v)) return false;
}
vis[nd]=2;
que[t++]=nd;
return true;
for(i=1;i<=n;i++)
if(vis[i]==0)
if(!dfs(i)) break;
}
************************應用***************************
1.給出一個只含有負權邊的圖G(|V|<2000000,|E|<3000000),求所有以任意頂點為起點的最短路徑長度的最小值。
Ans:
這一題用前面提到的最短路徑一定會超時。
首先如果這圖有環,那麽就存在負權回路,答案為-∞。
否則可以對這圖進行拓撲排序,用dist[v]表示以任意起點以結點v結尾的最短路長度。
然後在進行了拓撲排序的前提下,我們發現,松弛操作只可能是前面的結點對後面的結點進行松弛。所以我們按照這個序列的先後順序,一個個往後松弛,最後得到以所有結點結尾的最短路長度。
時間復雜度O(|V|+|E|)
2.給出一個有向無環圖(|V|<2000000,|E|<3000000),頂點v處有kv元錢,現在,任意選定出發的頂點,並在任意終點結束,求最多能從該圖收集到多少錢?
Ans:
首先對這一題進行拓撲排序,按照所得序列的順序,計算到達每個頂點時能收集到的最多的錢。
用dp[v]表示走到v最多能夠得多少錢,然後我們列出狀態轉移方程:
dp[v]=max(dp[v],dp[u]+k[v])
時間復雜度O(|V|+|E|)
3.給出一個有向圖(|V|<2000000,|E|<3000000),頂點v處有kv元錢,現在,任意選定出發的頂點,並在任意終點結束,求最多能從該圖收集到多少錢?
Ans:
這一題需要學習強連通分量才能解答,我們可以把這個有向圖的強連通分量縮成一個點,這個圖就變成DAG了。
總的時間復雜度O(|V|+|E|)。
【連通分量】
無向圖G=(V,E)是連通的,當且僅當其中任意兩個結點能互相到達,如果G是有向圖,這種情況稱G是強連通的。
對於一個連通圖G來說,如果刪掉了邊(u,v)以後,會使得圖不再連通,那麽稱這條邊(u,v)為橋(也稱割邊)
如果一個連通圖不包含橋的話,就意味著這個連通圖無法僅靠刪除一條邊變為不連通圖,此時稱這個連通圖為雙連通圖。
對於圖G的子圖G’來說,如果G’連通,那麽稱G’為G的連通子圖。
如果圖G的某個連通子圖G’的頂集不是其余連通子圖G’’的點集的真子集,那麽稱G’為G的一個連通分量。
對於強連通圖和雙連通圖,也有類似的性質。
【tarjan】求強聯通分量
tarjan算法借助dfs過程中產生的各種類型的邊,旨在為每一個結點通過後向邊找到dfs樹下的子結點最多能夠追溯到高度多低的祖先,從而確定強連通分量的範圍。
我們記錄dfn,表示結點的訪問順序,然後額外地記錄low表示以自身為根的子樹能夠到達的最小高度的祖先。
如左圖所示,2號結點有一個後代有直接連向1號點的邊
所以low[2]=1
操作:在進行dfs的時候,我們不斷把和上面失去聯系(dfn[nd]=low[nd])的子樹從樹上刪除,把它們劃分為獨立一個強連通分量,那麽怎麽維護low值呢?
對於當前結點nd,枚舉結點v
如果vis[v]=0,訪問v,在v返回的時候,如果v沒有失去聯系,那麽low[v]表示的v能到達的祖先,nd也能到達,我們有low[nd]=min(low[nd],low[v])
如果v訪問過,而且沒被切出去,那麽我們有low[nd]=min(low[nd],dfn[v])
當nd訪問完所有結點,而且dfn[nd]==low[nd],那麽就意味著nd與上面失去了聯系,刪除該子樹並將其結點劃分為一個強連通分量
我們劃分好強連通分量的時候,就意味著不同的強連通分量u和v之間不可能互相到達,這時候將所有的強連通分量縮成一個點,就能得到一個有向無環圖。
然後配合拓撲排序,就能找到那些互為前提的問題,用另外的方法解決。(拓撲排序例題3)
連向1號點的邊
所以low[2]=1
操作:在進行dfs的時候,我們不斷把和上面失去聯系(dfn[nd]=low[nd])的子樹從樹上刪除,把它們劃分為獨立一個強連通分量,那麽怎麽維護low值呢?
對於當前結點nd,枚舉結點v
如果vis[v]=0,訪問v,在v返回的時候,如果v沒有失去聯系,那麽low[v]表示的v能到達的祖先,nd也能到達,我們有low[nd]=min(low[nd],low[v])
如果v訪問過,而且沒被切出去,那麽我們有low[nd]=min(low[nd],dfn[v])
當nd訪問完所有結點,而且dfn[nd]==low[nd],那麽就意味著nd與上面失去了聯系,刪除該子樹並將其結點劃分為一個強連通分量
我們劃分好強連通分量的時候,就意味著不同的強連通分量u和v之間不可能互相到達,這時候將所有的強連通分量縮成一個點,就能得到一個有向無環圖。
然後配合拓撲排序,就能找到那些互為前提的問題,用另外的方法解決。(拓撲排序例題3)
國慶七天樂——第六天