最短路 (九度 OJ 1447)
1.题目描述:
在每年的校赛里,所有进入决赛的同学都会获得一件很漂亮的 t-shirt。但是每当我们的工作人员把上百件的衣服从商店运回到赛场的时候,却是非常累的!所以现在他们想要寻找最短的从商店到赛场的路线,你可以帮助他们吗?输入:输入包括多组数据。每组数据第一行是两个整数 N、M(N<=100,M<=10000),N 表示成都的大街上有几个路口,标号为 1 的路口是商店所在地,标号为 N 的路口是赛场所在地, M 则表示在成都有几条路。 N=M=0 表示输入结束。接下来M 行,每行包括 3 个整数 A, B, C(1<=A,B<=N,1<=C<=1000) ,表示在路口 A与路口 B 之间有一条路,我们的工作人员需要 C 分钟的时间走过这条路。输入保证至少存在 1 条商店到赛场的路线。当输入为两个 0 时,输入结束。
输出:
对于每组输入,输出一行,表示工作人员从商店走到赛场的最短时间。
样例输入:
2 1
1 2 3
3 3
1 2 5
2 3 5
3 1 2
0 0
样例输出:
3
2
2.基本思路
由于不考虑权值为负的情况,有两种思路。一种是求解多源最短路径的Floyd算法,另一种是求解单源最短路径的Dijkstra算法。
2.1Floyd算法
这里采用Floyd算法,其算法复杂度为
O
(
n
3
)
O(n^3)
O(n3),由于0≤n≤100,因此采用该算法是可以符合要求的。在Floyd中,维护一个邻接矩阵ans[N][N],其中ans[i][j]表示,结点i到结点j之间的距离。算法需要对待选择的结点进行遍历,依次将结点k(编号为1…n)加入到选中的集合中。在结点k加入到选中的集合中后,求解当前选中的集合中,所有的结点之间的最短路径。即利用以下的递推方程进行更新:
a
n
s
[
i
]
[
j
]
=
{
a
n
s
[
i
]
[
k
]
+
a
n
s
[
k
]
[
j
]
,
ans[i][k]+ans[k][j]<ans[i][j]
a
n
s
[
i
]
[
j
]
,
other
ans[i][j]= \begin{cases} ans[i][k]+ans[k][j], & \text{ans[i][k]+ans[k][j]<ans[i][j]} \\ ans[i][j], & \text{other} \end{cases}
ans[i][j]={ans[i][k]+ans[k][j],ans[i][j],ans[i][k]+ans[k][j]<ans[i][j]other
直到k=n时,即所有的结点都加入到选中的结点集合中的时候,此时的ans[1][n]便是所求的答案。
2.2Dijkstra算法
Dijkstra算法主要包括三个步骤:
1>.初始化,集合K中加入结点1,结点1到结点1的最短距离为0,到其他结点的距离为无穷(或不确定)。
2>.遍历与集合K中结点直接相邻的边(U,V,C),其中U属于集合K,V不属于集合K,计算由结点1出发按照先已得到的最短路径到达结点U,再由结点U经过该边到达V时的路径长度。比较到达所有与集合K中结点直接相邻的非集合K的结点的路径长度,得到最短路径长度对应的结点,将该结点加入到集合K中。
3>.若集合K中已经包含了所有的结点,算法结束;否则重复步骤2.
为了更加直观的说明Dijkstra算法,我们以下面一个简单的例子为例:
采用邻接链表表示上图所示的图。其中链表中的元素为结构体元素,定义如下
struct Edge{
int node;//vec[i][j].node,表示与第i个结点相邻的结点的第j个点的编号为node
int cost;//表示i与编号为node的结点之间的代价为cost
}
算法的求解过程如下所示,其中矩阵Dis[N]的元素Dis[i]表示结点1到结点i之间的最短距离
Mark[i]用于标记结点i是否已经访问过了。
随后我们来讲讲关于Dijkstra怎么打印出最短的路径。如下公式:
d
i
s
[
t
]
=
d
i
s
[
n
e
w
P
]
+
c
P
r
e
[
t
]
=
n
e
w
P
dis[t]=dis[newP]+c\\ Pre[t]=newP
dis[t]=dis[newP]+cPre[t]=newP
我们可以知道更新过程中,t点的前驱结点就是newP点,那么同样的道理,我们可以采用一个数组Pre[N]来存储每个结点的前驱结点的信息,其中Pre[j]表示结点j的前驱结点的编号。随后我们可以通过递归调用的方法打印出每个结点的前驱结点(类似于并查集查找根结点的过程)。
对于Floyd算法有同样的结论:
a
n
s
[
i
]
[
j
]
=
a
n
s
[
i
]
[
k
]
+
a
n
s
[
k
]
[
j
]
P
r
e
[
j
]
=
k
ans[i][j]=ans[i][k]+ans[k][j]\\ Pre[j]=k
ans[i][j]=ans[i][k]+ans[k][j]Pre[j]=k
打印过程的递归调用函数如下,由于按照从头到尾的顺序打印路径,因此我们在递归调用的下面书写打印语句,将函数回传时带有的结点信息打印出来。
void printThePath(int s,int e){//s为起点,e为终点
if(Pre[e]==s){
printf("%d->",s);
}
else{
printThePath(s,Pre[e]);
printf("%d->",Pre[e]);
}
}
3.代码实现
3.1Floyd算法
#include <iostream>
#define N 101
using namespace std;
int ans[N][N];
int n,m;
int x;
int main()
{
int a,b,cost;
while(scanf("%d%d",&n,&m)!=EOF){
if(n==0&&m==0)break;
for(int i=1;i<=n;i++){
for(int j=1;j<=n;j++){
if(i==j)
ans[i][j]=0;//对角线初始化为0
ans[i][j]=-1;//非初始初始化为-1
}
}
for(int i=0;i<m;i++){
scanf("%d%d%d",&a,&b,&cost);
ans[a][b]=ans[b][a]=cost;
}
for(int k=1;k<=n;k++){
for(int i=1;i<=n;i++){
for(int j=1;j<=n;j++){
if(ans[i][k]==-1||ans[k][j]==-1)
continue;
if(ans[i][j]==-1||ans[i][k]+ans[k][j]<ans[i][j])
ans[i][j]=ans[i][k]+ans[k][j];
}
}
}
printf("%d\n",ans[1][n]);
}
return 0;
}
/*
2 1
1 2 3
3 3
1 2 5
2 3 5
3 1 2
0 0
*/
3.2Dijkstra算法
#include <iostream>
#include <vector>
#include <climits>
#define N 101
using namespace std;
struct E{//声明链表中的结点
int next;//存储结点的编号
int cost;//存储前驱结点与结点相连的权重
};
vector<E> edge[N];//存储邻接链表
bool mark[N];//标记结点是否已访问过
int Dis[N];//存储从结点1到达该结点的最短路径长度
int main()
{
int n,m;
while(scanf("%d%d",&n,&m)!=EOF){
if(n==0&&m==0)break;
for(int i=1;i<=n;i++)
edge[i].clear();
for(int i=1;i<=m;i++){//将每一条边存储到邻接链表中
int a,b,c;//输入的一行数据
scanf("%d%d%d",&a,&b,&c);
E tmp;
tmp.next = b;
tmp.cost = c;
edge[a].push_back(tmp);
tmp.next = a;
tmp.cost = c;
edge[b].push_back(tmp);
}
//初始化Dis和mark数组
for(int i=1;i<=n;i++){
mark[i]=false;
Dis[i]=-1;
}
mark[1] = true;//初始化第1个结点
Dis[1] = 0;
int newP = 1;
for(int i=1;i<n;i++){//n-1次循环,寻找剩余的n-1个结点加入到选中的集合中
for(int j=0;j<edge[newP].size();j++){//遍历与newp结点相邻的所有结点
int t = edge[newP][j].next;
int c = edge[newP][j].cost;
if(mark[t]==true)
continue;
if(Dis[t]==-1||Dis[newP]+c<Dis[t])//Dis[newP]+c<Dis[t] 此时经由newP到达结点t的距离比经由K中其他结点到达t的距离更短,更新Dis[t]的值
Dis[t] = Dis[newP]+c;//对于到达结点t的距离,每次只利用经过新加入的点到达该点t的路径长度来更新Dis[t]即可
}
int min = INT_MAX;
for(int i=1;i<=n;i++){//遍历Dis数组中不属于集合k的结点,得到对用的Dis[i]最小的结点i,该结点即为将被加入到集合K中的结点
if(mark[i]==true)continue;
if(Dis[i]==-1)continue;
if(Dis[i]<min){
min = Dis[i];
newP = i;
}
}
mark[newP] = true;
}
printf("%d\n",Dis[n]);//输出答案
}
return 0;
}
/*
2 1
1 2 3
3 3
1 2 5
2 3 5
3 1 2
4 5
1 2 3
1 4 2
2 3 4
4 3 4
4 2 1
0 0
*/
以下为包括打印最短路径的完整代码:
#include <iostream>
#include <climits>
#include <vector>
#define N 101
using namespace std;
struct Edge{
int node;
int cost;
};
vector<Edge> Map[N];//Map[i]后面接的是与其相连的所有边的信息
bool mark[N];//标记某个结点是否已经加入了集合K中
int Dis[N];//Dis[i]存储结点1到结点i的距离
int Pre[N];//记录前驱结点的编号,Pre[j]表示结点j的前驱结点编号
void printThePath(int s,int e){
if(Pre[e]==s){
printf("[%d]->",s);
}
else{
printThePath(s,Pre[e]);
printf("[%d]->",Pre[e]);
}
}
int main()
{
int n,m;
while(~scanf("%d%d",&n,&m)){
if(n==0&&m==0)
break;
for(int i=1;i<=n;i++){
Map[i].clear();
mark[i]=false;
Dis[i]=-1;
}
int a,b,cost;
for(int i=1;i<=m;i++){//利用邻接链表创建图
scanf("%d%d%d",&a,&b,&cost);
Edge tmp;
tmp.node=b;
tmp.cost=cost;
Map[a].push_back(tmp);
tmp.node=a;
Map[b].push_back(tmp);
}
int newP=1;//将结点1加入到结合K中
mark[1] = true;
Dis[1] = 0;
Pre[1] = 1;
for(int i=1;i<=n-1;i++){//循环n-1次,确定1到其他n-1个结点的路径
for(int j=0;j<Map[newP].size();j++){
int t = Map[newP][j].node;
int c = Map[newP][j].cost;
if(mark[t])continue;//如果该结点已经访问过了
if(Dis[t]==-1||Dis[t]>Dis[newP]+c){
Dis[t]=Dis[newP]+c;
Pre[t]=newP;
}
}
//确定与第m近的结点最近的第m+1个点
int min=INT_MAX;
for(int j=1;j<=n;j++){
if(mark[j])
continue;
if(Dis[j]==-1)//第一遍漏了
continue;
if(Dis[j]<min){
min = Dis[j];
newP = j;
}
}
mark[newP]=true;
}
printf("The shortest path is:");
printThePath(1,n);
printf("[%d]\n",n);
}
return 0;
}
/*
2 1
1 2 3
3 3
1 2 5
2 3 5
3 1 2
0 0
4 5
1 2 5
1 3 2
2 4 1
3 4 5
2 3 5
6 9
1 2 1
1 3 2
2 3 1
2 4 1
3 4 1
3 5 2
4 5 1
4 6 10
5 6 1
*/