一、概述
求最短且花费最少的路径。
首先用Dijsktra求最短路径并保存,然后用DFS求花费最少的路径。
要求Dijsktra和DFS都写的比较熟练,那这题就不难。
二、分析
首先分析图的储存方式,由于是求最短路径,那一般使用邻接矩阵,要是遍历则可以使用邻接表。
开二维数组G。需要将二维数组初始化,这个初始化不能为0,因为G是用来储存两个点之间的距离的。
一定要分清是要求遍历图还是求最短路径,遍历图的话邻接矩阵可以只存0或者1,求最短路径那邻接矩阵必须储存距离了。
将G的所有元素初始化为INF(0x3f3f3f3f),使用fill函数,和sort一类函数差不多,数组头,数组头加元素个数,赋值。
开二维数组cost。储存两点间的花费。这个就可以在初始化时直接置零了。然后输入即可。如下:
fill(G[0],G[0]+510*510,INF);
scanf("%d %d %d %d",&N,&M,&S,&D);
//无向图
for(int i=0;i<M;i++)
{
int j,k,distance,roadcost;
scanf("%d %d %d %d",&j,&k,&distance,&roadcost);
G[j][k]=distance;
G[k][j]=distance;
cost[j][k]=roadcost;
cost[k][j]=roadcost;
}
注意是无向图,因此要赋值两次。
然后开始Dijsktra。
Dijsktra也需要一个访问数组,用于储存哪个元素已经访问完了。在这一点上类似DFS,但是它不采用递归,而是不断循环。通过维护另外一个数组min来求出源点到其他所有点的最短距离。如下:
void Dijkstra(int i)
{
int now=1;//目前已确定的点
//首先找到最小的MIN[],然后根据这个MIN[]重新优化整个MIN
fill(MIN,MIN+510,INF);
MIN[i]=0;
while(now<=N)
{
int nowmind=INF;
int nowminnode=-1;
//找到MIN中最小的未被访问过的节点
for(int j=0;j<510;j++)
{
if(MIN[j]<nowmind&&vis[j]==0)
{
nowmind=MIN[j];
nowminnode=j;
}
}
if(nowminnode==-1)
return;
//访问该节点
vis[nowminnode]=1;
now++;
//查看该节点的所有相邻节点
for(int j=0;j<510;j++)
{
//找到一个相邻节点
if(G[nowminnode][j]<INF)
{
//如果源点到该节点距离加上该节点到相邻节点距离小于当前到相邻节点的最小距离
if(MIN[j]>MIN[nowminnode]+G[nowminnode][j])
{
//更新min
MIN[j]= MIN[nowminnode]+G[nowminnode][j];
//更新pre
pre[j].clear();
pre[j].push_back(nowminnode);
}
else if(MIN[j]==MIN[nowminnode]+G[nowminnode][j])
{
pre[j].push_back(nowminnode);
}
}
}
}
}
Dijsktra算法的套路也是固定的:
两个数组min和vis,至少一个输入i,作为源点。
首先初始化min和vis,将i对应的min置零,vis置1。
然后进入循环:
在min中找出未访问过的最小的j,将其vis置一,然后遍历j的所有相邻节点,看看能不能把min更新。能更新就更新,不能更新就不动。
注意,vis置一也就意味着这个点的最短路径已经找到了。
所有相邻节点遍历完了,再去找min。直到最后所有节点都访问完退出循环,Dijsktra也就完成了。
这是最简单的Dijsktra,只能求单一源点的最短距离,我们如果想要保存路径,则需要使用vector数组,即数组元素是vector的数组。用它来保存每个点在最短路径上的前置。因为前置可能不止一个,因此要用vector。
这个保存路径有点难理解,是在遍历j的相邻节点时完成的。
对于j的所有相邻节点,我们进行如下判断:
设当前相邻节点为k,源点为a。那么如果现在a到k的距离大于a到j到k的距离,那么则找到了一条新的到k的途径,它与原来的完全不同。而想到我们储存路径用的是储存前驱的方法,既然路径不同了,那么要把k的前驱先全清除,再设置j为k的前驱。此外,还需更新min数组。
如果现在a到k的距离等于于a到j到k的距离,那么也是找到了一条新的路径,但是它和原来的路径地位相同,直接添加j为k的前驱即可。
那么存不存在这样一种情况呢:即我找到了一个点x,vis已经置为1,但现在又找到一个点y,y与x相邻,而且由源点到y再到x的距离等于甚至小于x。这是不存在的,这也就是为什么Dijsktra不能支持负边。我们想象一下,假如这种情况存在,那么说明源点到y的距离小于x,那么y就不可能在x之后选,这在min选择那一步已经防止了这种情况发生。
最后我们在Dijsktra之后得到了一个pre数组和一个min数组。
然后开始根据pre数组DFS。
首先明确使用DFS的目的:求出代价最小的路径。
因此既需要储存路径又需要储存最小代价。如下:
void DFS(int d,int nowcost)
{
if(d==S)
{
temp.push_back(d);
if(nowcost<costmin)
{
costmin=nowcost;
costpath.assign(temp.begin(),temp.end());
}
temp.pop_back();
return;
}
else
{
temp.push_back(d);
vector<int>::iterator it;
for(it=pre[d].begin();it!=pre[d].end();it++)
{
int x=*it;
DFS(x,nowcost+cost[d][x]);
}
temp.pop_back();
return;
}
}
写DFS,首先要明确递归出口。
这里递归出口即是当所遍历的节点为源点时。因为是从后往前找前置嘛,这个要记住。
其次就是递归体了。循环加递归即可。具体到本题来说,因为要存储路径上的节点,所以DFS有点麻烦:
DFS的参数是目的点。从目的点进来,将其压入路径向量,然后DFS它的pre,也就是它的前置,一直到源点。
当遍历到源点时,将源点压入向量,判断目前的路径代价和是否是最小的:
注意!求路径代价和有两种方法,一个是在到达源点时根据路径向量一个一个加,一个是将代价和作为DFS的参数参与运算。
我这时才明白为什么有的变量要作为DFS的参数而不能作为全局变量:以代价和为例,每进入一次DFS,代价和要变化一次,从这个DFS出去,又要变化一次,虽然加加减减也不是不可以,但是这样太麻烦了。因此对于这种每次进出DFS都要变化的量,作为参数是最好的。
如果是最小的,那么便另外储存目前的代价和和路径向量。这里注意一点,向量a,b,使用a=b是不能把b的所有内容赋值给a的,要使用a.assign(b)才行。
如果不是最小的,将源点弹出向量。return。
另外不要忘了一点,当一个点的所有前驱都遍历完了,要将它也弹出去。
这样就得到了代价最小路径。
然后是输出。注意我们代价最小路径是用向量存储的,而且是从目标点一路到源点,所以要倒序输出向量。但平时都是正序输出的,倒序输出最好使用专门的倒序迭代器reverse_iterator,使用rbegin和rend,这样就可以实现倒序输出了。如下:
vector<int>::reverse_iterator it;
/*for(it=costpath.end();it!=costpath.begin();it--)//vector倒序输出
{
int x=*it;
printf("%d ",x);
}*/
for(it=costpath.rbegin();it!=costpath.rend();it++)//vector倒序输出
{
int x=*it;
printf("%d ",x);
}
注意不要像注释里那样输出。
三、总结
蛮复杂的一道题,相当于两道题叠加。关键是通过Dijkstra得到pre和通过DFS得到path。要熟练。
PS:代码如下:
#include<stdio.h>
#include<cstdio>
#include<iostream>
#include<string>
#include<cstring>
#include<map>
#include<vector>
#include<algorithm>
const int INF=0x3f3f3f3f;
using namespace std;
//Dijsktra算法一般都是用邻接矩阵
//对二维数组初始化一般用fill函数
//邻接矩阵存储距离
int G[510][510];
//cost矩阵存储花费
int cost[510][510];
//已访问数组
int vis[510]={0};
//前驱数组
vector<int> pre[510];
//思路就是由G求出pre,由pre和cost求出结果
int N,M,S,D;//点数量,边数量,起点,终点
int MIN[510];
void Dijkstra(int i)
{
int now=1;//目前已确定的点
//首先找到最小的MIN[],然后根据这个MIN[]重新优化整个MIN
fill(MIN,MIN+510,INF);
MIN[i]=0;
while(now<=N)
{
int nowmind=INF;
int nowminnode=-1;
//找到MIN中最小的未被访问过的节点
for(int j=0;j<510;j++)
{
if(MIN[j]<nowmind&&vis[j]==0)
{
nowmind=MIN[j];
nowminnode=j;
}
}
if(nowminnode==-1)
return;
//访问该节点
vis[nowminnode]=1;
now++;
//查看该节点的所有相邻节点
for(int j=0;j<510;j++)
{
//找到一个相邻节点
if(G[nowminnode][j]<INF)
{
//如果源点到该节点距离加上该节点到相邻节点距离小于当前到相邻节点的最小距离
if(MIN[j]>MIN[nowminnode]+G[nowminnode][j])
{
//更新min
MIN[j]= MIN[nowminnode]+G[nowminnode][j];
//更新pre
pre[j].clear();
pre[j].push_back(nowminnode);
}
else if(MIN[j]==MIN[nowminnode]+G[nowminnode][j])
{
pre[j].push_back(nowminnode);
}
}
}
}
}
//DFS是从终点往回找起点
vector<int> costpath,temp;
int costmin=INF;
void DFS(int d,int nowcost)
{
if(d==S)
{
temp.push_back(d);
if(nowcost<costmin)
{
costmin=nowcost;
costpath.assign(temp.begin(),temp.end());
}
temp.pop_back();
return;
}
else
{
temp.push_back(d);
vector<int>::iterator it;
for(it=pre[d].begin();it!=pre[d].end();it++)
{
int x=*it;
DFS(x,nowcost+cost[d][x]);
}
temp.pop_back();
return;
}
}
int main()
{
fill(G[0],G[0]+510*510,INF);
scanf("%d %d %d %d",&N,&M,&S,&D);
//无向图
for(int i=0;i<M;i++)
{
int j,k,distance,roadcost;
scanf("%d %d %d %d",&j,&k,&distance,&roadcost);
G[j][k]=distance;
G[k][j]=distance;
cost[j][k]=roadcost;
cost[k][j]=roadcost;
}
Dijkstra(S);
DFS(D,0);
vector<int>::reverse_iterator it;
/*for(it=costpath.end();it!=costpath.begin();it--)//vector倒序输出
{
int x=*it;
printf("%d ",x);
}*/
for(it=costpath.rbegin();it!=costpath.rend();it++)//vector倒序输出
{
int x=*it;
printf("%d ",x);
}
printf("%d ",MIN[D]);
printf("%d",costmin);
}