ACM算法练习题单:最短路问题 & 总结

       在之前不久总算是结束了对最短路4四个算法的练习,在刚开始的时候每次都是拿到一题就一定要用参考书本把4个算法都敲一遍才对自己觉得放心,到最后上来就看数据规模(如果规模小就用Floyd-Warshall)然后就看是否有负圈,最后直接用SPFA的套路。首先我先列出我在练习期间所做的部分题单。(以POJ为主)

1.  POJ 1860   Currency Exchange :

题意:已经给出了不同种类的货币之间的汇率,问:是否能够通过不同货币之间的不断转换而盈利。

想法:对于第一次做最短路的人不建议做这题。虽然这是裸最短路求负圈,但是,对于第一次做最短路的我来说很难一眼看出这是最短路问题。

2.  POJ 3259  Wormholes :

题意:在农场中有单向的虫洞也有双向的路径,通过因为通过虫洞会回到之前的时间。问:能否通过这些虫洞使得时间不断的倒流。

想法:这题是一个比较明显的最短路求负圈的题,但是,使用Floyd算法会超时。

3.  POJ 1062  昂贵的聘礼 

题意:中文题,题意省略。

想法:是一个比较明显的最短路问题。但是,对于数据的存储、使用和代码实现稍微有点麻烦。建议多做些最短路问题后在返回来完成它。

4.  POJ 2253  Frogger:

题意:水面上有很多石头,一只青蛙在某一块石头上,现在这只青蛙需要跳到另外一块石头上去看它的朋友,问:在这途中的最大距离最小是多少。

想法:这是最典型的最短路问题,只需在数据的存贮和代码实现上稍作修改即可。

5.  POJ 1125  Stockbroker Grapevine :

题意:现在我们需要把一个消息在一群人中快速传播开,即所有人都知道这个消息。问:最快让所有人都知道消息的时间,和最后一个知道消息的人。

想法:这是一眼就能看出来的最短路问题。

6.  POJ 2240  Arbitrage:

题意:给出所有货币名称和它们之间的汇率和手续费,问:能否通过不同货币之间的不断转换盈利

想法:这题和前面的POJ 1860相似,只是这个题在存储上稍微麻烦点。<我是用map存储的>

7.  POJ 1511  Invitation Cards:

题意:已知单向的公交车路线和坐车的价格,问:去赴宴然后回来的最低消费。

想法:这是一个比较好的最短路问题,因为它的数据规模比较大。如果没有这题我不会去学习SPFA的。

8.  POJ 2387  Til the Cows Come Home :

题意:已知一个双向的路线,问:最短路的路程。

想法:这是典型的最短路问题。

9.  POJ 3037 Skiing :

题意:已知地形的各个点的高度,初始速度。问:从左上角出发,到右下角时的最快速度。

想法:我当时做这题是怎么看都不像是最短路问题,之后瞟了一眼别人的思路后,才恍然大悟。真是非常好的最短路问题。

10.POJ 3615  Cow Hurdles:

题意:已知两个点之间的栅栏高度。问:在两点间的最高的栅栏高度的最小值。(可以间接到达。)

想法:通过这个题马上就体现出了Floyd算法的优势,这真是专为Floyd算法准备的题啊。

11.POJ 3660  Cow Contest :

题意已知N头牛之间的M次比试的结果,其中等级高的必定会胜过等级低的。问:通过这些比试记录能确定几头牛的等级。

想法:这是一个与上面第四题相似的最短路问题。同时还能快速了解什么是传递闭包,怎么通过Floyd解决。

12.POJ 3013  Big Christmas Tree

题意已知一棵树的各个节点权值和边的权值。问:从根到所有节点的分值的最小值。

想法:这也是一个数据比较大的最短路问题,虽然只是数据比较大。

13.POJ 3159  Candies:

题意在n个人中有m条关系,每条关系是表示为 A  B  k  (B最多只能比A多k个糖)。问:1最多比n多几颗糖。

想法:这个题真心非常好,这是一个典型的最短路问题中差分约束题。想解决这题需要稍微拐个弯。

下面是我个人在练习完最短路问题后的小总结,以及我给我自己的最短路模板。

     不过刚开始自己琢磨四个算法的时候真是越看越觉得神奇,现在,粗略理解了四个算法后,我个人对四个算法的使用做出了如下总结:

1.Bellman-Ford算法:

     这是我学习的第一个最短路算法,它属于单源最短路算法。这个算法所利用的是任意两点之间最短路的更新有限的特点,(更新次数就是点的数量。)同时如果存在负圈就会无限更新。(根据这个特点可以用来判断是否存在负圈。)在不断的练习后差不多就会写出一个属于自己的代码风格吧!下面的是给我自己模板:(以无向图为例。)

#include<iostream>

#define INF 0x3f3f3f3f 
/*因为练习期间查了很多的网上代码他们用的都是一个很大的数字 
在一次偶然的情况下看到了kuangbin的写的模板,于是就看了下这
个值发现0x3f3f3f3f的大小是1061109567,这个大小刚刚好非常适
合使用如果小了在很多情况下就不够用,如果大了就容易溢出。*/
#define MAX 1000 //特殊情况特殊分析 
using namespace std;

