说是总结,其实自己也没有学多长时间只是把自己这段时间的一些经验总结下来,用来供后来的初学者涨点经验吧。对于学习算法,个人的理解就是首先要去理解算法的本质,然后想想算法的实现过程,如何用代码去描述这个算法,然后就是去记模板了(对于像我这种初学者来说,这一步其实蛮重要的)。另外说下做最短路问题的一些容易出错的地方。1、要小心重边,就是题目会给你一些边类似于2 4 5,2 4 3;这种边和权值的。2、要注意图中所给的是有向图还是无向图,一般情况下都是无向图的,如果有特殊说明的话,就是有向图。接下来我就以HDU的1874–畅通工程续这道题来简单分析一下这三种算法。
第一、dijkstra算法:这种算法只能解决权值不是负的图(稍后解释一下为什么权值不能为负)。
特点:可以求出单源点到其他顶点的最短距离,算法的复杂程度比floyd算法稍微低一些。
算法描述:比较类似于求最小生成树中的prime算法,设置一个点集合S,然后用贪心的方式去选择扩充这个集合。就是说如果1到3的距离为5,1到2为1,同时2到3为2;那么这个时候dis[3]应该=3;即用DIS数组来存储每个点到远点的最短距离,并不断更新这个最短距离,当所有的点都加入了集合S时就可以跳出循环了。
缺点:不能解决带负权值的图,如果图很大的话也不是很好处理,可能会需要用邻接表,这个没试过,图很大的话一般都用SPFA了。为什么不能解决负权值问题?归入S集合的节点的最短路径及其长度不再变更,如果边上的权值允许为负值,那么有可能出现当与S内某点(记为a)以负边相连的点(记为b)确定其最短路径时,它的最短路径长度加上这条负边的权值,结果小于a原先确定的最短路径长度,而此时a在Dijkstra算法下是无法更新的,由此便可能得不到正确的结果。(标记一下,醒目些)。
代码实现的过程(用HDU–1874–畅通工程续–来举例,题目自搜哦)
#include<stdio.h>
#include<string.h>
#define INF 0x3f3f3f3f
int vis[210],map[210][210],dis[210],n,m,beg,end;
void init(){
for(int i=0;i<n;i++)
for(int j=0;j<n;j++){
if(i==j)//如果是自己到自己的距离,那当然为0咯。
map[i][j]=map[j][i]=0;
else
map[i][j]=map[j][i]=INF;//最开始点都是不连通的
}
}
void dijkstra(){//dijk算法的核心 (借鉴了prime算法的实现过程)
int i,flag=0;
memset(vis,0,sizeof(vis));//用这个来标记点是否使用过,或者说是否进入集合S。
for(i=0;i<n;i++)
dis[i]=map[beg][i];//初始化距离,并把起点当做源点加入集合。
vis[beg]=1;//标记源点
for(i=0;i<n;i++){
int j,k,temp=INF;
for(j=0;j<n;j++)
if(!vis[j]&&temp>dis[j])
temp=dis[k=j];
vis[k]=1;
for(j=0;j<n;j++)
if(!vis[j]&&dis[j]>dis[k]+map[k][j])//每加入一个点,都要更新一下其他点到源点的最短距离。
dis[j]=dis[k]+map[k][j];
}
if(dis[end]!=INF)
printf("%d\n",dis[end]);
else
printf("-1\n");
}
int main(){
while(scanf("%d%d",&n,&m)!=EOF){
int i;
init();//在输入图的时候要记得对图初始化。
for(i=0;i<m;i++){
int a,b,cost;
scanf("%d%d%d",&a,&b,&cost);
if(map[a][b]>cost)//这个判断句是用来判断重边的 ,比如说3,4,5和3,4,1这条边,取最短的边进入图。
map[a][b]=map[b][a]=cost;//无向图就这样,有向图就看题意。
}
scanf("%d%d",&beg,&end);
dijkstra();
}
return 0;
}
第二、就是SPFA算法,这个是金牌陈给讲的,用了队列和邻接表(现在都不知道邻接表是何物,默默地记了个模板)
特点:也是用来求单源最短路径的,不过比DIJ稍微好一些的就是权值可正可负,不过如果是负权值的话需要判断一下是否有负环存在。也可以用来做判断负环的题。解释下负环,点与点之间相互连接构成一个环,并且该环的总权值为负,那么这就是一个负环。(重要的事情要标记。)关于负环问题可参照POJ –3259–Warmholes,我博客里有详解。
算法介绍:算法介绍:建立一个队列q,初始时队列里只有一个起始点,在建立一个数组dis记录起始点到所有点的最短路径,并且初始化这个数组。然后进行松弛操作,用 队列里面的点去刷新起始点到所有点的最短路,如果刷新成功且刷新点不再队列中则把该点加入到队列最后,重复执行直到队列为空。如果存在负环的话,需要建立一个数组来判断每个点进入队列了多少次,否则队列一直都不为空。
代码实现(还是DIJK上面说的那道题为例PS:没有考虑负环问题,负环问题可以参考POJ–3259,博客里面有)
#include<stdio.h>
#include<string.h>
#include<queue>
#define N 210
#define M 2010
#define INF 0x3f3f3f3f//定义无穷大
using namespace std;
int dis[N],vis[N],head[N],n,m,edgenum;
struct node{
int from,to,cost,next;
}edge[M];
void init(){
edgenum=0;
memset(head,-1,sizeof(head));
}
void add(int u,int v,int w){
node E={u,v,w,head[u]};
edge[edgenum]=E;
head[u]=edgenum++;
}
void spfa(int beg,int end){//SPFA算法核心
queue<int>q;
q.push(beg);//将起点加入队列
memset(vis,0,sizeof(vis));//用来标记是否在队列中
memset(dis,INF,sizeof(dis));
vis[beg]=1;
dis[beg]=0;
while(!q.empty()){
int u=q.front();
q.pop();
vis[u]=0;
int i;
for(i=head[u];i!=-1;i=edge[i].next){//遍历起点为U的所有的边。
int v=edge[i].to;
if(dis[v]>dis[u]+edge[i].cost){//更新点的最短路
dis[v]=dis[u]+edge[i].cost;
if(!vis[v]){
vis[v]=1;
q.push(v);
}
}
}
}
if(dis[end]==INF)
printf("-1\n");
else
printf("%d\n",dis[end]);
}
int main(){
while(scanf("%d%d",&n,&m)!=EOF&&n!=0&&m!=0){
init();//需要初始化邻接表的表头。
while(m--){
int a,b,cost;
scanf("%d%d%d",&a,&b,&cost);
add(a,b,cost);//对图进行输入,由于是无向图,所以正反两次输入,不用判断重边。
add(b,a,cost);
}
int beg,end;
scanf("%d%d",&beg,&end);
spfa(beg,end);
}
return 0;
}
第三、floyd算法。
特点:可以求多源最短路,权值同样不能为负。
1 floyd 的思想就是通过枚举n个点利用DP的思想来更新最短距离的,假设当前枚举到第k个点,那么就有任意的两个点i , j ,如果i k 相连 j k 相连 那么就可以知道这个时候dis[i][j] = min(dis[i][j] , dis[i][k] + dis[k][j]);,那么只要枚举完n个点,那么就说明已经完全更新完所有两点直间的最短路。
2 floyd算法是最简单的最短路径的算法,可以计算图中任意两点间的最短路径。floyd算法的时间复杂度为o(n^3),如果是一个没有边权的图,把相连的两点间的距离设为dis[i][j]=1.不相连的两点设为无穷大,用floyd算法可以判断i j两点是否相连。
3 floyd 算法不允许所有的权值为负的回路。可以求出任意两点之间的最短距离。处理的是无向图
4 缺点是时间复杂度比较高,不适合计算大量数据
5 如果dis[i][i] != 0,说明此时存在环(这种题目前为止还没见到过,也没考虑过,日后再深度剖析)。
6 如果利用floyd求最小值的时候,初始化dis为INF , 如果是求最大值的话初始化为-1.
代码实现(还是那道题)
#include<stdio.h>
#include<string.h>
#define INF 0x3f3f3f3f
int dis[300][300],n;
void init(){
memset(dis,INF,sizeof(dis));
for(int i=0;i<n;i++)
for(int j=0;j<n;j++)
if(i==j)
dis[i][j]=dis[j][i]=0;
}
void floyd(){
int i,j,k;
for(k=0;k<n;k++)//算法核心就是三重循环
for(i=0;i<n;i++)
if(dis[i][k]!=INF){//注意这个IF判断,是剪枝的操作,把不可能的情况给去掉,能够减少一些时间
for(j=0;j<n;j++)
if(dis[i][j]>dis[i][k]+dis[k][j])//以K为中间结点,更新点的距离。
dis[i][j]=dis[i][k]+dis[k][j];
}
}
int main(){
int m;
while(scanf("%d%d",&n,&m)!=EOF){
init();
while(m--){
int a,b,c;
scanf("%d%d%d",&a,&b,&c);
if(dis[a][b]>c)//对无向图进行输入,如果是有向图需要稍微改一下。
dis[a][b]=dis[b][a]=c;
}
floyd();
int beg,end;
scanf("%d%d",&beg,&end);//可以求任意两个点之间的最短路。
if(dis[beg][end]==INF)
printf("-1\n");
else
printf("%d\n",dis[beg][end]);
}
return 0;
}