①Kruskal算法能幹啥及它的原理和時/空間複雜度node
Kruskal算法用於構造最小生成樹,它是一個基於貪心思想的算法ios
咱們創建一個$O(n)$的並查集和一個$O(m)$的結構體,每一個結構體存儲一對點和點之間的邊權,而後按邊權從小到大排序,順次取每一對點。若是兩點未被併入一個集合裏,就把它們並起來,而後就作完了。c++
順便說一句,(因而可知,)Kruskal算法較適用於稀疏圖。算法
時間複雜度:$O(mlog$ $m)$ide
②Kruskal算法求最小生成樹的具體實現測試
1 #include<iostream> 2 #include<algorithm> 3 using namespace std; 4 const int N=10005,M=100005; 5 struct a{int n1,n2,val;}mst[M]; 6 int n,m,ans,t1,t2; 7 int aset[N]; 8 bool cmp(a x,a y)//按邊權從小到大排序 9 { 10 return x.val<y.val; 11 } 12 int find(int x) 13 { 14 return (aset[x]==x)?x:aset[x]=find(aset[x]); 15 } 16 int main() 17 { 18 cin>>n>>m; 19 for(int i=1;i<=m;i++) 20 cin>>mst[i].n1>>mst[i].n2>>mst[i].val; 21 for(int i=1;i<=n;i++) aset[i]=i; 22 sort(mst+1,mst+1+m,cmp);//排序 23 for(int i=1;i<=m;i++) 24 { 25 int t1=find(mst[i].n1),t2=find(mst[i].n2); 26 if(t1!=t2) aset[t1]=t2,ans+=mst[i].val;//合併 27 } 28 cout<<ans; 29 return 0; 30 }
①Prim算法能幹啥及它的原理和時/空間複雜度優化
Prim固然也是用來構造最小生成樹啦,它是一個基於搜索思想的算法。Prim算法先**將隨意的一個點做爲起始點,而後以相似BFS的方式進行擴展,以新掃到的邊更新已選邊**。咱們須要用**邊權創建一個小根堆進行Prim算法**。spa
因而可知Prim算法**比較適合於稠密圖**code
時間複雜度:採用鄰接表存儲,堆優化後能夠達到$O(mlog$ $n)$blog
②Prim的具體實現
1 #include<queue> 2 #include<cstdio> 3 #include<cstring> 4 using namespace std; 5 const int N=10005,M=100005; 6 struct a{int nxt,val;}; 7 int p[N],noww[M],goal[M],val[M],dis[N]; 8 int n,m,cnt,t1,t2,t3,c,tot; 9 bool inh[N]; 10 bool operator <(a x,a y) 11 { 12 return x.val>y.val; 13 }//邊權小根堆 14 priority_queue<a> qs; 15 void link(int f,int t,int v) 16 { 17 noww[++cnt]=p[f],p[f]=cnt; 18 goal[cnt]=t,val[cnt]=v; 19 } 20 int main () 21 { 22 scanf("%d%d",&n,&m); 23 for(int i=1;i<=m;i++) 24 { 25 scanf("%d%d%d",&t1,&t2,&t3); 26 link(t1,t2,t3),link(t2,t1,t3); 27 } 28 memset(dis,0x3f,sizeof dis); 29 int tn=1;dis[tn]=0,inh[tn]=true;//隨便找個初始點 30 for(int i=p[tn];i;i=noww[i]) 31 qs.push((a){goal[i],val[i]}),dis[goal[i]]=val[i];//把邊一股腦丟進去 32 while(!qs.empty()) 33 { 34 a tmp=qs.top();qs.pop(); 35 if(!judge[tmp.nxt])//沒選過 36 { 37 int tp=tmp.nxt,tv=tmp.val; 38 tot+=tv,c++,judge[tp]=true; 39 for(int i=p[tp];i;i=noww[i])//更新 40 if(dis[goal[i]]>val[i]) 41 dis[goal[i]]=val[i],qs.push((a){goal[i],val[i]}); 42 } 43 } 44 printf("%d",tot); 45 return 0; 46 }
①Bellman_Ford能幹啥
Bellman-Ford是一個單源最短路算法,適用於各類類型的圖。不過在國內,咱們通常給它加上一個隊列優化(並不能下降其理論複雜度上界,可是會讓它跑不滿,而後在一些圖上就跑的很快),在平時咱們都是這麼寫的(指隊列優化),而後叫它"SPFA(Shortest Path Fast Algorithm)"。
(至因而不是真的"Fast」就不必定了)
②Bellman_Ford的原理
以一個隊列存儲等待進行鬆弛的各個點,隊列中起初只有一個起點,利用三角形不等式進行鬆弛操做,直到隊列爲空。在這個過程當中一個節點可能會入/出隊屢次,即便存在負權邊,也能正常求解。初始化即將起點入隊。
③Bellman_Ford的時間複雜度
時間複雜度:$O(nm)$,較爲適用於稀疏圖(稀疏圖上跑得很不滿),可是複雜度上界仍然是$O(nm)$,出題人只要想卡就能卡,因此在沒有負權邊時看好數據範圍,考慮使用下文的Dijkstra。
什麼?有負權還$O(nm)$跑不過去?那就是出題人的問題了,強行Bellman_Ford吧
(因此國際上通常不認可所謂的SPFA算法,而是稱它爲「隊列優化的Bellman-Ford算法」)。
④Bellman_Ford的具體實現
1 #include<queue> 2 #include<cstdio> 3 #include<cstring> 4 #include<algorithm> 5 using namespace std; 6 const int N=10005,M=100005; 7 int dis[N],p[N],noww[M],goal[M],val[M]; 8 int N,M,S,cnt,t1,t2,t3; 9 bool inq[N]; 10 queue<int> qs; 11 void link(int f,int t,int v) 12 { 13 noww[++cnt]=p[f],p[f]=cnt; 14 goal[cnt]=t,val[cnt]=v; 15 } 16 void setit(int s) 17 { 18 memset(dis,0x3f,sizeof dis); 19 qs.push(S);dis[s]=0;inq[s]=true; 20 } 21 void SPFA() 22 { 23 while(!qs.empty()) 24 { 25 int tn=qs.front(); 26 qs.pop(),inq[tn]=false; 27 for(int i=p[tn];i;i=noww[i]) 28 if(dis[goal[i]]>dis[tn]+val[i]) 29 { 30 dis[goal[i]]=dis[tn]+val[i]; 31 if(!inq[goal[i]]) 32 inq[goal[i]]=true,qs.push(goal[i]); 33 } 34 35 } 36 } 37 int main() 38 { 39 scanf("%d%d%d",&N,&M,&S); 40 for(int i=1;i<=M;i++) 41 scanf("%d%d%d",&t1,&t2,&t3),link(t1,t2,t3); 42 setit(S);SPFA(); 43 for(int i=1;i<=N;i++) 44 printf("%d ",dis[i]); 45 return 0; 46 }
⑤一些奇怪的東西— —所謂的SPFA的「優化」
SPFA有一個相對常見優化,叫作SLF(Small Label First),它基於雙端隊列(可以使用<queue>裏的std::deque或者手寫)。在每次鬆弛後,將$dis[temp]$與隊首節點的$dis$做比較,若$dis[temp]$較小,將其從隊頭入隊,不然從隊尾入隊。
還有個叫作LLL(Large Label Last)的優化,原理差很少,看字面意思就能懂吧= =
SLF的代碼便是將原代碼中的
1 if(!inq[goal[i]]) 2 { 3 inq[goal[i]]=true; 4 qs.push(goal[i]); 5 }
改成
1 if(!inq[goal[i]]) 2 { 3 if(!q.empty()&dis[goal[i]]<dis[q.front()]) 4 q.push_front(goal[i]); 5 else 6 q.push_back(goal[i]); 7 inq[goal[i]]=true; 8 }
但是這麼明顯的優化爲何不多聽人說呢?由於這是個假的優化,原本SPFA在的複雜度就是$O(nm)$,這兩個優化本質上不會下降複雜度(可能能過掉一些弱數據),同時在有負權邊的圖上會被喪心病狂地卡成$O(2^n)$......
因此沒有負權邊時老老實實寫堆優化下的迪傑斯特拉算法,有負權邊就老老實實寫正常的SPFA,不要總是想什麼騷操做,畢竟
(NOI 2018講解現場,不要在乎二重存在的luogu水印=。=)
哦你問什麼是堆優化下的迪傑斯特拉算法?往下看就知道了quq
①原理
$_1$基於Bellman_Ford判負環的原理
根據Bellman_Ford的原理,顯然在圖中存在負環時,SPFA會將這個負環上的節點不斷循環入隊。而一張圖若存在最短路則每一個點至多入隊$n$次(顯),因此每一個點記錄一個入隊次數,超過$n$次即存在負環(據lyd學長說記錄邊的次數會更快),最差狀況下複雜度爲$O(n^2)$
$_2$基於深度優先思想判負環的原理
基本與DFS相同,咱們能夠想象出遞歸時的那個棧。判斷時加上一個初始爲假的判斷標誌$flag$,在進行鬆弛時,若是這個點已經入棧或者$flag$爲真,標記$flag$爲真,回溯便可。雖然是指數複雜度,可是隨機數據下跑的飛快,通常沒人卡這個(不過luogu新數據卡了,可真是有心。。。)
另:我的測試下,若是你跑深度優先的話,$dis$初值爲$0$每一個點跑一次比加超級源點再跑超級源點要快。。。
②具體實現
1 #include<cstdio> 2 #include<cstring> 3 #include<algorithm> 4 using namespace std; 5 const int N=1005,M=10005; 6 int p[N],noww[M],goal[M],val[M],dis[N]; 7 int n,m,typ,t1,t2,t3,cnt,T; 8 bool ins[N]; 9 bool found; 10 void link(int f,int t,int v) 11 { 12 noww[++cnt]=p[f],p[f]=cnt; 13 goal[cnt]=t,val[cnt]=v; 14 } 15 void gtmd() 16 { 17 memset(ins,false,sizeof ins); 18 memset(dis,0,sizeof dis); 19 memset(p,0,sizeof p); 20 cnt=0;found=false; 21 } 22 void DF_SPFA(int nde) 23 { 24 ins[nde]=true; 25 for(int i=p[nde];i;i=noww[i]) 26 if(dis[goal[i]]>dis[nde]+val[i]) 27 { 28 if(ins[goal[i]]||found) 29 { 30 found=true; 31 return ; 32 } 33 dis[goal[i]]=dis[nde]+val[i]; 34 DF_SPFA(goal[i]); 35 } 36 ins[nde]=false; 37 return ; 38 } 39 int main () 40 { 41 scanf("%d",&T); 42 while(T--) 43 { 44 gtmd(); 45 scanf("%d%d",&n,&m); 46 for(int i=1;i<=m;i++) 47 { 48 scanf("%d%d%d",&t1,&t2,&t3); 49 link(t1,t2,t3);if(t3>=0) link(t2,t1,t3); 50 } 51 for(int i=1;i<=n;i++) 52 if(!found) 53 DF_SPFA(i); 54 else 55 break; 56 printf(found?"Ye5\n":"N0\n"); 57 } 58 return 0; 59 }
1 //SPFA改造 2 #include<queue> 3 #include<cstdio> 4 #include<cstring> 5 #include<algorithm> 6 using namespace std; 7 const int N=1005,M=10005; 8 int dis[N],p[N],noww[M],goal[M],val[M],xnt[N]; 9 int n,m,t1,t2,t3,cnt,T; 10 bool inq[N]; 11 queue<int> qs; 12 void link(int f,int t,int v) 13 { 14 noww[++cnt]=p[f],p[f]=cnt; 15 goal[cnt]=t,val[cnt]=v; 16 } 17 void gtmd() 18 { 19 memset(p,0,sizeof p); 20 memset(xnt,0,sizeof xnt); 21 memset(dis,0x3f,sizeof dis); 22 memset(inq,false,sizeof inq); 23 while(!qs.empty()) qs.pop(); 24 qs.push(1);cnt=0; 25 inq[1]=true,dis[1]=0; 26 } 27 bool SPFA() 28 { 29 while(!qs.empty()) 30 { 31 int tn=qs.front();inq[tn]=false;qs.pop(); 32 for(int i=p[tn];i;i=noww[i]) 33 { 34 int tp=goal[i],tv=val[i]; 35 if(dis[tp]>dis[tn]+tv) 36 { 37 dis[tp]=dis[tn]+tv; 38 if(!inq[tp]) 39 { 40 if(++xnt[tp]>n) return true; 41 inq[tp]=true,qs.push(tp); 42 } 43 } 44 } 45 46 } 47 return false; 48 } 49 int main () 50 { 51 scanf("%d",&T); 52 while(T--) 53 { 54 gtmd(); 55 scanf("%d%d",&n,&m); 56 for(int i=1;i<=m;i++) 57 { 58 scanf("%d%d%d",&t1,&t2,&t3); 59 link(t1,t2,t3);if(t3>=0) link(t2,t1,t3); 60 } 61 printf(SPFA()?"YE5\n":"N0\n"); 62 } 63 return 0; 64 }
①Dijkstra能幹啥
Dijkstra也是一種求最短路的算法,缺點是不能處理負權邊,可是其複雜度穩定,卡不掉
②Dijkstra的原理
Dijkstra樸素作法是枚舉每一個點及和這個點相連的點進行進行擴展,而擴展過的點就不會再擴展,於是沒法處理負權邊,複雜度爲$O(n^2)$,比較差。一般會用一個堆進行優化:每次擴展將目標點和邊權加入一個小根堆,在擴展時取堆頂的那個點進行新一輪擴展,複雜度爲$(mlog n)$。
③Dijkstra的時間複雜度
時間複雜度:樸素$O(n^2)$ 堆優化下$O(mlogn)$
④Dijkstra的具體實現
1 #include<queue> 2 #include<cstdio> 3 using namespace std; 4 struct SP{int value,node;}; 5 bool operator <(const SP &a,const SP &b) {return a.value>b.value;}//堆優化 6 int p[N],noww[M],goal[M],val[M],dis[N]; 7 bool inh[N]; 8 priority_queue<SP> qs; 9 int N,M,S,cnt,t1,t2,t3; 10 void link(int f,int t,int v) 11 { 12 noww[++cnt]=p[f],p[f]=cnt; 13 goal[cnt]=t;val[cnt]=v; 14 } 15 void Dijkstra() 16 { 17 while(!qs.empty()) 18 { 19 SP tmp=qs.top();qs.pop(); 20 int tn=tmp.node; //取點 21 if(!inh[tn]) 22 { 23 inh[tn]=true; 24 for(int i=p[tn];i;i=noww[i])//擴展 25 { 26 int tp=goal[i],tv=val[i]; 27 if(dis[tp]>dis[tn]+tv) 28 { 29 dis[tp]=dis[tn]+tv; 30 qs.push((SP){dis[tp],tp}); 31 } 32 } 33 } 34 } 35 } 36 int main () 37 { 38 scanf("%d%d%d",&N,&M,&S); 39 for(int i=1;i<=M;i++) 40 scanf("%d%d%d",&t1,&t2,&t3),link(t1,t2,t3); 41 memset(dis,0x3f,sizeof dis); 42 dis[S]=0;qs.push((SP){0,S}); 43 Dijkstra(); 44 for(int i=1;i<=N;i++) 45 printf("%d ",dis[i]); 46 return 0; 47 }