圖論是NOIP必考的知識點。
松弛操作
如圖:
比如說從1到2可以有2種解法,一種是直接走,另一種就是用一個點來中轉;
從這兩條路上選最短的走法的操作就叫松弛。
根據這個操作啊就可以做出像暴力一樣的最短路算法————Floyd算法.
我們可以先初始化把不相連的邊都設為無窮大,再不斷進行松弛操作不斷更新最短路。
這樣就可以得出所有的兩點之間的最短路,還能處理負邊權。
不過就是有點慢時間復雜度是O(n3)
for(k=1;k<=n;k++) //中轉點
for(i=1;i<=n;i++)
for(j=1;j<=n;j++)
if(dis[i][j]>dis[i][k]+dis[k][j]) //松弛操作
dis[i][j]=dis[i][k]+dis[k][j];
但是該算法適用於求解多源最短路徑,所以時間復雜度大也是正常的。
而單源最短路徑主要有兩種
Dijkstra算法O(n2)加堆優化O(nlogn)
用來計算從一個點到其他所有點的最短路徑的算法。
Dijkstra它不能處理存在負邊權的情況。
算法描述:
設起點為s,dis[v]表示從s到v的最短路徑,。
a)初始化:dis[v]=∞(v≠s); dis[s]=0;;
b)For (i = 1; i <= n ; i++)
1.在沒有被訪問過的點中找一個頂點u使得dis[u]是最小的。(可以認為是貪心操作)
2.u標記為已確定最短路徑的點
3.與u相連的每個沒有被確定最短路徑的頂點進行松弛操作。
算法思想:我們把點分為兩類,一類是已確定最短路徑的點,稱為“白點”,另一類是未確定最短路徑的點,稱為“藍點”。如果我們要求出一個點的最短路徑,就是把這個點由藍點變為白點。從起點到藍點的最短路徑上的中轉點在這個時刻只能是白點。
Dijkstra的算法思想,就是一開始將起點到起點的距離標記為0,而后進行n次循環,每次找出一個到起點距離dis[u]最短的點u,將它從藍點變為白點。隨后枚舉所有的藍點vi,如果以此白點為中轉到達藍點vi的路徑dis[u]+w[u][vi]更短的話,這將它作為vi的“更短路徑”dis[vi](此時還不確定是不是vi的最短路徑)。
就這樣,我們每找到一個白點,就嘗試着用它修改其他所有的藍點。中轉點先於終點變成白點,故每一個終點一定能夠被它的最后一個中轉點所修改,而求得最短路徑。
例題:
luogu p[3371]
#include<iostream> #include<cstdio> #include<algorithm> using namespace std; bool b[500010]; long long dis[500010],lin[500010],tot,n,m,s; struct cym{ int from,to,len,next; }e[2000010]; int main() { scanf("%d%d%d",&n,&m,&s); for(int i=1;i<=n;i++) dis[i]=2147483647; for(int i=1;i<=m;i++) { int a,b,c; scanf("%d%d%d",&a,&b,&c); e[++tot].from=a; e[tot].to=b; e[tot].len=c; e[tot].next=lin[a]; lin[a]=tot; } dis[s]=0; for(int i=1;i<=n;i++) { int minn=2147483647; int k=0; for(int j=1;j<=n;j++) if(minn>dis[j]&&!b[j]) { minn=dis[j]; k=j; } b[k]=1; for(int j=lin[k];j;j=e[j].next) if(dis[e[j].to]>dis[k]+e[j].len) dis[e[j].to]=dis[k]+e[j].len; } for(int i=1;i<=n;i++) { printf("%lld ",dis[i]); } }
除了這種算法,還有兩個思想相同但速度不一樣的算法。
一個是SPFA,一個是Bellman_ford算法。
這兩種算法的思想都一樣,但是SPFA是有隊列優化的,所以介紹SPFA算法。
也是一個單源最短路徑算法,但是不同的是他的速度一般是要比dijkstra要快的,且它可以處理負邊權,甚至還可以判負環,但是容易被卡,所以如果在比賽中時間真的充足的話,還是建議寫堆優化dijkstra。
算法描述:
設起點為s,dis[v]表示從s到v的最短路徑,vis[i]數組表示i是否在隊中。
a)初始化:dis[v]=∞(v≠s); dis[s]=0;
將s入隊,vis[i]=1.
b)while(!q.empty())
1.取出隊首u,並將vis數組設為零。
2.與u相連的每個沒有被確定最短路徑的頂點進行松弛操作。
3.如果被確定最短路徑的頂點沒有在隊中,入隊。
算法思想:動態逼近法
設立一個先進先出的隊列用來保存待優化的結點,優化時每次取出隊首結點u,並且用u點當前的最短路徑估計值對離開u點所指向的結點v進行松弛操作,
如果v點的最短路徑估計值有所調整,且v點不在當前的隊列中,就將v點放入隊尾。這樣不斷從隊列中取出結點來進行松弛操作,直至隊列空為止。
代碼(題目同上):
#include<iostream> #include<cstdlib> #include<cstdio> #include<algorithm> #include<queue> #include<cstring> using namespace std; queue<long long>q; long long v[1000010],minn[1000100]; long long n,m,s,lin[1000010],tot=0; struct min_road{ long long from,to,next,len; }e[1000010]; void add(long long f,long long t,long long l) { e[++tot].from=f; e[tot].to=t; e[tot].len=l; e[tot].next=lin[f]; lin[f]=tot; } int main() { scanf("%lld%lld%lld",&n,&m,&s); for(int i=1;i<=n;i++)minn[i]=2147483647; for(int i=1;i<=m;i++) { long long f,t,l; scanf("%lld%lld%lld",&f,&t,&l); add(f,t,l); } q.push(s); v[s]=1; minn[s]=0; while(!q.empty()) { long long cur=q.front(); q.pop(); v[cur]=0; for(long long i=lin[cur];i;i=e[i].next) { if(minn[e[i].to]>minn[cur]+e[i].len) { minn[e[i].to]=minn[cur]+e[i].len; if(!v[e[i].to]) { q.push(e[i].to); v[e[i].to]=1; } } } } for(int i=1;i<=n;i++) printf("%lld ",minn[i]); }
講完了圖論的最短路算法,還有最小生成樹算法。
如果一個圖有n個點,那么如果有n-1條邊。那么他一定是一棵樹。
反之也成立。
克魯斯卡爾算法即是一種解決最小生成樹的算法。
算法思想:
我們用一種並查集的數據結構,用來判斷該邊是否在生成樹中。
如果要想生成樹最小,即可以貪心將每一條邊的權值都排一下序。
然后逐個判斷是否在樹中,如果沒有就加上,且合並,用並查集維護連通性。
反之就繼續,直到全都判斷完畢或已經出現一棵樹。
代碼(洛谷p3366)
#include<bits/stdc++.h> using namespace std; int fa[200001]; struct edge{ int u; int v; int w; }e[200001]; int cmp(edge a,edge b) { return a.w<b.w; } int find(int x) { return fa[x]==x?x:fa[x]=find(fa[x]); } long long cnt=0; long long ans=0; long long n,m; int main() { cin>>n>>m; for(int i=1;i<=n;i++) fa[i]=i; for(int i=1;i<=m;i++) cin>>e[i].u>>e[i].v>>e[i].w; sort(e+1,e+1+m,cmp); for(int i=1;i<=m;i++) { if(cnt==n-1) break; int x=find(e[i].u); int y=find(e[i].v); if(x!=y) { ans+=e[i].w; fa[y]=x; cnt++; } } if(cnt!=n-1) { cout<<"orz"; return 0; } cout<<ans; return 0; }