Dijkstra+DFS模板总结

关于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是边权
}
    原文作者:Dijkstra算法
    原文地址: https://blog.csdn.net/u011240016/article/details/59758033
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