最短路径算法是PAT甲级考试常考算法,具体说来,最短路径包括Dijkstra算法、Floyd算法,其余的Bellman-Ford和SPFA基本不会考(参《算法笔记》胡凡,曾磊著)
目录
一、最短路径基本概念与问题分类
1.基本概念
给定图G(V,E),求一条从起点到终点的路径,是的这条路径上经过的所有边的边权之和最小。
2.问题分类
无负权单源最短路径:Dijkstra算法
有负权单源最短路径:Bellman-Ford算法(简称BF算法)以及BF算法的优化算法:SPFA(Shortest Path Faster Algorithm)
全源最短路径问题:Floyd算法
二、Dijkstra算法
1.伪代码
//G为图,一般设为全局变量;数组d为原点到达各个点的最短路径的长度,s为起点
Dijkstra(G, d[], s){
初始化;
for(循环n次){
u=未访问过且d[u]最小的顶点标号;
记u被访问过;
for(从u出发能到达的所有顶点v){
if(v未被访问 && 以u为中介点使s到顶点v的距离d[v]更小){
优化d[v];
}
}
}
}
2.示意图
3.参考代码
由于图可以用邻接矩阵和邻接表来实现,因此最短路径Dijkstra算法也有两种写法 。
3.1.邻接矩阵版
const int maxv=1000;
const int inf=1000000000;//无穷大量,也可以写作0x3fffffff(7个)
int n,G[maxv][maxv];//n为顶点数,G是邻接矩阵
int d[maxv]; //用于记录最短路径
bool vis[maxv]={false}; //用于记录是否已访问
void Dijkstra(int s){//s为源点
fill(d, d+maxv, inf);
d[s]=0;
for(int i=0; i<n; i++){
int u=-1, MIN=INF;
for(int j=0; j<n; j++){
if(vis[j]==false && d[j]<MIN){
u=j;
MIN=d[j];
}
}
//找不到小于INF的d[u],说明剩下的顶点和起点不连通
if(u==-1) return;
vis[u]=true;
//如果v未访问 && v能到达 && u能优化d[v]
for(int v=0; v<n; v++){
if(vis[v]==false && G[u][v]!=0 && d[v]>d[u]+G[u][v]){
d[v]=d[u]+G[u][v];//优化
}
}
}
}
3.2.邻接表版
struct Node{
int w;
int data;
};
vector<Node> Adj[maxn];
int n, dis[maxn];
bool vis[maxn] = {false};
void Dijkstra(int s){
fill(dis, dis+maxn, inf);
dis[s]=0;
for(int i=0; i<n; i++){
int u=-1; MIN=inf;
for(int j=0; j<n; j++){
if(vis[j]==false && dis[j]<MIN){
u=j;
MIN=dis[j];
}
}
if(u==-1) return;
vis[u]=true;
//只有下面部分不一样
for(int j=0; j<Adj[u].size(); j++){
v=Adj[u][j].data;//直接获得该连接点v
if(vis[v]==false && d[v]>d[u]+Adj[u][v].w){
d[v]=d[u]+Adj[u][v].w;
}
}
}
}
3.3.路径求法
上面讲的方法是计算每个点到源点最小距离,没有得出源点到该点的具体路径。
下面写最短路径的求法:
一般来说,我们需要在上面的基础上增加一个数组pre[maxn],其中的每个数据pre[v]表示使v点为当前最短路径长度这样的情况下的前驱点(前面一个点,也就是例子中的u)。也就是说,在更新d[v]的时候顺便将pre[v]=u。
以邻接矩阵为例,见修改后的代码(注意看“新添加”字样):
const int maxv=1000;
const int inf=1000000000;//无穷大量,也可以写作0x3fffffff(7个)
int n,G[maxv][maxv];//n为顶点数,G是邻接矩阵
int d[maxv]; //用于记录最短路径
int pre[maxv]; //用于记录每个点的前驱点————————————————————————————(新添加)
bool vis[maxv]={false}; //用于记录是否已访问
void Dijkstra(int s){//s为源点
fill(d, d+maxv, inf);
for(int i=0; i<n; i++) pre[i]=i; //初始化—————————————————————(新添加)
d[s]=0;
for(int i=0; i<n; i++){
int u=-1, MIN=INF;
for(int j=0; j<n; j++){
if(vis[j]==false && d[j]<MIN){
u=j;
MIN=d[j];
}
}
//找不到小于INF的d[u],说明剩下的顶点和起点不连通
if(u==-1) return;
vis[u]=true;
//如果v未访问 && v能到达 && u能优化d[v]
for(int v=0; v<n; v++){
if(vis[v]==false && G[u][v]!=0 && d[v]>d[u]+G[u][v]){
d[v]=d[u]+G[u][v];//优化
pre[v]=u; //记录前驱点 —————————————————————————(新添加)
}
}
}
}
完成了这一步,在我们的数组pre中就记录了每个点的上一个点,上一个点也可以找到他的前一个点,直到找到源点,再将其从原点开始输出。这样一个过程其实就是递归 :
void PrintPath(int s, int v){//从源点s到结点v的最短路径
if(v==s){//当递归到源点时
cout<<s<<endl;
return;
}
PrintPath(s,pre[v]);//递归
cout<<v<<endl;//最后一个点递归结束开始输出
}
4.升级
至此,基本介绍了Dijkstra算法的基本使用方法,但是题目肯定不会考的这么直接,至少会给它加些包装或者升级。我们按照以往考题,总结出以下三种升级形式:
- 给每条边增加一个边权(比如花费),然后要求在最短路径有多条时花费最小;
- 或是给每个节点增加一个点权(比如每个城市能够收到的物资),然后在路径最短时收集到的物资最多;
- 再或者直接问有多少条最短路径
下面针对这三种情况分别对代码进行修改:
//增加边权考法
/*增加int cost[][]数组,cost[u][v]表示u->v的花费
(类似于G[][]中的权重),并增加一个数组c[],c[u]表
示从源点到u点的最小花费(类似于最短路径的数组d[])
初始化:c[s]为0,其余全为inf*/
//则,在更新最短路径片段这样表示(邻接矩阵)
for(int v=0; v<n; v++){
if(G[u][v]!=0 && vis[v]==false){
//最短路径不同时更新路径与花费
if(d[v]>d[u]+G[u][v]){
d[v]=d[u]+G[u][v];
c[v]=v[u]+cost[u][v];
}
//最短路径相同看花费较小
else if(d[v]==d[u]+G[u][v] && c[v]>v[u]+cost[u][v]){
c[v]=c[u]+cost[u][v];
}
}
}
//增加点权考法,类似于增加边权
/*weight[u]表示城市中的物资数目,w[u]表示收集到的物资最大值。
初始化时:只有w[s]为weight[s],其余初始化为0
之后再更新最短路径更新即可*/
for(int v=0; v<n; v++){
if(vis[v]==false && G[u][v]!=0){
if(d[v]>d[u]+G[u][v]){
d[v]=d[u]+G[u][v];
w[v]=w[u]+weight[v];
}
else if(d[v]=d[u]+G[u][v] && w[v]<w[u]+weight[v]){
w[v]=w[u]+weight[v];
}
}
}
//直接问最短路径有几条
/*增加数组num[]表示起点到该点的最短路径图条数
初始化:只有num[s]为1,其余全为0*/
for(int v=0; v<n; v++){
if(vis[v]==false && G[u][v]!=0){
if(d[v]>d[u]+G[u][v]){
d[v]=d[u]+G[u][v];
num[v]=num[u];
}
else if(d[v]==d[u]+G[u][v]){
num[v]=num[u]+num[v];//距离相同则累加
}
}
}
5.Dijkstra+DFS
Dijkstra算法在进行多标尺或复杂情况处理还不尽完美,使用Dijkstra算法(计算最短路径)+DFS算法(按照标尺对各个路径进行比较)结合的方式可以让逻辑结构更为清晰。
要达到目的,首先需要调整pre[]数组改为vector<int> pre[maxn],这样在一个点可以存放所有具有最短路径的前置节点。
修改后的代码如下:
vector<int> pre[maxn];
void Dijkstra(int s){//开始节点
//初始化
fill(d, d+maxn, inf);//最短路径
d[s]=0;
//循环n次
for(int i=0; i<n; i++){
int MIN=inf, u=-1;
for(int j=0; j<n; j++){
if(vis[j]==false && d[j]<MIN){
MIN=d[j];
u=j;
}
}
if(u==-1) return;
vis[u]=true;
for(int v=0; v<n; v++){
if(vis[v]==false && G[u][v]!=0){
if(d[v]>d[u]+G[u][v]){//更新最短路径
d[v]=d[u]+G[u][v];
pre[v].clear();
pre[v].push_back(u);
}
else if(d[v]==d[u]+G[u][v]){
pre[v].push_back(u);
}
}
}
}
}
DFS代码可以修改为:
//增加变量
//path为最优路径、temp临时路径
vector<int> path, tempPath;
int optValue;//第二指标最优值
void DFS(int v, int s){//终点和起点
//边界条件:计算第二指标,比较替换保存到最优路径
if(v==s){
tempPath.push_back(v);//把最后一个点推入路径
int value;//新建指标值
计算路径tempPath的value;
if(value优于optValue){//如果更优更新路径
optValue = value;
path = tempPath;
}
tempPath.pop_back();//删除刚加入的节点
return;
}
//递归式
tempPath.push_back(v);
for(int i=0; i<pre[v].size(); i++){
DFS(pre[v][i],s);
}
//遍历完所有前驱节点,将当前节点删除,☆☆☆☆
tempPath.pop_back();
}
三、Floyd算法jiu
未完待续,先做题。。。