10.23 閒話 圖論複習
還有2天就複賽了,現在暫時不知道做啥題了,寫一下這兩天覆習的圖論知識。
1.存圖方式
(1.) 鄰接矩陣
沒什麼好說的,最簡單的存圖方式,一眼就會。
定義矩陣陣列 \(a[n][n](n為點的數量數)\) ,\(a[u][v]=w\) 代表 \(u,v\) 之間存在一條權值為 \(w\) 的路徑。
由於採用二維陣列存圖,導致其在稠密圖中效率低下,會有很多空間被浪費掉,限制了其發揮。
(2.) \(vector\)
動態陣列存圖,比較好寫也比較常用的一種方式,定義的話為 \(vector<資料型別>a[n](n為點的數量)\) 對於 \(a[u][i]=v\) 來說,其代表點 \(u\) 的第 \(i\) 條邊的終點是 \(v\) ,若需儲存其權值,則需要改變其資料型別,使用結構體型別。
儲存權值的方式以及遍歷點 \(i\) 的所有出度的方式。
struct node{
int id,w;
};
vector<node>a[maxn];
//向點u壓入一條權值為w的通往v的出度
a[u].push((node){v,w});
for(int j=0;j<a[i].size();j++){
//所有的a[i][j]即為其所有出度
}
比起鄰接矩陣存圖,其省去了大量的冗餘空間儲存不存在的邊。故其在稠密圖的表現明顯優於鄰接矩陣。
(3.)鏈式前向星
學的不太好,可能講起來會比較抽象。
建立結構體陣列 \(e[m](m為邊的數量)\) 結構體的變數為 \(to(邊的終點),next(其同起點的一個兄弟),w(邊的權值)\) ,與一個 \(head\) 陣列, \(head[i]\) 表示 \(i\) 點的最近一條連邊。
儲存方式及遍歷方法:
struct edge{
int to,next,w;
}e[m];
int head[n];
void add_edge(int u,int v,int w){ //新增一條由u通向v的權值為w的邊
tot++; //tot為當前邊的編號
e[tot].to=v; //邊的終點
e[tot].next=head[u]; //更新其兄弟
e[tot].w=w;
head[u]=tot; //記錄u點的最近一條出度
}
//遍歷點x的所有出度
for(int i=head[x];i;i=e[i].next){ //最早的點的next值為0
}
較起前兩種,鏈式前向星由於不用儲存起點,只儲存邊,不由點決定,決定其空間利用率極高,是最省空間的存圖方式。
2.拓撲排序
首先闡述其的定義,在一張 \(DAG(有向無環圖)\) 中,若 \(i,j\) 存在一條由 \(i\) 指向 \(j\) 的邊,則稱 \(j\) 依賴於 \(i\) ,則拓撲排序的目的就是使排序後排在前面的點不依賴於後面的點。
可能有點抽象,形象點來說,就是在一張由有向無環圖中,輸出入度為零的點,同時將其所連的點的入度減一,重複此過程,直到所有的點都已被輸出。這也點出了我們的解決思路,佇列儲存入度為零的點,當佇列非空時,每次取出隊首,遍歷其所有出度,將其能到達的點的入度減一,若減為零,則將其加入佇列,否則繼續遍歷。
int idx[maxn]; //入度陣列
vector<int>e[n];
queue<int>qu;
for(int i=1;i<=n;i++){
if(idx[i]==0){
qu.push(i);
cout<<i<<" ";
}
}
while(!qu.empty()){
int op=qu.top();qu.pop();
for(int i=0;i<e[op].size();i++){
int k=a[op][i];
idx[k]--;
if(idx[k]==0){
qu.push(k);
cout<<k<<" ";
}
}
}
3.最短路
很經典的圖論問題,也是很常考的知識點。求圖上兩點的最短路,分為全源最短路及單源最短路兩種。
(1.)全源最短路:
全源最短路常使用 \(Floyd\) 演算法,其主要思想是透過列舉中繼點來縮小路徑長度,複雜度為 \(O(n^3)\) 很差,不過相較於對每個點進行一遍尋找單源最短路, \(Floyd\) 演算法還是佔優勢的。
主要採用 \(dp\) 的思想 \(dp[k][i][j]=min(dp[k-1][i][j],dp[k-1][i][k]+dp[k-1][k][j])\) 三維陣列效率有點低下,所以使用滾動陣列,最佳化掉第一維 \(dp[i][j]=max(dp[i][j],dp[i][k]+dp[k][j])\) 當然由於使用了滾動陣列的原因,導致列舉 \(k\) 的迴圈必須在 \(i\) , \(j\) 迴圈的外層。
void flord(){
for(int k=1;k<=n;k++){
for(int i=1;i<=n;i++){
for(int j=1;j<=n;j++){
dp[i][j]=min(dp[i][j],dp[i][k]+dp[k][j]);
}
}
}
}
//則dp[i][j] 為i點到j點的最短路
(2.)單源最短路
這裡是 \(dijkstra\) 演算法,一種透過貪心來確定最短路的演算法。只能用於無負邊權值的圖。使用優先佇列儲存點到起點的距離,對於一個已經在優先佇列中的點,若期之前未被遍歷過,則遍歷其所有出度,更新其可到達的點,若更新後距離更短,則更新距離。
struct edge{
int u,v;
int w;
};
vector<edge>e[maxn]; //vector存圖
struct node{
int id,dis;
bool operator < (const node &x)const{
return dis<x.dis;
}
}
int dis[maxn]; //dis記錄點i到起點的距離
bool vis[maxn]; //vis代表是否已被確定
void dijkstra(){
for(int i=1;i<=n;i++){
dis[i]=inf;
vis[i]=false;
}
dis[s]=0;
priority_queue<node>pq;
pq.push((node){s,0});
while(!pq.empty()){
node u=pq.top();pq.pop();
if(vis[u.id]) continue;
vis[u.id]=true;
for(int i=0;i<e[u].size();i++){
edge k=e[op.id][i];
if(vis[k.v]) continue;
if(dis[k.v]>y.w+u.dis){
dis[k.v]=y.w+u.dis;
q.push((node){k.v,dis[y.v]});
}
}
}
}
4.最小生成樹
最小生成樹( \(MST\) ),在圖上,聯通所有點且不含迴路的子圖稱為一顆生成樹,其中邊權和最小的稱為最小生成樹。
這裡使用 \(kruskal\) 演算法,由於需要連線每一個點,所以可以使用貪心的思路,對所有的邊進行排序。選出其中較小的,維護一個並查集,儲存已經加入最小生成樹的點,維護一個點的集合,直到每一條邊都已經遍歷。
struct node{
int u,v,w; //邊集陣列u:起點 v:終點 w:權值
}e[maxn];
bool cmp(node a,node b){return a.w<b.w;} //按權值大小排序
int fa[maxn];
int find(int x){
if(fa[x]==x) return x;
return fa[x]=find(fa[x]);
}
void merge(int x,int y){
int f1=find(x),f2=find(y);
if(fa!=f2){
fa[f1]=f2;
}
}
int kruskal(){
for(int i=1;i<=n;i++) fa[i]=i;
int ans=0;
sort(e+1,e+1+m,cmp);
for(int i=1;i<=m;i++){
int u=e[i].u,v=e[i].v;
if(find(u)==find(v)) continue;
merge(u,v);
ans+=e[i].w;
}
return ans;
}
總結
大概先寫到這,圖論就學到這了,確實不多,還不一定熟練覺得有點懸。
S組複賽祝好。