【清北】【筆記】圖論

歐拉回路

經過整個圖的所有邊的路徑。

連通圖

令無向圖G=(V,E),如果∀x,y∈V,x和y之間都存在路徑。

強連通圖

令有向圖G=(V,E),如果∀x,y∈V,x到y都存在路徑。

圖的遍歷

有2種最簡單的方法:
深度優先搜索(DFS)和廣度優先搜索(BFS)
前者的原則是建立一個棧,只要棧頂結點u還有相鄰的點v未入過棧,就把v入棧,遍歷v,繼續遞歸地搜索,當棧頂結點u的相鄰結點都入過棧時,將u出棧。
後者則是建立一個隊列,每次從隊頭取出一個結點,然後將相鄰的沒進過隊列的結點入隊並遍歷,然後再取出一個結點如此做。

最短路徑問題

性質:
給定s—>t 不存在負權:最短路一定是簡單路徑
從s—>t最短路,走到v0時一定繼承了前面的最短路
每個節點的最短路可以組織成一棵樹

  • Dijkstra算法

這算法的時間主要耗費在兩個地方:鬆弛結點,找最小值。
對於找最小值的部分,如果直接枚舉的話,對於固定完所有結點才能結束的最壞情況,時間複雜度可達到O(|V|2),此時鬆弛操作的時間複雜度爲O(|E|)。總的時間複雜度爲O(|V|2+|E|)。

如果我們用一個高效支持單元素修改,詢問全體最小值的數據結構來記錄(線段樹/堆/優先隊列),那麼在單次修改複雜度變爲O(log|V|)的前提下,單次詢問的時間複雜度爲O(1)。因此總的時間複雜度爲O(|E|log|V|+|V|)。

用pair的原因:自帶比較函數(偷懶)
加堆優化的程序實現(使用鄰接鏈表,優先隊列):

//pair存路徑長度和節點
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 (dist[p->to]>dist[nd]+p->w) {
            dist[p->to]=dist[nd]+p->w;
            que.push( make_pair(-dist[p->to],p->to) );
        }
}

Dijkstra算法的侷限性
經過堆優化的dijkstra算法可以應付|V|和|E|比較大的情況,但是這個算法無法處理負權邊,換句話說,這個算法的貪心的依據在圖中有負權邊的時候不成立。因此在有負權邊的時候,我們需要換用別的方法。

  • Bellman-ford/spfa算法

我們可以採取枚舉的方案。
用一個數組d[ ]記錄從s出發到所有結點當前的最短路徑。
一開始設d[s]=0,然後枚舉所有的邊,利用三角形不等式更新最短距離。
不斷枚舉直到某一次枚舉中沒有結點被更新。
如果沒有負權迴路,算法應當在第|V|次枚舉之前就求出所有結點的最短路徑。
如此做的時間複雜度爲O(nm)。
耗時巨大:用隊列優化—–>spfa。

//鏈表
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; }
        }
}

時間複雜度O(k(n+m)),其中k和最短路經過的路徑條數有關。
如果圖中存在負權迴路,前面的程序會死循環。
如果圖中不存在負權迴路,那麼任何的最短路都會是簡單路徑。而一個階爲n的圖,到一個點的最短路所經過的路徑數量最多爲n-1。如果出現負權迴路,那麼和負權迴路相關的結點必定會無限次經過,只要有一個點入隊超過n次,就可以判斷出現了負權迴路

  • Floyd

這個算法需要依次添加|V|箇中間點,每次需要比較|V|2個結點對,因此時間複雜度爲O(|V|3)。

最小生成樹

  • 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算法加堆優化:

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) );
        }
}

拓撲排序

檢查入度法
如果一個活動入度爲0,那就表示這個活動沒有前置活動,可以放在序列的最前面。將這個活動放入拓撲序列中,並且將其出度全部刪除,以找到一個新的入度爲0 的結點。
檢查入度法的時間複雜度O(n+m)
深度搜索法
我們從任意的結點出發,進行深度搜索,找任意一個vis值<2的相鄰結點遞歸下去,如果我們找到了一個vis值=1的結點,就表示我們找到一個環。
當某個結點不存在相鄰結點,或者相鄰結點全部vis值爲2的時候,這個結點爲拓撲序列的最後一位,然後返回。
如果出發的結點返回後仍未加入所有結點,找一個未加入的結點進行上面的操作,直到發現環或者加入所有結點爲止。時間複雜度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;
    }
}

深度搜索法

bool dfs(int nd)//vis=1被訪問在站裏 2已返回 0未訪問
{
    vis[nd]=1;
    for(each v adjacent to nd)
    {
        if(vis[v]==1) return 0;
        if(vis[v]==0) if(!dfs(v)) return 0;
    }
    vid[nd]=2;
    que[t++]=nd;
    return true;
} 
for(i=1;i<=n;i++)
        if(vis[nd]==0) if(!dfs(i)) break;

例題1
給出一個只含有負權邊的圖G(|V|<2000000,|E|<3000000),求所有以任意頂點爲起點的最短路徑長度的最小值。
解答
這一題用前面提到的最短路徑一定會超時。
首先如果這圖有環,那麼就存在負權迴路,答案爲-∞。
否則可以對這圖進行拓撲排序,用dist[v]表示以任意起點以結點v結尾的最短路長度。
然後在進行了拓撲排序的前提下,我們發現,鬆弛操作只可能是前面的結點對後面的結點進行鬆弛。所以我們按照這個序列的先後順序,一個個往後鬆弛,最後得到以所有結點結尾的最短路長度。
時間複雜度O(|V|+|E|)

例題2
給出一個有向無環圖(|V|<2000000,|E|<3000000),頂點v處有kv元錢,現在,任意選定出發的頂點,並在任意終點結束,求最多能從該圖收集到多少錢?
解答
首先對這一題進行拓撲排序,按照所得序列的順序,計算到達每個頂點時能收集到的最多的錢。
用dp[v]表示走到v最多能夠得多少錢,然後我們列出狀態轉移方程:
dp[v]=min(dp[v],dp[u]+k[v])
時間複雜度O(|V|+|E|)

連通/強連通/雙連通

無向圖G=(V,E)是連通的,當且僅當其中任意兩個結點能互相到達,如果G是有向圖,這種情況稱G是強連通的。

對於一個連通圖G來說,如果刪掉了邊(u,v)以後,會使得圖不再連通,那麼稱這條邊(u,v)爲橋(也稱割邊)
如果一個連通圖不包含橋的話,就意味着這個連通圖無法僅靠刪除一條邊變爲不連通圖,此時稱這個連通圖爲雙連通圖。

連通分量/強連通分量/雙連通分量

對於圖G的子圖G’來說,如果G’連通,那麼稱G’爲G的連通子圖。

如果圖G的某個連通子圖G’的頂集不是其餘連通子圖G’’的點集的真子集,那麼稱G’爲G的一個連通分量。

對於強連通圖和雙連通圖,也有類似的性質。

如何通過一個無向圖求它的連通分量?
簡單,bfs
那麼如何求出強連通分量呢?
emmmmmm
從每個點出發bfs?O(n2)
藉助dfs?tarjan算法?

發佈了68 篇原創文章 · 獲贊 4 · 訪問量 1萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章