额,博主只是做了几(约数)道题而已,写这篇小结纯粹想留作纪念(勿喷,但是可以交流)(那啥,转载的话注明一下来源。。打字不容易。。)
最短路呢,包括三种算法,但是各有各的变种,其中变化有很多。
简单记录一下。
首先是三种算法:
1、Dijkstra算法。(单源点最短路径)
双手奉上啊哈雷算法。
然后开始叙述我所理解(如有雷同。。那就雷同吧)的:
有n个点,相互之间可能有所连接或者没有,那么现在,如果让求点1到点n之间的最短路,该怎么求?
这里面有个很有趣的东西,叫做松弛,翻译过来就是这么个意思:
假如已知最短路1-3长度为5,最短路1->4长度为7(无论是1-3,还是1-4均是当前最短,也可以理解成当前最优(dp)),而3->4的长度为1,那么这个时候,明显可以知道:1->3->4的长度是小于1->4的,那么现在把1-4更新到当前最优,长度也就是变成了6。
以上就是松弛操作。也就是假设在1-n中有一个x点,且用d数组表示1~其他点的当前最优最短路,用w二维数组表示两点之间的距离,那么给定一个当前最优点y,如果d[y]>d[x]+w[x][y],是不是说明d[y]可以有更小的值呢?答案显而易见。
那么这是一个点,题目中一共n个点,那么我用每一个点都去更新1到其他点的距离,那么全部更新过后,是不是d[n]表示的就是从1~n的最短路了呢?
而这一切,每一次更细都相当于在所有任意的两个点(其中一个点是1(起点))里插入第三个点,看是否能够松弛,若能,就松弛,不能,就不管了呗。
还有,既然,n个点都要去更新一遍,那么谁先去更新呢?
答案:当然是d值最小的那个点去更新,因为用最小的去更新,才可能更新的动呀。但是既然,只需要每个点更新一次,那么要做个标记,以防更新过的点再次进行更新。
d值会越来越小,那么初值肯定是越大越好咯。
但是呢,既然d数组代表的是1到其余各点的最短距离,那么d[1]肯定是0。。
例题博客(基本上我第一次写的题博客都很长。。):Til the Cows Come Home
来啊,代码伺候!!!
int INF=0x3f3f3f3f
for(int i=1;i<=n;i++)
{
d[i]=INF;
vis[i]=0;
}
d[1]=0;
for(int i=1;i<=n;i++)//每个点都要参与更新,所以循环n次
{
int m=INF,x=-1;//m是为了找出最小那一个,x记录节点
for(int j=1;j<=n;j++)
{
if(!vis[j]&&d[j]<m)
{
m=d[x=j];
}
}
if(x!=-1)
{
vis[x]=1;//用于更新过了,就要标记
for(int j=1;j<=n;j++)//用最小的d[x]去更新d[j]
{
if(d[j]>d[x]+w[x][j])
{
d[j]=d[x]+w[x][j];
}
}
}
}
2、bellman(附加spfa)
既然已经知道什么是松弛(不知道的再看一遍,因为是环环相扣的),接下来就说一下什么是bellman。
刚才已经说了,Dijkstra算法是用n个点去更新从1点到其余各点(这里的1代表起点,谁是起点看题意,只需要把起点的d变成0就可以了)的最短路,那么也就是用的边去更新两点之间的距离。
然后就提出了bellman,思想是从源点逐次经过其他点,以缩短到达终点的距离,假设n个点不存在负权值回路,那么最多存在n-1条边,因为假设存在超过n-1条边,那么肯定会重复经过一个点,那么最短路就可以更新:
Bellman-Ford算法构造一个最短路径长度数组序列:dist(1)[u],dist(2)[u],dist(3)[u],…,dist(n-1)[u]。其中:
dist(1)[u]为从源点v0到终点u的只经过一条边的最短路径的长度,并有dist(1)[u]=edge[v0,u]。
dist(3)[u]为从源点v0出发最多经过不构成负权值回路的3条边到达终点u的最短路径长度。
……
dist(n-1)[u]为从源点v0出发最多经过不构成负权值回路的n-1条边到达终点u的最短路径长度。
算法的最终目的是计算出dist(n-1)[u],为源点v0到顶点u的最短路径长度,也就是利用n-1条边更新过后的最短距离。
采用递推方式计算dist(k)[u]。
设已经求出dist(k-1)[u],u=0,1,…,n-1,此即从源点v0最多经过不构成负权值回路的k-1条边到达终点u的最短路径的长度。
从图的邻接矩阵可以找出各个顶点j到达顶点u的(直接边)距离edge[j,u],计算min{distk-1[j]+edge[j,u]},可得从源点v0途经各个顶点,最多经过不构成回路的k条边到达终点u的最短路径的长度。 比较dist(k-1)[u]和min{dist(k-1)[j]+edge[j,u]},取较小者作为dist(k)[u]的值。
因此Bellman-Ford算法的递推公式(求源点v0到各顶点u的最短路径)为: 初始:dist(1)[u]=edge[v0,u],v0是源点 递推:dist(k)[u]=min{dist(k-1)[u],min{dist(k-1)[j]+edge[j,u]}} j=0,1,…,n-1,j<>u; k=2,3,4,…,n-1 。
所以,n-1次过后便可以得到最短路,如果n-1次后依旧可以更新,那么说明存在负权回路或者是正权回路。
其次,对于无论是Dijkstra还是Bellman,均有一个选点去更新的操作,那么有的时候,可能选出的点不能对任何点进行更新,所以造成了时间的浪费,而且,还有一句是被更新过的点一定可以去更新其他点(不知道对不对。。),然后呢,就会想到,为什么不把每次更新过的点装进一个容器呢?直到再没有点可以装进容器(代表最短路已是最优,更新完毕)。
然后想到了数组,想到了栈和队列,但是呢,数组模拟需要一数组还要一指针变量时刻维护,所以就用栈和队列吧,相同的时间复杂度,但是呢,还有一点,选出尽量小的去更新其他点,这样更好。所以,考虑优先队列,那为啥不考虑优先。。栈呢?因为意义一样呀。。。
回到正,负权的问题上,怎样进行初始化呢、、
对于负权,自然是越来越小,所以数组赋为极大值,正权的话,赋为0好了。
给出一篇判定负环的博客:Wormholes。
给出一篇判定正环的博客:Arbitrage,内含两种判定正环的方法,一个是bellman,一个是floyed(下文有)。
给出一篇使用spfa的题解(模板):Til the Cows Come Home。
然后,奉上优先队列(经典,当然不是我的。。)博客:优先队列详解。
以上就是bellman和spfa用法,对了,提到spfa就不得不提到差分约束系统,自行看下(尝试理解,因矩阵知识浅薄,所以只会最基础的):Candies,里面有差分约束的博客,但是最好还是先看下题,理解这种思想用于哪个方面。
3、floyed
floyed是求任意两点间的最短路算法,因为三个for让他拥有不小的局限性,若是1s的话,n的范围只能是100以下,若是bellman能够理解成为利用n-1条边去更新起点到定点的最短路,记录的是前i条边更新过后的状态,那么floyed就可以理解为利用点去更新,记录的是前i个点更新过后的当前最优状态,三个for,假如分别是k,i,j,那么每次都会利用k作为中间桥梁,询问,i->j,i->k->j这两个哪个更短。。。保留当前最优,直到利用所有点把这对i,j更新n遍。(也就是n遍Dijkstra),这样理解会不会容易一些。
然后呢,floyed会牵扯出一个问题,叫做传递闭包问题,也就是关系的一个转换而已,给出一道例题解析:Cow Contest,这种题数据小的话(<=100)直接floyed暴力过,大了的话,就又牵扯出一个概念,叫做强连通分量,又有三种算法(暂不解释)。
以上呢,就是三种算法,在这里,补充一点Dijkstra的变种,其实也不算是变种,只是松弛操作的内容有点区别而已,都是共通的。
其一呢:最小生成树之prim算法(补了一觉继续写。。)。
现在我不再区别最短路的三种算法,统称为最短路。
那么思考最短路与最小生成树的区别,一样是求的最小值,但是最短路两点之间的最短路,而最小生成树求得是将全图的点连起需要的最小长度。
给出百度百科解释(很容易理解):prim算法百度百科,每次都找已经找过的点的最小距离,那么利用代码怎么实现呢?
找到离部分连通图最近的d值的点,加入标记,然后用这条边去更新所有没有被标记的点的距离。
附代码:
void prim()
{
for(int i=1;i<=n;i++)
d[i]=w[1][i];//初始化,也可以全设为INF
vis[1]=1;
int sum=0;
for(int i=2;i<=n;i++)
{
int minn=INF,pos=0;
for(int j=1;j<=n;j++)//选出一个最近的点
{
if(!vis[j]&&d[j]<minn)
{
minn=d[pos=j];
}
}
sum+=minn;//求最小距离
vis[pos]=1;
for(int j=1;j<=n;j++)//更新没有被标记的点
if(!vis[j]&&d[j]>w[pos][j])
d[j]=w[pos][j];
}
}
d[j]>w[pos][j]是指:因为只要把全图连接起来便好,所以不用像最短路那样严格控制是一条路。并且,d数组的用法在变种里面都代表不同的含义,因为之前在写题解的时候写过了,所以直接给出那道题(内含d数组的讲解)Frogger,然后给出一道模板题:Jungle Roads。
其二呢,就是刚才给出的那道Frogger,这类题的基本题意是最一个有向或者无向图里,从一个起点(假设为st)到一个重点(假设为ed)有很多条路,那么,每一条路呢都有一个最大长度边和一个最小长度边,那么这类题就会拿这个做文章,问:从st到ed里所有路的最小边长的最大值是多少?或者是从st到ed里所有路的最大边长的最小值是多少?
而这些呢,主要就是d数组的差异,给出两道题,分别对应两种问法。
Frogger
Heavy Transportation。
那么整理到这里,也就是我花了那么多天的做的专题的成果了。
参考:floyed算法、bellman算法。
对了对了,再补充一个邻接表,双手奉上啊哈雷大大的邻接表博客,然后,就是自己的补充了:
假设一个有向图,存在n个点,m条边
那么:
我见过的有两种方式:
①
struct djh
{
int u,v,w;代表含义分别是:左端点,右端点,边长
}edge[];这个数组的范围是按照m的范围而定的
int first[]这个数组的范围是按照n的范围而定的
int next[]这个数组的范围是按照m的范围而定的
初始化:
for(int i=1;i<=n;i++)
{
first[i]=-1;
}
for(int i=1;i<=m;i++)
{
next[i]=-1;
}
输入边的时候:
for(int i=1;i<=m;i++)
{
scanf("%d%d%d",&edge[i].u,&edge[i].v,&edge[i].w);
next[i]=first[edge[i].u];
first[edge[i].u]=i;
}
dijs或者spfa或者其他引用的写法:
选出一个点;
res
for(int i=first[res];i!=-1;i=next[i])
{
if(dis[edge[i].v>dis[edge[i].u]+edge[i].w)
{
dis[edge[i].v=dis[edge[i].u]+edge[i].w
}
}
②
struct djh
{
int to,w,next;分别代表右端点,边长,next数组
}edge[];这个数组的范围是按照m的范围而定的
int first[]这个数组的范围是按照n的范围而定的
初始化不变
输入边的时候:
tot=0;
while(m--)
{
int u,v,w;
scanf("%d%d%d",&u,&v,&w);
edge[tot].nexx=first[u];
edge[tot].v=v;
edge[tot].w=w;
first[u]=tot++;
}
dijs或者spfa或者其他引用的写法:
选出一个点;
res
for(int i=first[res];i!=-1;i=edge[i].nexx)
{
if(d[edge[i].v]>d[res]+edge[i].w)
{
d[edge[i].v]=d[res]+edge[i].w;
}
}
以上便是个人用法。