1 Dijkstra(单源点最短路径问题)

1.1 最短距离

1.1.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];
}
}
}
}

1.1.2 邻接矩阵

  • 用于点数不大(不超过1000)
  • 时间复杂度
  • 外层循环O(V)【V就是顶点个数】与内层循环(寻找最小的d[u]需要O(V)、枚举V需要O(V),总时间复杂度为O(V * (V + V)) = O(V2)

const int MAXV = 1000;//最大顶点数
const int INF = 0x3fffffff;
int G[MAXV][MAXV], n;//图、顶点个数
int d[MAXV];//起点到达各点的最短路径
bool vis[MAXV] = {false};//标记是否被访问

void Dijkstra(int s){//s起点
fill(d, d + MAXV, INF);//将整个数组d赋值为INDF,即不可达
d[s] = 0;//起点到达自身的距离为0
for (int i = 0; i < n; ++i)//循环c次
{
int u = - 1, min = INF;//u使d[u]最小,MIN存放该最小的d[u]
for (int j = 0; j < n; ++j)//找到未访问的顶点中d[]最小的
{
if(vis[j] == false && d[j] < min){
u = j;
min = d[j];
}
}

//找不到小于INF的d[u],说明剩下的顶点与起点s不连通
if(u == -1) return;
vis[u] = true;//标记u已被访问过
for (int v = 0; v < n; ++v)
{
//如果v未访问 && u能达到v && 使u为中介点能使d[v]更优
if(vis[v] == false && G[u][v] != INF && d[u] + G[u][v] < d[v]){
d[v] = d[u] + G[u][v];//优化d[v]
}
}
}
}

1.1.3 邻接表

  • 时间复杂度
  • 外层循环O(V)【V就是顶点个数】与内层循环(寻找最小的d[u]需要O(V)、枚举V需要O(adj[u].size()),枚举V的总次数为O(无负权图最短路径之Dijkstra——附模版伪码、完整实现代码(邻接矩阵、邻接表)、示例和常考点_数组) = O(E),总时间复杂度为O(V2+ E)
struct node
{
int v;//边的目标顶点
int dis;//边权
};

const int MAXV = 1000;//最大顶点数
const int INF = 0x3fffffff;
vector<node> Adj[MAXV];邻接表
int n;//顶点个数
int d[MAXV];//起点到达各点的最短路径
bool vis[MAXV] = {false};//标记是否被访问

void Dijkstra(int s){//s起点
fill(d, d + MAXV, INF);//将整个数组d赋值为INDF,即不可达
d[s] = 0;//起点到达自身的距离为0
for (int i = 0; i < n; ++i)//循环c次
{
int u = - 1, min = INF;//u使d[u]最小,MIN存放该最小的d[u]
for (int j = 0; j < n; ++j)//找到未访问的顶点中d[]最小的
{
if(vis[j] == false && d[j] < min){
u = j;
min = d[j];
}
}

//找不到小于INF的d[u],说明剩下的顶点与起点s不连通
if(u == -1) return;
vis[u] = true;//标记u已被访问过
for (int j = 0; j < Adj[u].size(); ++j)
{
int v = Adj[u][j].v;//通过邻接表直接获得u能到达的顶点v
if(vis[v] == false && d[u] + Adj[u][j].dis < d[v]){
//v未被访问 && 以u为中介点可以使d[v]更优
d[v] = d[u] + Adj[u][j].dis;//优化d[v]
}
}
}
}

1.1.4 优化以及注意点

  • 优化
  • 上面两种算法的时间复杂度都是O(V2)级别,由于必须把每个顶点都标记为已访问,所以外层循环的O(V)无法避免,但寻找最小d[u]的却可以不必到达O(V)的长度,可以使用堆优化来降低复杂度,最简洁的写法是直接使用STL的优先队列priority_queue,这样可以使得邻接表的时间复杂度降低到O(VlogV+E)
  • 注意
  • 如果求的是无向边,只用把无向边当作两条指向相反的有向边即可。

1.1.5 示例

  • 问题描述:
    Input Specification:
    Each input file contains one test case. For each case, the first line contains three positive integer n(<=1000), m , s, which are the number of nodes, the number of edge, and start of nodes.Then m lines follow, each describes each edge by 3 integers, which are starting node, ending node, and weight of node.

Output Specification:
output in a line the number of nodes distance which from starting node to each nodes .

Sample Input:
6 8 0
0 1 1
0 3 4
0 4 4
1 3 2
2 5 1
3 2 2
3 4 3
4 5 3
Sample Output:
0 1 5 3 4 6
  • 求解代码
#include 
#include

using std::fill;

const int MAXV = 1000;
const int INF = 0x3fffffff;
int G[MAXV][MAXV];
bool vis[MAXV] = {false};
int d[MAXV];
int n, m, s;

void Dijkstra(int 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];
}
}

