最小生成树和最短路算法
是很久以前就学过的东西。图论最基础的算法。通常在各种题目中担任题解的基础部分(这道题先跑个生成树再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附带了最短路,稍后我会单独贴题解。动态最小生成树我还不会搞……
以上