struct edge{
	int from,to,cost;
};
edge E[MAX*6];
int n,m,tol,start,en;
int cost[MAX];
void fill()
{//因为其他算法中基本上都要对很多东西进行初始化,所以干脆就
 //直接用这种方式进行初始花了万一写到一半想换个算法了呢? 
	for(int j=1;j<=n;j++)
		cost[j]=INF;
}
void Bellman_Ford(int start)
{
	cost[start]=0; //起点到起点的自然就是0了。 
	bool update=true; //用来判断每次玄幻中是否更新。
	int count=0; //用count记录更新的次数如果更新次数过多就说明出现了负圈。 
	while(update){
		if(count==n){ //更新次数过多说明已经出现了负圈。 
			cout<<"出现负圈"<<endl;return;
		}
		count++;
		update=false;
		for(int j=0;j<tol;j++){
			if(cost[E[j].to]>cost[E[j].from]+E[j].cost){
				cost[E[j].to]=cost[E[j].from]+E[j].cost;
				update=true;
			}
			if(cost[E[j].from]>cost[E[j].to]+E[j].cost)
			{//如果是定向图的就把这个掉。 
				cost[E[j].from]=cost[E[j].to]+E[j].cost;
				update=true;
			}
		}
	}
	return;
}
int main()
{
	while(cin>>n>>m){
		fill();
		tol=0;
		for(int j=0;j<m;j++){
			int from,to,cost;
			cin>>from>>to>>cost;
			E[tol].from=from,E[tol].to=to,E[tol].cost=cost;
			tol++;
		}
		cin>>start>>en;
		Bellman_Ford(start);
		cout<<cost[en]<<endl;
	}
}

       算法复杂度为O(n*m)并不是任何情况下都适用,不过在可以使用的情况下用来判断是否有负圈还是不错的选择。


2.Dijkstra算法:

       Dijkstra算法采用的是邻接表/邻接矩阵的做法,也是单源最短路算法。它的思路是在没有负圈的情况下,每次只要找出已知答案中最小的一个来进行更新即可。不过正应如此采用Dijkstra算法是无法判断负圈的存在,但是,我们大天朝的一个大学生在n年前就在这个基础上想出了一个时间复杂度更短而且能判断负圈的SPFA算法。同时使用不同的方式创建邻接表/邻接矩阵也会对代码的优劣性产生很大影响。下面是使用邻接矩阵方法写的一个我个人的模板:(还是以无向图为例)

#include<iostream>
#include<cstring>
#define INF 0x3f3f3f3f
#define MAX 1000
using namespace std;

int n,m;
int V[MAX][MAX],cost[MAX];
bool used[MAX];
void fill()
{//这里要对cost(总路程),used(是否被使用过),V(所有的边)进行初始化。 
	memset(V,-1,sizeof(V));
	for(int j=1;j<=n;j++){
		cost[j]=INF;
		used[j]=false;
	}
}
int min(int a,int b){ return a>b?b:a; }
void Dijkstra(int start)
{
	cost[start]=0; //起点设置路程为0。 
	while(true){
		int a=-1;
		for(int j=1;j<=n;j++)
		//通过这个找出最小的一个,在SPFA中对这一步直接就使用了优先队列。 
			if(!used[j]&&(a==-1||cost[a]>cost[j])) a=j;
		if(a==-1) break;
		//如果没有找到,就说明能找的都已经标记过了所以跳出循环 
		used[a]=true; //找出来之后就进行标记。 
		for(int j=1;j<=n;j++){ //然后对到所有点的路程进行更新。 
			if(V[a][j]!=-1)  //如果没有a->j的路径就可以忽略。 
				cost[j]=min(cost[j],cost[a]+V[a][j]);
		}
	}
	return;
}
int main()
{	
	while(cin>>n>>m){
		fill();
		int from,to,val;
		for(int j=0;j<m;j++){
			cin>>from>>to>>val;
			V[from][to]=val;
			V[to][from]=val; //如果是定向图就去掉这句。 
		}
		int start,en;
		cin>>start>>en;
		Dijkstra(start);
		cout<<cost[en]<<endl;
	}
}

       这个算法有很大的优化空间,比如可以把邻接矩阵换成使用邻接表,不过这两个就各有优劣了,所以,还是要具体情况具体分析。时间和空间发杂度都为O(n*n)。

3.Floyd-Warshall算法:

       Floyd-Warshall算法简称Floyd算法,它是一个多源最短路算法,同时还能用来判断负圈的存在。但是,它也具有很大的局限性。它的思路就是使用了DP的做法。因为它的实现比较简单,而且代码简短。所以,在数据规模比较小时比较常用。下面是我个人的模板:(以无向图为例)

#include<iostream>
#include<cstring>