if(u == -1) return;

vis[u] = true;

for (int v = 0; v < n; ++v)
{
if(vis[v] == false && G[u][v] != INF && d[u] + G[u][v] < d[v]){
d[v] = d[u] + G[u][v];
}
}
}
}

int main(int argc, char const *argv[])
{
int u, v ,w;
scanf("%d%d%d", &n, &m, &s);//顶点个数、边数、起点编号

fill(G[0], G[0] + MAXV * MAXV, INF);//初始化图G

for (int i = 0; i < m; ++i)
{
scanf("%d%d%d", &u, &v, &w);//输入u、v以及u->v的边权
G[u][v] = w;
}

Dijkstra(s);

for (int i = 0; i < n; ++i)
{
printf("%d ", d[i]);
}
return 0;
}
/*
6 8 0
0 1 1
0 3 4
0 4 4
1 3 2
2 5 1
3 2 2
3 4 3
4 5 3
*/

1.2 最短路径

1.2.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];
令v的前驱为u;
}
}
}
}

1.2.2 邻接矩阵

const int MAXV = 1000;
const int INF = 0x3ffffff;
int G[MAXV][MAXV], n;
bool vis[MAXV] = {false};
int pre[MAXV];
int d[MAXV];

void Dijkstra(int s){//求出最短路径上每个结点的前驱
fill(d, d + MAXV, INF);

for (int i = 0; i < n; ++i)//新增
{
pre[i] = i;//初始状态每个结点的前驱为自身
}

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];
}
}

if(u == -1) return;
vis[u] = true;
for (int v = 0; v < n; ++v)
{
if(vis[v] == false && G[u][v] != INF && d[u] + G[u][v] < d[v]){
d[v] = d[u] + G[u][v];
pre[v] = u;
}
}
}
}

void DFS(int s, int v){//s:起点编号;v:当前访问的顶点编号(从终点开始递归)
if(v == s){
printf("%d\n", s);
return;
}

DFS(s, pre[v]);//递归访问v的前驱结点pre[v]
printf("%d\n", v);//从最深处return后,输出每一层的顶点号
}

1.2.3 邻接表

struct node
{
int v;
int dis;
};

const int MAXV = 1000;
const int INF = 0x3ffffff;
vector<node> Adj[MAXV];邻接表
int n;
bool vis[MAXV] = {false};
int pre[MAXV];
int d[MAXV];

void Dijkstra(int s){//求出最短路径上每个结点的前驱
fill(d, d + MAXV, INF);

for (int i = 0; i < n; ++i)//新增
{
pre[i] = i;//初始状态每个结点的前驱为自身
}

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];
}
}

if(u == -1) return;
vis[u] = true;
for (int j = 0; j < Adj[u].size(); ++j)
{
int v = Adj[u][j].v;
if(vis[v] == false && d[u] + Adj[u][j].dis < d[v]){
d[v] = d[u] + Adj[u][j].v;
pre[v] = u;
}
}
}
}

void DFS(int s, int v){//s:起点编号;v:当前访问的顶点编号(从终点开始递归)
if(v == s){
printf("%d\n", s);
return;
}

DFS(s, pre[v]);//递归访问v的前驱结点pre[v]
printf("%d\n", v);//从最深处return后,输出每一层的顶点号
}

1.3 相关考点

当起点到终点的最短路径不止一条时,题目会给出第二标尺(第一标尺为距离),要求在所有的最短路径中选择第二标尺最优的一条。第二标尺常见的是以下三种出题方法或组合:

  • 1 新增边权
    以新增边权代表花费为列,用cost[u][v]表示u->v的花费(由题目输入),并新增一个数组c[],令从起点s到达顶点u最少花费为c[u],初始化时,c[s]为0,其余c[u]为INF。
