最小生成树和最短路算法
是很久以前就学过的东西。图论最基础的算法。通常在各种题目中担任题解的基础部分(这道题先跑个生成树再balabala)
这次也是复习了。
最小生成树
最小生成树是用来解决用最小的代价用N-1条边连接N个点的问题。常用的算法是Prim和Kruskal,两者时间复杂度并没有差很多(Prim堆优化的前提下),但是因为写Prim就要手撕堆所以我比较偏向Kruskal。
没错我不会(can't)用sort以外的STL
Prim
Prim 算法使用和 Dijkstra 相似的蓝白点思想,用 dis 数组来表示 i 点与白点相连的最小权值,每一轮取出 dis 最小的蓝点,将其变为白点并修改与其相连的蓝点的 dis 值。
n 次循环,每次循环 Prim 算法都能让一个新的点加入生成树,n 次循环就能把所有点囊括到其中;每次循环 Prim 算法都能让一条新的边加入生成树,n-1 次循环就能生成一棵含有 n 个点的树;每次循环 Prim 算法都取一条最小的边加入生成树,n-1 次循环结束后,我们得到的就是一棵最小的生成树。这就是 Prim 采取贪心法生成一棵最小生成树的原理。
朴素Prim的时间复杂度是O(n²),显然的可以使用堆优化来获得更好的时间复杂度。
Kruskal
Kruskal 算法先将所有点认为是孤立的,然后将边按权值排序,每次选择一条边,如果这条边连接着两个不同的联通块,就合并这两个联通块,如果不是,就不选择这条边,直到选择了 n-1 条边为止。
Kruskal 算法每次都选择一条最小的,且能合并两个不同集合的边,一张 n 个点的图总共选取 n-1 次边。因为每次选的都是最小的边,所以最后的生成树一定是最小生成树。每次选的边都能够合并两个集合,最后 n 个点一定会合并成一个集合。通过这样的贪心策略, Kruskal 算法就能得到一棵有 n-1 条边,连接着 n 个点的最小生成树。
使用并查集来支持查询和合并的话,Kruskal的时间复杂度是O(Elog2E)的,E为边数。
附上模板题Kruskal代码(Prim?没写,不想手撕堆。)
#include <algorithm> #include <iostream> #include <cstring> #include <cstdlib> #include <cstdio> #include <cmath> using namespace std; struct node { int u; int v; int c; }; node edge[90005];//没有M范围 请出题人自裁 int n,m,ans,head[305],pa[305]; int Find(int x); bool uni(int x,int y); void kruskal(); bool cmp(node a,node b); int main(void) { scanf("%d%d",&n,&m); int u,v,c; for(int i=1;i<=m;i++) { scanf("%d%d%d",&u,&v,&c); edge[i].u=u; edge[i].v=v; edge[i].c=c; } sort(edge+1,edge+m+1,cmp); for(int i=1;i<=n;i++)pa[i]=i; kruskal(); printf("%d",ans); return 0; } bool cmp(node a,node b) { if(a.c<b.c)return 1; return 0; } int Find(int x) { if(pa[x]==x)return x; pa[x]=Find(pa[x]); return pa[x]; } bool uni(int x,int y) { int px,py; px=Find(x),py=Find(y); if(px==py)return 0; pa[py]=px; return 1; } void kruskal() { int cnt=0,a=0,b=0; for(int i=1;i<=m;i++) { a=edge[i].u; b=edge[i].v; if(!uni(a,b))continue; cnt++; ans+=edge[i].c; } return; }
最短路
最开始学图论的时候学习的算法,也是就算到现在为止也能够随时随地手撕的玩意。
以及,就算有这样那样各种的理由,我还是要喊出: SPFA天下第一!!!!!!!!
Floyd
全源最短路算法,使用动态规划的思想,代码是极其优美的三层嵌套循环。时间复杂度也是极其优美的O(n³)
唯一需要注意的一点是,枚举中间节点的k一定要放在最外层。
伪代码如下
for(int k=1;k<=n;k++) for(int i=1;i<=n;i++) for(int j=1;j<=n;j++) dis[i][j]=min(dis[i][j],dis[i][k]+dis[k][j]);
除了求全源最短路以外基本不用,偶尔用来找负环?
Dijsktra
单源正权最短路算法,意思是图上边权有负权的话dijsktra会WA。
Dijkstra 的基本思想是蓝白点思想。蓝点是最短路径未确定的点,白点是最短路径确定了的点。最开始只有起点是白点,然后在蓝点中寻找一个离起点的距离最小的点,标记它为白点,它的 dis 值为它离更新它的点的距离加上更新它的白点的 dis 值,再用这个点去更新其余的点。
与Prim算法相同的,可以使用堆优化。
因为懒得手撕堆所以懒得写的算法+1
昨天越写越觉得自己在写SPFA
贴上模板题代码
#include <algorithm> #include <iostream> #include <cstdlib> #include <cstring> #include <cstdio> using namespace std; struct node { int nw; int nxt; int value; }; node edge[20005]; int n,m,st,ed,cnt,dis[1005],head[1005]; int Heap[4005],cnt_H; bool closed[1005]={0},IN[1005]={0}; void build(int x,int y,int v); void add(int x); int get(); void dijsktra(); int main(void) { scanf("%d%d%d%d",&n,&m,&st,&ed); memset(dis,127/2,sizeof(dis)); memset(head,-1,sizeof(head)); int a,b,c; for(int i=1;i<=m;i++) { scanf("%d%d%d",&a,&b,&c); build(a,b,c); build(b,a,c); } a=dis[ed]; dijsktra(); if(a==dis[ed])printf("-1"); else printf("%d",dis[ed]); return 0; } void build(int x,int y,int v) { edge[++cnt].nw=y; edge[cnt].nxt=head[x]; edge[cnt].value=v; head[x]=cnt; return; } void add(int x) { Heap[++cnt_H]=x; int now=cnt_H,next; while(now/2) { next=now/2; if(dis[Heap[next]]<=dis[Heap[now]])break; swap(Heap[now],Heap[next]); now=next; } return; } int get() { int ans=Heap[1]; Heap[1]=Heap[cnt_H]; cnt_H--; int now=1,next; while(now*2<cnt_H) { next=now*2; if(dis[Heap[next+1]]<dis[Heap[next]])next++; if(dis[Heap[next]]>=dis[Heap[now]])break; swap(Heap[next],Heap[now]); now=next; } return ans; } void dijsktra() { dis[st]=0; add(st); int white=0,blue=0; while(cnt_H) { white=get(); if(closed[white])continue; closed[white]=true; for(int j=head[white];j>0;j=edge[j].nxt) { blue=edge[j].nw; if(dis[blue]>dis[white]+edge[j].value) { dis[blue]=dis[white]+edge[j].value; add(blue); } } closed[white]=false; } return; }
SPFA
单源最短路算法,有负权也不会WA,我最常用的算法。
我知道SPFA严格来说不被承认它就是个队列优化Bellman-Ford但是我就是要喊它SPFA
SPFA先将起点放入队列, 每次使用队列首的点去试图更新所有与它相连的点的(距离起点的)最短距离,假如更新成功就把被更新的点放入队列去准备更新其它点。
除此之外,也可以使用SPFA查出负环。
因为各种原因,正权图最好还是使用DIjsktra算法说的就是那些闲着无聊卡SPFA的出题人
附上模板题代码。
#include <algorithm> #include <iostream> #include <cstdlib> #include <cstring> #include <cstdio> using namespace std; struct node { int nw; int nxt; int value; }; node edge[20005]; int n,m,st,ed,cnt; int dis[1005]={0},head[1005]={0}; bool closed[1005]={0}; void build(int x,int y,int v); void SPFA(); int main(void) { scanf("%d%d%d%d",&n,&m,&st,&ed); memset(dis,0x7f/2,sizeof(dis)); memset(head,-1,sizeof(head)); int a,b,c; for(int i=1;i<=m;i++) { scanf("%d%d%d",&a,&b,&c); build(a,b,c); build(b,a,c); } a=dis[ed]; dis[st]=0; SPFA(); if(dis[ed]==a)printf("-1"); else printf("%d",dis[ed]); return 0; } void build(int x,int y,int v) { edge[++cnt].nw=y; edge[cnt].nxt=head[x]; edge[cnt].value=v; head[x]=cnt; return; } void SPFA() { int dl[2005]={0},Head=0,Tail=1,Now; dl[1]=st; closed[st]=true; do { Head++; if(Head>2000)Head=1; for(int i=head[dl[Head]];i>0;i=edge[i].nxt) { Now=edge[i].nw; if(dis[Now]>dis[dl[Head]]+edge[i].value) { dis[Now]=dis[dl[Head]]+edge[i].value; if(!closed[Now]) { Tail++; if(Tail>2000)Tail=1; dl[Tail]=Now; closed[Now]=true; } } } closed[dl[Head]]=false; }while(Head!=Tail); }
本周习题……换教室严格来说是个DP附带了最短路,稍后我会单独贴题解。动态最小生成树我还不会搞……
以上