#define MAX 100
#define INF 0x3f3f3f3f
using namespace std;
int cost[MAX][MAX];
int n,m;
int min(int a,int b) { return a>b?b:a; }
void Floyd()
{ 
	for(int i=1;i<=n;i++){
		for(int j=1;j<=n;j++){
			for(int k=1;k<=n;k++){
				if(cost[j][i]!=-1&&cost[i][k]!=-1){
					if(cost[j][k]==-1)
						cost[j][k]=cost[j][i]+cost[i][k];
					else
						cost[j][k]=min(cost[j][k],cost[j][i]+cost[i][k]);
					if(cost[j][k]<-1&&j==k)
						cout<<"存在负圈"<<endl; 
				}
			}
		} 
	}
	return;
}
int main()
{
	while(cin>>n>>m){
		
		memset(cost,-1,sizeof(cost));
		for(int j=1;j<=n;j++) cost[j][j]=0;
		for(int j=0;j<m;j++){
			cin>>from>>to>>val;
			cost[from][to]=min(cost[from][to],val); 
			cost[to][from]=min(cost[to][from],val);//如果是定向图解删除这一句。 
		}
		Floyd();
		for(int j=1;j<=n;j++)
		{//这个算法结束后就可以得到所有点之间的最短路程。 
			for(int k=1;k<=n;k++){
				cout<<cost[j][k]<<" ";
			}cout<<endl;
		}
	}
}
        三层for循环O(n*n*n )的复杂度,但是,思路简单代码实现也比较简单。所以,只有在数据规模比较小的情况下才会使用。


4.SPFA算法

      这个算法应该算是我们现在使用的最多的了,同时也是我最喜欢用的一个。就是因为它的时间发杂度比较低。它的思路就是在Dijkstra的基础上加上优先队列,同时在很多时候如果能在加上邻接表就可以是时间复杂度再降低一个等级。下面是我个人的模板:(由于使用的是邻接表所以下面的就是以定向图为例。)

#include<iostream>
#include<cstring>
#include<queue>

#define MAX 5005
#define INF 0x3f3f3f3f
using namespace std;
struct edge{
	int from;
	int to,cost;
};
struct Qpoit{  //在优先队列中的值是点和对应的路程。 
	int k,w;
};
bool operator < (const Qpoit & p1,const Qpoit & p2){ //优先队列从小取到大。 
	return p1.w>p2.w;
}
edge E[MAX*2];
int head[MAX],cost[MAX];  //用head和结构体来得到邻接表。 
bool Bque[MAX];   //使用这个来记录对应的点是否在队列中 
int tol;
int n,m;

void add_edge(int from,int to,int cost){  //加入的过程中更新邻接表 
	E[tol].to=to,E[tol].cost=cost,E[tol].from=head[from];
	head[from]=tol++;
}
void fill()  //初始化 
{
	for(int j=1;j<=n;j++){
		head[j]=-1;
		Bque[j]=false;
		cost[j]=INF;
	}
}
void SPFA(int start)
{
	priority_queue<Qpoit> que;
	Qpoit st,en;
	st.k=start,st.w=0;
	cost[start]=0;
	que.push(st);
	while(!que.empty()){
		st=que.top();que.pop();//这样st中取出的直接就是最小的。同时取出后必须马上弹出。 
		for(int j=head[st.k];j!=-1;j=E[j].from){ //在这里就已经在使用邻接表遍历了 
			en.k=E[j].to;
			if(cost[en.k]>cost[st.k]+E[j].cost){
				cost[en.k]=cost[st.k]+E[j].cost;
				en.w=cost[en.k];
				que.push(en);  //如果使用队列优化则,在加入队列前需要判断它是否在队列中。
			}
		}
	}
	return;
}
int main()
{
	while(cin>>n>>m){
		fill();
		tol=0;
		for(int j=0;j<m;j++){
			int from,to,cost;
			cin>>from>>to>>cost;
			add_edge(from,to,cost);
			add_edge(to,from,cost);//如果是有向图就可以把这句去掉。 
		}
		int start;
		cin>>start;
		SPFA(start);
	//在SPFA算法结束后输出每个点到起点的距离。 
		for(int j=1;j<=n;j++)
			cout<<cost[j]<<" ";
		cout<<endl;
	}
}


      这个SPFA中取数字的方式有很多除了使用优先队列以外还有队列、数组、堆、栈等,总之它们的作用的就是不断的更新每个点到起点的路程。同时更新后又将它加入到队列/数组/堆/栈中。(需要注意如果是用队列优化则在加入队列前还需要判断它是否在队列中。)


      上面四个代码中的内容对于时间复杂度上并不是最优的,在练习期间因为使用cin超时的不在少数,(因为,在输入过程中输入的量太大。)所以,一般情况下,我还会备一个输入输出外挂。而且上面的四个代码,是我再不断的练习后所形成的一种写的方式,优化的空间本身就还有很大。(而且说不定有些时候还不一定对。)总是最短路算法就是在每次都不断的更新每个点到起点的之间的距离。

     另外,在练习最短路的过程中下面这个网址中的内容对我也起到了很大的帮助。所以顺便就贴上这个网址。

    求最短路径的四种算法的详细讲解:HDU 1874 (最短路)Floyd-->>Dijkstra-->>Bellman_Ford-->>SPFA 
     http://blog.sina.com.cn/s/blog_7b7c7c5f01011yuu.html



  • 4
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值