关于Dijstra的初级运用是,在第一标尺的基础上有下面三个角度:
- 边权:c[maxn] = {maxn}, cost[manx][maxn] = {inf};
- 点权:w[maxn] = {0}, weight[maxn] = {0};
- 最短路径条数:num[maxn] = {0};
a1003.cpp 用到了其中的两个,作为模板来刻意练习,练习如何将问题结构化,模板化。
再额外补充边权的代码,不是这道题的,但是加进来使其完整。
#include <stdio.h>
#include <string.h>
#include <algorithm>
using namespace std;
const int maxv = 510; // 最大顶点数
const int inf = 1 << 30;
int G[maxv][maxv], weight[maxv],cost[maxv][maxv]; // 定义图的邻接矩阵存法,点权
int d[maxv] = {inf} ; //记录从起点s到u的最短距离,
int n,m,st,ed; // n:顶点数,m:边数
int w[maxv],num[maxv]c[manv]; // w:记录最大点权之和,num:最短路径条数
bool vis[maxv] = {false}; // 标记是否已经访问
// 最朴素的迪杰斯塔拉就是为了找到最短路径,最终效果是更新了d数组,
// 但是在产生d数组的过程中,有意思的事情正在发生
void Dijkstra(int s)
{
// 初始化
fill(d,d + maxv, inf); //
fill(num,num + maxv, 0);
fill(w,w + maxv, 0);
d[s] = 0;
w[s] = weight[s];
num[s] = 1;
// 循环n次
for(int i = 0; i < n; i++)
{
// 找到u和最小值
int u = -1, MIN = inf;
for(int j = 0; j < n; j++)
{
if(vis[j] == false && d[j] < MIN)
{
u = j;
MIN = d[j];
}
}
if(u == -1) return; // 没有找到
vis[u] = true; // 找到了就标记为已经访问
// 优化路径长度
for(int v = 0; v < n; v++)
{
if(vis[v] == false && G[u][v] != inf)
{
if(d[u] + G[u][v] < d[v]) // 这个条件拿下来用
{
d[v] = d[u] + G[u][v]; // 覆盖
c[v] = c[u] + cost[u][v]; // 补充的边权
w[v] = w[u] + weight[v]; // 最短路径的权重更大
num[v] = num[u]; // 三个基础问题中唯一一个用到继承的概念的
}
else if(d[u] + G[u][v] == d[v]) // 最短路径有相同的,这时候就看点权之和
{
if(w[u] + weight[v] > w[v])
{
w[v] = w[u] + weight[v];
}
if(c[u] + cost[u][v] < c[v])
{
c[v] = c[u] + cost[u][v];
}
num[v] += num[u];
}
}
}
}
}
int main()
{
scanf("%d%d%d%d", &n, &m, &st, &ed);
for(int i = 0; i < n; i++)
{
scanf("%d",&weight[i]);
}
int u, v;
fill(G[0],G[0] + maxv * maxv, inf);// 初始化二维数组的写法
for(int i = 0; i < m; i++)
{
scanf("%d%d", &u, &v);
scanf("%d", &G[u][v]);//读入边权
G[v][u] = G[u][v];
}
Dijkstra(st);
printf("%d %d\n",num[ed],w[ed]);
return 0;
}
为了理解Dijkstra + DFS,需要首先消化掉只在第一标尺下的DFS与记录前驱的pre数组的方式,然后才能更好的理解多个标尺下的Dijkstra + DFS的思路。
void DFS(int s, int v) // s是起点编号, v是当前访问的顶点编号,这个递归函数目的是为了求s到v的路径
{
// 前提是pre数组准备好,pre[i] = i,初始化时是自身是自身的前驱(联想到并查集啦)
// 递归边界
if(v == s)
{
printf("%d\n", s);
return;
}
DFS(s,pre[v]);
printf("%d\n",v);
}
单纯的用递归函数的设计逻辑来理解这个问题,就会很简单:
- 递归边界:v == s,即起点和终点重合了,自然要输出,并结束本层函数
- 递归式:起点是固定的,是fixed,第二个函数是终点往前挪到前驱
- 本层逻辑:递归回来,要输出当前这个结点的编号
用Dijkstra + DFS组合解题的情景是解脱出在Dijikstra时要处理的逻辑较为复杂,这里的说的复杂逻辑不是DIjkstra本身,Dijkstra的框架是非常简洁优美且代码好写的。
如果只在Dijkstra中记录所有最短路径,然后再在DFS中根据其他标尺求出最优路径,这种做法也非常符合关注点分离的思想。
所以,首先看第一个问题:如何在Dijkstra中记录所有最短路径。
vector<int> pre[maxn] // 存储多条最短路径:仅在第一标尺--距离的指引下
在这种记录多条最短路径的pre数组中,开始不用初始化,看到代码会更清晰:
if(d[u] + G[u][v] < d[v]) { d[v] = d[u] + G[u][v]; pre[v].clear(); // 清空,此时找到更好的了 pre[v].push_back(u); } else if(d[u] + G[u][v] == d[v]) { pre[v].push_back(u); // 令v的前驱为u }
关于为什么在找到更小的距离时清空pre[v]数组,是因为pre[v]存的是v这个顶点距离最小的前驱,那么现在找到了更小的,意味着原来的记录的前驱没用了,自然清空。也因为此,pre数组开始不必初始化。
现在数组已经准备好,开始写DFS遍历所有最短路径,依据其他标尺选择最优。
int optValue;
vector<int> pre[maxv];
vector<int> path, tempPath;
void DFS(int v)
{
if(v == st) // st是起点
{
tempPath.push_back(v); // 将起点st加入临时路径tempPath的最后面
int value; // 存放临时路径的第二标尺值
// 计算tempPath上的value值
if(value 优于 optValue)
{
optValue = value;
path = tempPath;
}
tempPath.pop_back();
return;
}
tempPath.push_back(v); // 将当前访问结点加入临时路径tempPath最后面
for(int i = 0; i < pre[v].size(); i++)
{
DFS(pre[v][i]);
}
tempPath.pop_back();// 遍历完所有前驱结点,将当前结点v删除
}
注意到push_back和pop_back是成对出现的这里。
这是形式上的准确识别记忆,那么如何理解呢?
其实非常简洁,tempPath存储的是一条路径 ,即需要考察起点st到当前结点之间的某个标尺值,tempPath因为是采用递归写法,所以是倒着的:从v到st。
本层逻辑是先把v加入进来,然后递归v的前驱,刚好前驱是往前(向着起点)走。我们先忽略掉递归式,看到最后一句pop_back,这样就能保证执行完一次递归获得一条路径后,tempPath被清空。
而至于递归边界中的一对,是因为本层逻辑中无法把起始点加入tempPath,所以这里需要临时用到,所以临时加进来。
这样Dijkstra + DFS模板的问题框架与部分细节就搭起来了,再来看最初提到的三个基础问题这里如何计算:
- 点权之和
- 边权之和
- 最小路径条数
最简单的是最小路径条数:用一个全局变量num = 0, 在递归边界中num++即可。
点权之和与边权之和都是上面模板中的value,具体写法也很简单,就是遍历tempPath数组。注意,path是存的几种标尺综合的最优解。
// 边权之和:注意是i > 0
int value = 0;
for(int i = tempPath.size() - 1; i > 0; i++)
{
int u = tempPath[i], v = tempPath[i - 1];
value += G[u][v];// G[u][v]是边权
}
// 点权之和
int value = 0;
for(int i = tempPath.size() - 1; i >= 0; i++)
{
int u = tempPath[i];
value += weight[u];// weight是边权
}