for (int v = 0; v < n; ++v)
{
//如果v未访问&&u能到达v
if(vis[v] == false && G[u][v] != INF){
if(d[u] + G[u][v] < d[v]){//以u为中介点可以使d[v]更优
d[v] = d[u] + G[u][v];
c[v] = c[u] + cost[u][v];
}else if(d[u] + G[u][v] == d[v] && cost[u] + cost[u][v] < cost[v]){
c[v] = c[u] + cost[u][v];//最短距离相同时看能否使c[v]更优
}
}
}
  • 2 新增点权
    以新增的点权代表城市中能收集到的物资为例,用weight[u]代表城市u中的物资数目(由题目输入),并增加一个数组w[],令从起点s到达顶点u可以收集到的最大物资为w[u],初始化时只有w[s]、其余w[u]均为0。
for (int v = 0; v < n; ++v)
{
//如果v未访问&&u能到达v
if(vis[v] == false && G[u][v] != INF){
if(d[u] + G[u][v] < d[v]){//以u为中介点可以使d[v]更优
d[v] = d[u] + G[u][v];
w[v] = w[u] + weight[u][v];
}else if(d[u] + G[u][v] == d[v] && w[u] + weight[v] > w[v]){
w[v] = w[u] + weight[v];//最短距离相同时看能否使w[v]更优
}
}
}
  • 3 求最短路径条数。
    只需增加一个数组num[],令从起点s到达顶点u的最短路径条数为num[u],初始化时,只有num[s]为1、其余num[u]均为0。
for (int v = 0; v < n; ++v)
{
//如果v未访问&&u能到达v
if(vis[v] == false && G[u][v] != INF){
if(d[u] + G[u][v] < d[v]){//以u为中介点可以使d[v]更优
d[v] = d[u] + G[u][v];
num[v] = num[u];
}else if(d[u] + G[u][v] == d[v]){
num[v] += um[u];//最短距离相同时看能否使w[v]更优
}
}
}

1.4 扩展

如果第二标尺为逻辑复杂的计算边权或点权的方式,只是用Dijkstra算法不一定能计算出正确结果(不一定满足最优子结构),此时可以先用Dijkstra算法记录下所有最短路径(只考虑距离),然后从这些最短路径中选出一条第二标尺最优的路径。

1.4.1 使用Dijkstra记录所有最短路径

  • 邻接矩阵版
const int MAXV = 1000;
const int INF = 0x3fffffff;
int G[MAXV][MAXV];
bool vis[MAXV];
int d[MAXV];
int N;
vector<int> pre[MAXV];//记录每个结点的前驱结点

void Dijkstra(int 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];
}
}

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];
pre[v].clear();
pre[v].push_back(u);
}else if(d[u] + G[u][v] == d[v]){
pre[v].push_back(u);
}
}
}
}
}
  • 邻接表
struct node
{
int v;//结点编号
int dis;//边权
};

const int MAXV = 1000;
const int INF = 0x3fffffff;
vector<node> G[MAXV];
bool vis[MAXV];
int d[MAXV];
int N;
vector<int> pre[MAXV];

void Dijkstra(int 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];
}
}

if(u == -1) return;

vis[u] = true;
for (int j = 0; j < N; ++j)
{
int v = G[u][j].v;
if(vis[v] == false){
if(d[u] + G[u][v].dis > d[v]){
d[v] = d[u] + G[u][v].dis;
pre[v].clear();
pre[v].push_back(u);
}else if(d[u] + G[u][v].dis == d[v]){
pre[v].push_back(u);
}
}
}
}
}

1.4.2 遍历所有最短路径,找一条使第二标尺最优的路径

int st;//起点
int optvalue;//第二标尺最优
vector<int> path, tempPath;//最优路径、临时最优路径
void DFS(int v){
//递归边界
if(v == st){//如果达到了叶子结点st(即路径起点)
tempPath.push_back(v);//将起点st放在临时路径tempPath的最后
int value; //存放临时路径tempPath的第二标尺值
//计算tempPath的第二标尺值
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();//遍历完所有前驱结点,将刚加入的结点删除
}
  • 存放在tempPath中的路径结点为逆序,因此访问结点时要倒着进行。
  • value 优于 optvalue:以计算tempPath上的边权和点权之和为例
//边权之和
int value = 0;
for (int i = tempPath.size(); i > 0 ; --i)//倒着访问,循环条件为i大于0
{
//当前结点id,下一个结点idNext
int id = tempPath[i], idNext = tempPath[i -1];
value += V[id][idNext];//value增加id->idNext的边权
}

//点权
int value = 0;
for (int i = tempPath.size(); i >= 0 ; --i)//倒着访问,循环条件为i>=0
{
//当前结点id
int id = tempPath[i];
value += W[id];//value增加结点id的点权
}