清北NOIP训练营集训笔记——图论(提高组精英班)
清北NOIP訓練營集訓筆記——圖論(提高組精英班)
本文摘自清北學堂內部圖論筆記,作者為潘愷璠,來自柳鐵一中曾參加過清北訓練營提高組精英班,筆記非常詳細,特分享給大家!更多信息學資源關注微信訂閱號noipnoi。
最短路徑:
1.Floyd算法(插點法):
通過一個圖的權值矩陣求出它的每兩點間的最短路徑(多源最短路)。
算法描述:
一個十分暴力又經典的DP,假設i到j的路徑有兩種狀態:
①i和j直接有路徑相連:
②i和j間接聯通,中間有k號節點聯通:
假設dis[i][j]表示從i到j的最短路徑,對于存在的每個節點k,我們檢查一遍dis[i][k]+dis[k][j]。
?
//Floyd算法,時間復雜度:O(n^3)?
//Floyd算法,時間復雜度:O(n^3)?
int dis[MAXN][MAXN];
for(k=1;k<=n;k++)//枚舉?
{
? ? for(i=1;i<=n;i++)
? ? {
? ? ? ? for(j=1;j<=n;j++)
? ? ? ? {
? ? ? ? ? ? dis[i][j]=min(dis[i][j],dis[i][k]+dis[k][j]);//DP
? ? ? ? }
? ? }
}
【清北2019NOIP夏令營助你圓夢OI】
2.Dijkstra算法(無向圖,無負權邊):
算法描述:
多源最短路!
a.初始時,S只包含源點,即S={v},v的距離為0。U包含除v外的其他頂點,即:U={其余頂點},若v與U中頂點u有邊,則<u,v>正常有權值,若u不是v的出邊鄰接點,則<u,v>權值為∞。
b.從U中選取一個距離v最小的頂點k,把k,加入S中(該選定的距離就是v到k的最短路徑長度)。
c.以k為新考慮的中間點,修改U中各頂點的距離;若從源點v到頂點u的距離(經過頂點k)比原來距離(不經過頂點k)短,則修改頂點u的距離值,修改后的距離值的頂點k的距離加上邊上的權。
d.重復步驟b和c直到所有頂點都包含在S中。
啊~上面的的亂七八糟的概念太難懂了,還是舉個例子吧!如下圖!
?
我們假設1號節點為原點。
第一輪,我們可以算出2,3,4,5,6號節點到原點1的距離為[7,9,∞,∞,14],∞表示無窮大(節點間無法直接連通),取其中最小的7,就確定了1->1的最短路徑為0,1->2的最短路徑為7,同時取最短路徑最小的2節點為下一輪的前驅節點。
第二輪,取2節點為前驅節點,按照 前驅節點到原點的最短距離 + 新節點到前驅節點的距離 來計算新的最短距離,可以得到3,4,5,6號節點到原點1的距離為[17,22,∞,∞](新節點必須經過2號節點回到原點),這時候需要將新結果和上一輪計算的結果比較,3號節點:17>9,最短路徑仍然為9;4號節點:22<∞,更新4號節點的最短路徑為22,;5號節點:仍然不變為∞;6號節點:14<∞,更新6號節點的最短路徑為14。得到本輪的最短距離為[9,22,∞,14],1->3的最短路徑為9,同時取最短路徑最小的3節點為下一輪的前驅節點。
第三輪:同理上,以3號節點為前驅節點,可以得到4,5,6號節點到原點1的距離為[20,∞,11],根據最短路徑原則,和上一輪最短距離比較,刷新為[20,∞,11],1->3->6的最短路徑為11,同時取最短路徑最小的6節點為下一輪的前驅節點。
第四輪:同理,得到4,5號節點最短距離為[20,20],這兩個值相等,運算結束,到達這兩個點的最短距離都是20,如果這兩個值不相等,還要進行第五輪運算!
#include<cstdio>
#include<cstring>
const int N=100500;
const int M=200500;
int point[N]={0},to[M]={0},next[M]={0},len[M]={0},cc=0;
int dis[N];//最短路長度,dis[i]表示第i號點到源點(1號點)的最短距離
bool ever[N];//當前節點最短路有沒有確定?
int n,m;?
void AddEdge(int x,int y,int z)//添加新的邊和節點:x到y邊長z
{
? ? cc++;
? ? next[cc]=point[x];
? ? point[x]=cc;
? ? to[cc]=y;
? ? len[cc]=z;//len記錄x到y的邊長?
}
int main()
{
? ? int i,j,k;
? ? scanf("%d%d",&n,&m);
? ? for(i=1;i<=m;i++)
? ? {
? ? ? ? int a,b,c;
? ? ? ? scanf("%d%d%d",&a,&b,&c);
? ? ? ? AddEdge(a,b,c);//無向圖,要加兩遍?
? ? ? ? AddEdge(b,a,c);
? ? }
? ? memset(dis,0x3f,sizeof dis);//用極大值來初始化?
? ? dis[1]=0;//1號節點到自己最短距離為0?
? ? for(k=1;k<=n;k++)
? ? {
? ? ? ? int minp,minz=123456789;
? ? ? ? for(i=1;i<=n;i++)
? ? ? ? {
? ? ? ? ? ? if(!ever[i])
? ? ? ? ? ? {
? ? ? ? ? ? ? ? if(dis[i]<minz)
? ? ? ? ? ? ? ? {
? ? ? ? ? ? ? ? ? ? minz=dis[i];
? ? ? ? ? ? ? ? ? ? minp=i;
? ? ? ? ? ? ? ? }
? ? ? ? ? ? }
? ? ? ? }
? ? ? ? ever[minp]=1;
? ? ? ? int now=point[minp];
? ? ? ? while(now)
? ? ? ? {
? ? ? ? ? ? int tox=to[now];
? ? ? ? ? ? if(dis[tox]>dis[minp]+len[now])
? ? ? ? ? ? dis[tox]=dis[minp]+len[now];
? ? ? ? ? ? now=next[now];
? ? ? ? }
? ? }
? ? for(i=1;i<=n;i++)
? ? ? ? printf("%d\n",dis[i]);
return 0;
【清北2019NOIP夏令營與你相隨OI之路】
?
3.SPFA算法(有負權邊,無負圈,能檢測負圈但不能輸出):
多源最短路!
SPFA和Dijkstra極為相似,只是加了個隊列優化來檢測負圈和負權邊。
算法描述:
建立一個隊列,初始時隊列里只有起始點,再建立一個表格記錄起始點到所有點的最短路徑(該表格的初始值要賦為極大值,該點到他本身的路徑賦為0)。然后執行松弛操作,用隊列里有的點作為起始點去刷新到所有點的最短路,如果刷新成功且被刷新點不在隊列中則把該點加入到隊列最后。重復執行直到隊列為空。
判斷有無負環:
如果某個點進入隊列的次數超過N次則存在負環(SPFA無法處理帶負環的圖)
#include<cstdio>
#include<cstring>
const int N=100500;
const int M=200500;
int point[N]={0},to[M]={0},next[M]={0},len[M]={0},cc=0;
int dis[N];//最短路長度
int queue[N],top,tail;//雙向隊列queue,隊頭,隊尾?
bool in[N];//記錄這個點在不在隊列中,1表示在,0表示不在?
int n,m; //n個節點,m條邊?
void AddEdge(int x,int y,int z)//x到y邊長為z?
{
? ? cc++;
? ? next[cc]=point[x];
? ? point[x]=cc;
? ? to[cc]=y;
? ? len[cc]=z;
}
int main()
{
? ? int i,j;
? ? scanf("%d%d",&n,&m);
? ? for(i=1;i<=m;i++)
? ? {
? ? ? ? int a,b,c;
? ? ? ? scanf("%d%d%d",&a,&b,&c);
? ? ? ? AddEdge(a,b,c);//因為是雙向隊列,左邊加一次,右邊加一次
? ? ? ? AddEdge(b,a,c);
? ? }
? ? memset(dis,0x3f,sizeof dis);//用極大值來初始化
? ? dis[1]=0;//1號節點到自己最短距離為0?
? ? top=0;tail=1;queue[1]=1;in[1]=1;//初始化,只有原點加入?
? ? while(top!=tail)
? ? {
? ? ? ? top++;
? ? ? ? top%=N;
? ? ? ? int now=queue[top];
? ? ? ? in[now]=0;
? ? ? ? int ed=point[now];
? ? ? ? while(ed)
? ? ? ? {
? ? ? ? ? ? int tox=to[ed];
? ? ? ? ? ? if(dis[tox]>dis[now]+len[ed])
? ? ? ? ? ? {
? ? ? ? ? ? ? ? dis[tox]=dis[now]+len[ed];
? ? ? ? ? ? ? ? if(!in[tox])
? ? ? ? ? ? ? ? {
? ? ? ? ? ? ? ? ? ? tail++;
? ? ? ? ? ? ? ? ? ? tail%=N;
? ? ? ? ? ? ? ? ? ? queue[tail]=tox;
? ? ? ? ? ? ? ? ? ? in[tox]=1;
? ? ? ? ? ? ? ? }
? ? ? ? ? ? }
? ? ? ? ? ? ed=next[ed];
? ? ? ? }
? ? }
? ? for(i=1;i<=n;i++)
? ? ? ? printf("%d\n",dis[i]);
? ? return 0;?
}
4.Bellman Ford算法(有負權邊,可能有負圈,能檢測負圈并輸出):
單源最短路!
算法描述:
1.初始化:將除源點外的所有頂點的最短距離估計值 d[all]=+∞, d[start]=0;
2.迭代求解:反復對邊集E中的每條邊進行松弛操作,使得頂點集V中的每個頂點v的最短距離估計值逐步逼近其最短距離;(運行|v|-1次)
3.檢驗負權回路:判斷邊集E中的每一條邊的兩個端點是否收斂。如果存在未收斂的頂點,則算法返回false,表明問題無解;否則算法返回true,并且從源點可達的頂點v的最短距離保存在 d[v]中。
簡單的說,如下圖所示:
?
松弛計算之前,點B的值是8,但是點A的值加上邊上的權重2,得到5,比點B的值(8)小,所以,點B的值減小為5。這個過程的意義是,找到了一條通向B點更短的路線,且該路線是先經過點A,然后通過權重為2的邊,到達點B
如果出現了以下情況:
?
松弛操作后,變為7,7>6,這樣就不修改(Bellman Frod算法的高妙之處就在這),保留原來的最短路徑就OK,代碼實現起來非常簡單。
int n,m;//n個點,m條邊?
struct Edge//定義圖類型結構體?
{
? ? int a,b,c;//a到b長度為c?
}edge[];
int dis[];
memset(dis,0x3f,sizeof dis);
dis[1]=0;
for(int i=1;i<n;i++)
{
? ? for(int j=1;j<=m;j++)
? ? {
? ? ? ? if(dis[edge[j].b]>dis[edge[j].a]+edge[j].c)
? ? ? ? {
? ? ? ? ? ? dis[edge[j].b]=dis[edge[j].a]+edge[j].c;? ? ? ??
? ? ? ? }
? ? }
}
5.A*算法:
這玩意兒我是沒看懂,等以后我看懂了再更吧(無奈臉)~
七、拓撲排序:
對一個有向無環圖(Directed Acyclic Graph簡稱DAG)G進行拓撲排序,是將G中所有頂點排成一個線性序列,使得圖中任意一對頂點u和v,若邊(u,v)∈E(G),則u在線性序列中出現在v之前。通常,這樣的線性序列稱為滿足拓撲次序(Topological Order)的序列,簡稱拓撲序列。
打個比喻:我們要做好一盤菜名字叫做紅燒茄子,那么第一步得買茄子和配料,第二步就是要洗茄子,第三步就是要開始倒油進鍋里啊什么七七八八的,第四步…,你不可能先洗茄子再買茄子和配料,這樣的一些事件必須是按照順序進行的,這些依次進行的事件就構成了一個拓撲序列。
算法描述:
我們需要一個棧或者隊列,兩者都可以無所謂,只是找個容器把入度為0的元素維護起來而已。
①從有向圖中選擇一個入度為0(無前驅)的頂點,輸出它。
②從網中刪去該節點,并且刪去從該節點出發的所有有向邊。
③重復以上兩步,直到剩余的網中不再存在沒有前驅的節點為止。
具體操作過程如下:
若棧非空,則在棧中彈出一個元素,然后枚舉這個點能到的每一個點將它的入度-1(刪去一條邊),如果入度=0,則壓入棧中。?
如果沒有輸出所有的頂點,則有向圖中一定存在環
//拓撲排序,時間復雜度:O(n+m)?
#include<cstdio>
#include<cstring>
const int N=100500;
const int M=200500;
int point[N]={0},to[M]={0},next[M]={0},cc=0;
int xu[N]={0};//棧,初始值為空,xu[0]表示棧的大小?
int in[N]={0};//入度,a可以到達b,in[b]++?
int ans[N]={0};//ans[0]整個拓撲序列的大小?
int n,m;?
void AddEdge(int x,int y)//鄰接表a到b?
{
? ? cc++;
? ? next[cc]=point[x];
? ? point[x]=cc;
? ? to[cc]=y;
}
int main()
{
? ? int i,j;
? ? scanf("%d%d",&n,&m);
? ? for(i=1;i<=m;i++)
? ? {
? ? ? ? int a,b;
? ? ? ? scanf("%d%d",&a,&b);
? ? ? ? in[b]++;//統計每個節點的入度?
? ? ? ? AddEdge(a,b);
? ? }
? ? for(i=1;i<=n;i++)
? ? {
? ? ? ? if(in[i]==0)//這個節點入度為0,壓入棧?
? ? ? ? xu[++xu[0]]=i;
? ? }
? ? while(xu[0])
? ? {
? ? ? ? int now=xu[xu[0]];//出棧?
? ? ? ? xu[0]--;
? ? ? ? ans[++ans[0]]=now;
? ? ? ? int ed=point[now];
? ? ? ? while(ed)
? ? ? ? {
? ? ? ? ? ? int tox=to[ed];
? ? ? ? ? ? in[tox]--;
? ? ? ? ? ? if(!in[tox])
? ? ? ? ? ? xu[++xu[0]]=tox;
? ? ? ? ? ? ed=next[ed];//找下一個相鄰節點?
? ? ? ? }
? ? }
? ? if(ans[0]<n)//有向圖中一定存在環,無結果?
? ? printf("no solution");
? ? else
? ? {
? ? ? ? for(i=1;i<=n;i++)
? ? ? ? printf("%d ",ans[i]);
? ? }
? ? return 0;
}
?
?
聯通分量:
強連通:有向圖中,從a能到b并且從b可以到a,那么a和b強連通。
強連通圖:有向圖中,任意一對點都滿足強連通,則這個圖被稱為強連通圖。
強聯通分量:有向圖中的極大強連通子圖,就是強連通分量。
一般用Tarjan算法求有向圖強連通分量:
?
歐拉路徑與哈密頓路徑:
1.歐拉路徑:從某點出發一筆畫遍歷每一條邊形成的路徑。
歐拉回路:在歐拉路徑的基礎上回到起點的路徑(從起點出發一筆畫遍歷每一條邊)。
歐拉路徑存在:
無向圖:當且僅當該圖所有頂點的度數為偶數 或者 除了兩個度數為奇數外其余的全是偶數。
有向圖:當且僅當該圖所有頂點 出度=入度 或者 一個頂點 出度=入度+1,另一個頂點 入度=出度+1,其他頂點 出度=入度
歐拉回路存在:
無向圖:每個頂點的度數都是偶數,則存在歐拉回路。
有向圖:每個頂點的入度都等于出度,則存在歐拉回路。
求歐拉路徑/歐拉回路算法常常用Fleury算法:
在這里推薦一個不錯的知乎作者:神秘OIer
2.哈密頓路徑:每個點恰好經過一次的路徑是哈密頓路徑。
哈密頓回路:起點與終點之間有邊相連的哈密頓路徑是哈密頓回路。
?
轉載于:https://www.cnblogs.com/qbxt/p/10981407.html
總結
以上是生活随笔為你收集整理的清北NOIP训练营集训笔记——图论(提高组精英班)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 宜人贷蜂巢API网关技术解密之Netty
- 下一篇: 容器资源需求、资源限制(二十二)