图论的题也刷了不少了,但是近期才发现前面的一些dij什么的都忘记怎么写了,甚至分不清楚dij和spfa的区别了…所以想到这里做一些简单图论算法归纳。主要涉及的算法有:Floyd算法,dijkstra算法,spfa算法,prim算法和kruskal(其实这两个就是最小生成树算法),以及一维,二维的并查集算法(抱歉萌新最近也就学了这些算法大佬们见笑了QAQ)。
欧克!let’s begin !
其实这个算法,就是依据动态规划原理,枚举出jk中间的一个点i以作为jk连接的桥梁,从而递推出任意两个点直接距离的最小值。有人问我i,j,k三个循环为什么不能调换位置,原因很简单,就是因为枚举ij之间的点k的时候只要用到前面的结论f(ji)和f(ik)的,所以ij必须是最快更新的一个,后面才能够用到。
虽然代码简单,但是时间复杂度o(n3),一不留神就超时了,注意数据。代码如下:
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
for(int k=1;k<=n;k++)
{
if(g[j][i]+g[i][k]<g[j][k])
g[j][k]=g[j][i]+g[i][k];//先算出各个点之间的权值情况,前提是已经全部初始化gij=+∞,具体大小判断得依据题意
}
原理:
1.先标记起点,然后从起点出发,得出到各个点的最小距离。
2.再每次找出离起点最近(并且之前还没有讨论过)的那个点,从这个点出发,将前面得到的起点到各个点的最短路径和以这个点为中介点,再到各个点的距离大小作比较,去最小值。时间复杂度o(n2)。
代码如下:
int i,j,t,flag[N],dis[N],g[N][N];
void dij()
{
for(i=1;i<=t-1;i++)//起点不用讨论最后一个
{
int maxx=2e9,now=0;//最小值和当前最小值位置
for(j=1;j<=t;j++)
{
if(!flag[j])//如果j未被讨论过
{
if(dis[j]<maxx)
{
maxx=dis[j];//记录最小值
now=j;//记录最小值位置
}
}
}
flag[now]=1;//搜索这个点,同时标记已经搜索
if(!now)break;//说明已经搜索完毕
for(int j=1;j<=t;j++)
{
if((dis[j]>dis[now]+g[now][j])&&!flag[j])//注意终点不能是以前已经走过的位置,否则又回到原来的地方,讨论重复点,肯定更长
dis[j]=dis[now]+g[now][j];
}
}
}
其实个人觉得这个算法和dij有点类似,不过它用队列做了一些优化,而且算法稍有不同。
原理:
1.同样以起点出发,先算出到各个点的最小距离然后入队最小距离的各个点。
2.当队列非空时,分别以每个队列中的点作为中介点,再算出到别的点最短距离;同时该点继续入队,直到继续入队时到所有点距离都不是最小时,队列变空,停止循环。
代码如下:
int dis[N],map[N][N],vis[N],n,m,a[5555];//n是对象数目,假设1 是起点(其实这里随便可以随便设,哪个是起点就最先让哪个入队)
void spfa()//dis是起点到各点最小距离,vis是标记变量
{
int m,k;queue<int> q;q.push(1);//起点入队
while(!q.empty()){//非空说明还要讨论情况,继续
m=inf;int k=q.front();//k为当前讨论间隔点
for(int j=1;j<=n;j++){
if(map[k][j]!=inf&&dis[j]>dis[k]+map[k][j]){//如果kj可以联通并且之前到j的最小距离大于以k作为中介点的距离和,则赋值
dis[j]=dis[k]+map[k][j];
if(!vis[j]){//说明该轮循环j还没有入队(可能之前已经入队,这里不重复入队)
q.push(j);
vis[j]=1;
}
}
}
vis[k]=0;q.pop();//重新赋值为vis=0是为了后面对该数据的更新(有可能遇到以k为中介点更小距离的情况)
}
}
首先简单说说啥是最小生成树(详细见百度,这里只是简单提及一下)。就是建一颗树,使得所有连接边的权值和最小。
1.prim算法:
原理介绍:
1.tot记录当前连接点的数目,当n=tot时说明所有点都已经完成连接,最小生成树建立完毕
2.对还没有连接的点讨论,选取已连接点中与该点连接后权值最小的那个点,并相连
3.每次将已经连接的点记录如数组,便于下次对已连接点讨论
代码如下:
while(cin>>n)
{
init();//初始化函数,这里就不多写了,memset什么的会用吧
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
cin>>g[i][j];
f[1]=1;p[++tot]=1;//首先以第一个作为起点,同理起点也可以因情况而变化
while(tot<n)//pirm算法,最小生成树
{
minx=inf;
for(int i=1;i<=tot;i++)
for(int j=1;j<=n;j++)
if(!f[j]&&minx>g[p[i]][j])//以此考虑每个树中的元素与剩下的元素相连的情况,找出最小值
{minx=g[p[i]][j];sp=j;}//如果找到还没有加入最小生成树的,而且于该树枝连接的值最小,则赋值
ans+=minx;p[++tot]=sp;f[sp]=1;//该代码中ans是记录所有边的最小权值和,p存放所加入点位置
}
cout<<ans<<endl;
}
2.kruskal算法
这个算法的整体性比较强,还用到了并查集,所以干脆一起讲了吧
原理:就是 排序+并查集
1.对所有边的权值排序。
2.通过并查集搜寻两个点是否来自于同一个发源地,如果相同则说明已经连上最小生成树,跳过;否则连上并标记这个点的发源地是树的源点。(由于已经排好序故在前面的情况肯定是最优解,后面遇到已经连上的点权值肯定比前面的情况要大)
具体看下列代码:
#include
#include
#include
using namespace std;
int n,m,s,step,x,ans,y,z,f[330];
struct node
{
int x,y,val;
bool operator < (node a)const//运算符重载,这里是为了排序需要,根据结构体中的val进行排序
{
return val<a.val;
}
}p[10001];
int find(int k)//并查集哦,用于寻找两个点的发源地
{
if(k!=f[k])return find(f[k]);
return k;
}
int main()
{
while(cin>>n>>m)
{
memset(f,0,sizeof f);
memset(p,0,sizeof p);
ans=0;step=0;
for(int i=1;i<=n;i++)f[i]=i;
for(int i=1;i<=m;i++)
{
cin>>x>>y>>z;
p[i].x=x;p[i].y=y;p[i].val=z;
}
sort(p+1,p+m+1);//对所有边的权值进行排序
for(int i=1;i<=m;i++)
{
int a=find(f[p[i].x]);//分别寻找两个点的祖先
int b=find(f[p[i].y]);
if(a!=b)
{
ans=max(ans,p[i].val);//这里的ans用于记录操作过程中的最大权值
f[a]=b;step++;//step记录建树过程步数
}
}
cout<<step<<' '<<ans<<endl;
}
return 0;
}
啊啊啊。
内容太多了,具体并查集内容我的其他博客里面也有,这里只是简单提及了一下~
好滴,大咖一起来复习下哈(其实我也就是为了自己的复习用),祝愿大家学业有成啊!不专业的地方大佬见谅!