一、基本的圖算法
-
存圖
-
鄰接矩陣:
int graph[max_n][max_m];
-
鄰接鏈表:
struct Edge { int v, w; // v是鄰接的結點,w是邊權 }; vector<Edge> g[N]; void add(int u, int v, int w) // 加邊 { Edge edge; edge.v = v, edge.w = w; g[u].push_back(edge); }
-
鏈式前向星存圖:
struct Edge { int v, w; // v是鄰接的結點,w是邊權 int next; // 記錄同起點上一條邊的位置 }; Edge edge[m_max]; // 數組存邊 int cnt=0; // 記錄存入邊的下標 int head[n_max]; // 記錄每個邊的起始點指向的其最后一個終點的那條邊的位置 // 需在main函數里使用循環全部初始化為-1 void add_edge(int u,int v,int w) //起點,終點,權值 { cnt++; edge[cnt].v=v; edge[cnt].w=w; edge[cnt].next=head[u]; head[u]=cnt; // 更新head[u]指向的最末終點所在邊的位置 }
如果讀者仍不明白其原理,可參考這個網址:https://blog.csdn.net/sugarbliss/article/details/86495945
注意:此方法只適用於有向邊,但仍可以在兩點間存兩條方向相反的邊,達到無向的效果。
-
-
BFS
廣度優先搜索-
使用重要數據結構
queue
(其使用方法可參考我上篇博客:https://www.cnblogs.com/yuyi-21/p/15568742.html):void BFS(int s) { queue<int> q; q.push(s); while (!q. empty()) { top = q.front(); // 訪問隊首元素top,訪問可以是任何事情 q.pop(); // 出隊 // 將top的下一層結點中未曾入隊的結點全部入隊並設置為已入隊(鏈式前向星存圖的優勢在此顯現) } }
-
題目練習可跳轉:https://leetcode-cn.com/tag/breadth-first-search/problemset/
-
-
DFS
深度優先搜索-
一般采用遞歸實現:
void DFS(int num,......) // 參數用來表示狀態 { if() // 判斷邊界 { ... return; } if(/*越界或者是不合法狀態*/) return; for() // 有時為if,依照題意 { if() // 若合法 { // 修改操作 // 標記 DFS(); // 往下遍歷,自底向上 // 還原(根據題意,有回溯要求時才需還原標記) } } }
-
典型例題(全排列):
void DFS(int x) { if(x==len) { // 打印並return } for(int i=0; i<len; i++) { if(vis[i]==0) // 如果沒有被標記 { ans[x] = a[i]; // ans記錄最終排列數組,a為讀入的原數組 vis[i]=1; // 標記已被使用 DFS(x+1); vis[i]=0; // 取消標記 } } }
-
其它題目可跳轉:https://leetcode-cn.com/tag/depth-first-search/problemset/
-
-
拓撲排序
- 拓撲排序的原理(頂點表示活動,有向邊表示頂點之間的優先關系):
(1)任意選擇一個沒有前驅的頂點;
(2)去掉該頂點以及以該頂點為出發點的所有邊;(用鏈式前向星存圖!)
(3)重復上述過程,直到所有頂點被去掉(或還有頂點,但不存在入度為0的邊——AOV網存在回路)。
為了方便,使用優先隊列(類型依據題意):
priority_queue <int,vector<int>,less<int> > big_out;
(大頂堆)、priority_queue <int,vector<int>,greater<int> > small_out;
(小頂堆)(當然,對於大頂堆,你也可以這樣定義:
priority_queue<int> a;
)-
代碼模型:
for(int i=1;i<=n;i++) { if(in[i]==0) // 輸入時用in記錄每個點的入度,加入所有入度為0的點進入優先隊列 big_out.push(i); } while(!big_out.empty()) // 為空說明已經找完所有點,跳出循環 { int temp=big_out.top(); // 彈出隊列中的首元素(按照題意,由大到小輸出,可以變通) big_out.pop(); // 出隊 // 對temp按照題意進行操作 for(int k=head[temp];k!=-1;k=edge[k].next) // 鏈式前向星圖的遍歷,刪除所有連接的邊 { in[edge[k].to]--; // 所有終點入度-1 if(in[edge[k].to]==0) big_out.push(edge[k].to); // 如果入度變為0,加入隊列 } head[temp]=-1; // 將該起點標記為“空巢” }
強連通分量此處暫鴿,以后有時間再來更吧。若有需要可參考《算法導論》p357。
二、最小生成樹:
何為最小生成樹?再一次引用百度百科的話就是:一個有 n 個結點的連通圖的生成樹是原圖的極小連通子圖,且包含原圖中的所有 n 個結點,並且有保持圖連通的最少的邊(也就是n-1條邊)。
接下來介紹兩個偉大的算法:kruskal
算法 & prim
算法
-
kruskal
算法(稀疏圖時推薦使用)基本原理:假設連通網G =(V,E),在E中選擇代價最小的邊,若該邊依附的頂點分別在T中不同的連通分量上,則將此邊加入到T中;否則,舍去此邊而選擇下一條代價最小的邊。依此類推,直至T中所有頂點構成一個連通分量為止(可用已經加入了n-1條邊來判定)。
-
代碼模型:
struct Edge{ int from; int to; int w; }; // 結構體存邊 Edge edge[m_max]; int fa[n_max]; // 存每個結點所在連通圖的祖先(通過找是否擁有共同祖先來判斷兩者是否在同一個連通圖) // 需要在main函數中全部初始化為自身,即fa[i] = i int find_fa(int t) // 找共同祖先的函數 { if(fa[t]==t) //如果找到頭了,就返回祖先 return t; else return find_fa(fa[t]); } int main() { // 使用qsort或其它排序方法從小到大排序 for(int i=0; i<m; i++) //遍歷每條邊 { a=find_fa(edge[i].from); b=find_fa(edge[i].to); if(a!=b) // 如果兩點分屬兩個不同連接子圖,則將該邊加入圖中 { fa[a]=b; // 更新祖先的值 // 對加入的邊,依據題意,做一些操作 cnt_edge++; } if(cnt_edge==(n-1)) // 如果已經加到n-1條邊,便說明已找到最小生成樹,跳出循環 break; } }
-
-
prim
算法(稠密圖時推薦使用)基本原理:假設連通網G =(V,E),①依次在G中選擇一條一個頂點僅在V中,另一個頂點在U中(U為已加入最小生成樹的點),並且權值最小的邊加入集合TE,②同時將該邊僅在V中的那個頂點點入集合U。重復上述過程n-1次,使得u=v,此時T為G的最小生成樹。
-
代碼模型:
void Prim(int n) { int lowcost[MAXINT]; // 未確定生成樹的頂點至已確定生成樹上頂點的邊權重 int closest[MAXINT]; // 前驅結點(最靠近的已確定生成樹上的頂點) bool s[MAXINT]; // 是否已加入生成樹 s[0]=true; // 初始化第一個結點已找到 for(int i=1;i<=n;i++) { lowcost[i]=c[0][i]; // 存放時記得初始化邊權為無窮大 closest[i]=0; s[i]=false; } // 初始化,默認前驅結點都是第一個結點 /*依照題意,對第一個點0的操作*/ for(int i=0;i<n;i++) { int min=INF; // 臨時最小值 for(int k=1;k<=n;k++) // 找還未加入的滿足算法的最小邊 { if((lowcost[k]<min) && (!s[k])) { min = lowcost[k]; int j=k; // 存放當前找到的新結點 } } /*依照題意,對找到的點j的操作*/ s[j]=true; // 將找到的點標記一下 for(k=1;k<=n;k++) // 遍歷其它為找過的點,會不會因為新點的加入而有更小的邊權 { if((c[j][k]<lowcost[k]) && (!s[k])) { lowcost[k]=c[j][k]; closest[k]=j; } } } }
-
三、所有結點對的最短路徑問題:
給一個圖,不一定要對所有點進行操作。如果想找到圖中任意兩個點,得到它們之間的最短路徑,那么前面的算法都不適用了。
接下來,就將介紹一種解決此類問題的算法——Floyd-Warshall
算法:
這是一種利用動態規划的思想尋找給定的加權圖中多源點之間最短路徑的算法,即插入第三個點,看是否可能通過這個點,縮短原來兩點間的距離。對於每一個插入點,都需要遍歷更新一遍圖,所以時間復雜度為O(V^3)
。因為對於每兩個點的最短路徑,題目都有可能要求輸出,故采用鄰接矩陣存圖。
如果仍不明白的話,可以移步這篇博客:https://blog.csdn.net/yuewenyao/article/details/81021319,講的很清楚。
代碼模板:
long long node_edge[n_max][n_max];
int main()
{
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
node_edge[i][j]=INF; // 將最短路徑初始化為無限大
for(int i=1;i<=m;i++)
{
cin>>x>>y>>z;
if(node_edge[x][y]!=INF) // 存邊,對於可能有多重邊的情況,我們只保留最小邊
node_edge[x][y]=(node_edge[x][y]<=z? node_edge[x][y]:z);
else
node_edge[x][y]=z;
}
for(int i=1;i<=n;i++)
node_edge[i][i]=0; // 對角線上均為點,故最短路徑初始化為0
long long temp=0; // 存放臨時插入點后的路徑長度
for(int k=1;k<=n;k++) // 遍歷插入點
{
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++) // 遍歷圖
{
if(node_edge[i][k]==INF || node_edge[k][j]==INF)
continue; // 本沒有運算的必要,但可能因為溢出,反而被更新為錯誤的值,故直接排除
temp=node_edge[i][k]+node_edge[k][j];
if(temp<node_edge[i][j]) //如果更小,則更新對應的最短路徑
node_edge[i][j]=temp;
}
}
}
四、最大流:
這類題目是要干什么呢?題目給定指定的一個有向圖,其中有兩個特殊的點源S(Sources)和匯T(Sinks),每條邊有指定的容量(Capacity),求滿足條件的從S到T的最大流(MaxFlow)。它的應用面很廣,比如:開閘放水。
廢話不多說,先來了解幾個概念:
-
3個基本性質:
-
容量限制(流量不能大於容量):f(u,v)≤c(u,v);
-
反對稱性(從u到v的流量一定是從v到u的流量的相反值):f(u,v) = -f(v,u);
-
流守恆性(流入u的流量和u點流出的流量相等):∑f(u,v)=0 ,v代表相鄰結點。
-
-
殘留網絡:
殘留網絡 = 容量網絡 - 流量網絡
-
增廣路:
存在一條從s到t的有向通路。
顯然只要殘量網絡中存在增廣路,流量就可以增大;反之,如果不存在增廣路,流量就已經最大。
朴素的預留推進算法的復雜度較高,且寫法較長。這里推薦使用更高效的Dinic
算法或者Isap
、Sap
算法。
-
Dinic
算法關鍵思路:①存圖的時候存入一條正向邊、一條反向邊(鏈式前向星法存圖),為了方便,不妨讓所有的偶數邊為正向
i
,奇數邊為負向i^1
。因為找到的增廣路不一定最優,反邊給了我們后悔的機會,最初應初始化為0(不會對流量造成影響),同時,為了保證原點和匯點流量不變,當正向邊減去d,反向邊應當加上d。②BFS
分層(檢查有無增廣路的時候順便分層),DFS
增廣,循環往復。實現一次找到多條增廣路。代碼模板:
// 初始化 cnt=1; head[n_max]=0; int cur[n_max]; // 此數組用於優化,由於DFS中先被遍歷到的邊已經增廣過或確定無法繼續增廣了,那么下次再到達該節點時,不妨跳過廢邊,只走有用的邊(當前弧優化) void add_edge(int a,int b,int c) // 為了方便,不妨直接將加邊封裝成一個函數 { cnt++; edge[cnt].to=b; edge[cnt].w=c; edge[cnt].next=head[a]; head[a]=cnt; return; } int main() { // 讀入部分省略 while(BFS()) // 如果還有增廣路,則繼續增廣 DFS(s,INF); // 第一個元素為當前點,第二個元素為當前增廣路上的最小邊權:初始化INF=0x3f3f3f3f cout<<max_flow<<endl; return 0; } bool BFS() // 借助數據結構queue { for(int i=0; i<=n; i++) { cur[i] = head[i]; // 尚無廢邊,初始化為head的起點 depth[i] = INF; // 初始化深度為INF inque[i] = 0; // 初始化不在隊列中 } depth[s]=0; // 源點深度為0 queue<int> q; q.push(s); int temp; // 存放隊列首元素 while(!q.empty()) { temp=q.front(); q.pop(); inque[temp]=0; // 標記不在隊列中 for(int i=head[temp];i>0;i=edge[i].next) // 鏈式前向星遍歷 { int towards=edge[i].to; // 當前以temp為起點的邊的終點 if(edge[i].w>0 && (depth[towards]>(depth[temp]+1))) { depth[towards]=depth[temp]+1; // 每多走一個點,層數+1,為了之后DFS簡化操作 // 之后如果depth[towards]==(depth[temp_site]+1),我們就可以判斷該路徑在一條最短增廣路上 if(!inque[towards]) // 如果該點不在隊列中,讓它入隊 { q.push(towards); inque[towards]=1; } } } } if(depth[t]!=INF) // 如果終點值的層數被更新了,說明有一條增廣路,反之則沒有 return 1; else return 0; } // 於是我們再來一波深搜,更新最大流 int DFS(int temp_site,int min_left) { if(temp_site==t) // 到達匯點,可以使用當前路徑,最小邊權min_left有效 { max_flow += min_left; return min_left; } int used=0; // 表示這個點的流量用了多少,如果use還沒有找到流量上限,則可以繼續找別的增廣路 int temp_min=0; // 增廣路最小殘余流量 for(int i=cur[temp_site];i>0;i=edge[i].next) // 從cur[temp_site]開始遍歷,略過已經遍歷過的廢邊 { int towards=edge[i].to; // 目標點 cur[temp_site]=i; if(edge[i].w>0 && (depth[towards]==(depth[temp_site]+1))) // 根據BFS的分層,尋找最短的可增廣路徑 { if (temp_min=DFS(towards,min(min_left-used,edge[i].w))) // 深搜,找該增廣路徑的最小殘余流量,且不為0時進行下面的操作 { used += temp_min; // 該點流量增加 edge[i].w -= temp_min; edge[i^1].w += temp_min; // 正向邊加流,反向邊減流 if (used==min_left) //已到達流量上限 break; } } } return used; }
-
Isap
算法讀者可移步該博客學習:https://www.cnblogs.com/scx2015noip-as-php/p/MFP.html
-
題目練習可跳轉(模板題):https://www.luogu.com.cn/problem/P3376