洛谷 P3371 【模版】单源最短路径(弱化版)

洛谷 P3371 【模版】单源最短路径(弱化版)

题目

给出一个有向图,请输出从某一点出发到所有点的最短路径长度。
题目链接【模板】单源最短路径(弱化版) - 洛谷

输入

第一行包含三个整数N、M、S,分别表示点的个数、有向边的个数、出发点的编号。接下来M行每行包含三个整数Fi、Gi、Wi,分别表示第i条有向边的出发点、目标点和长度。

输出

一行,包含N个用空格分隔的整数,其中第i个整数表示从点S出发到点i的最短路径长度(若S=i则最短路径长度为0,若从点S无法到达点i,则最短路径长度为2147483647)

样例

输入
4 6 1
1 2 2
2 3 2
2 4 1
1 3 5
3 4 3
1 4 4

输出
0 2 4 3

题解

求单源最短路径,最经典的算法便是Dijkstra,本质上是一种贪心的思想。
整体思路是:按路径长度递增的次序产生最短路径的算法。
利用一个辅助数组D[],D[i]表示从起始点v0到点i的最短路径,通过不断更新这个数组,最后就可以得到一个点v0到各个点的最短路径。
该数组的初始状态为:若从v0到vi有弧,则D[i]为边长;否则将D[i]置为一个极大值(比如说int的最大值2147483647)。
有了这个数组后,我们可以从数组中找到一条最短的弧,意义为:从v0到周围中某一个距离最近的点。
之后我们要找下一条长度次短的最短路径,假设该次短路径的终点是vk,则这条路径为(v0,vk)或者(v0, vj, vk),vj即之前找到的最短路径相连的那个点。
这里有一条非常重要的定理:假设S为已求得最短路径的终点的集合,则下一条最短路径(设其终点为x)或者是弧(v0,x),或者是中间只经过S中的顶点而最后到达顶点x的路径。(具体证明可以用反证法证明,在此不多赘述)
因此,下一条长度次短的最短路径的长度必是
D[j] = Min{D[i] | vi 属于(V - S)}
其中Di或者是弧(v0,vi)上的权值,或者是D[k](k属于S)和弧(vk,vi)上的权值之和。
由此我们便可以得到Dijkstra的算法:
每次从辅助数组D[]中找到最短路径,并将对应的点标记为已经加入集合S,之后更新辅助数组D[]的值,利用公式:
D[i] = min {D[v] + 弧(v, i) , D[i]}
(i为当前遍历到的某个未被加入S的点,v为刚加入集合S的点)
重复上述操作,直至所有点都被加入S,此时的辅助数组D[]就是从v0出发到各个顶点的最短路径长度数组了。

代码

#include <stdio.h>
#define MAX 2147483647
#define MAX_POINT 10005
#define MAX_EDGE 500005

struct node{
    int v;
    int w;
    int next;
}edge[MAX_EDGE];

int pre[MAX_POINT];
int final[MAX_POINT],D[MAX_POINT];

int main(){
    int n,m,s,min,v,index;
    index = 1;
    scanf("%d %d %d", &n, &m, &s);
    for (int i = 1; i <= n; i++)
        pre[i] = -1;
    for (int i = 1; i <= m; i++){
        int f,g,w;
        scanf("%d %d %d", &f, &g, &w);
        if (f == g)
            continue;
        edge[index].v = g;
        edge[index].w = w;
        edge[index].next = pre[f];
        pre[f] = index;
        index++;
    }
    for (int i = 1; i <= n; i++){
        final[i] = 0;
        D[i] = MAX;
    }
    index = pre[s];
    while (edge[index].next != -1){
        if (edge[index].w < D[edge[index].v])
            D[edge[index].v] = edge[index].w;
        index = edge[index].next;
    }
    D[edge[index].v] = edge[index].w;
    D[s] = 0;
    final[s] = 1;
    v = s;
    for (int i = 1; i < n; i++){
        min = MAX;
        for (int j = 1; j <= n; j++){
            if (final[j] == 0){
                if (D[j] < min){
                    v = j;
                    min = D[j];
                }
            }
        }
        final[v] = 1;
        index = pre[v];
        if (index == -1)
            continue;
        while (edge[index].next != -1){
            if (final[edge[index].v] == 0){
                if ((min + edge[index].w) < D[edge[index].v]){
                    D[edge[index].v] = min + edge[index].w;
                }
            }
            index = edge[index].next;
        }
        if (final[edge[index].v] == 0){
            if ((min + edge[index].w) < D[edge[index].v]){
                D[edge[index].v] = min + edge[index].w;
            }
        }
    }
    for (int i = 1; i < n; i++)
        printf("%d ", D[i]);
    printf("%d", D[n]);
    return 0;
}

代码解释

数据结构:

struct node{
    int v;
    int w;
    int next;
}edge[MAX_EDGE];

int pre[MAX_POINT];

由于题目中N <= 10000,用邻接矩阵存的话会爆内存,因此选择用静态邻接表来存边。其中pre[]存储某个点的第一条边在edge数组中的位置,结构体node中,v代表有向边指向的节点,w代表边的权值,next代表同一个节点出发的另一条边的下标

int final[MAX_POINT],D[MAX_POINT];

Final用来记录节点是否被加入最短路径(即是否在集合S中)
D即辅助数组

初始化:

index = 1;
    scanf("%d %d %d", &n, &m, &s);
    for (int i = 1; i <= n; i++)
        pre[i] = -1; //初始化数组pre
    for (int i = 1; i <= m; i++){
        int f,g,w;
        scanf("%d %d %d", &f, &g, &w);
        if (f == g)
            continue; //如果有一条边,它的起点和终点都是同一个节点,则它只会增加路径的长度,因此我们可以直接不存储这条边
        edge[index].v = g;
        edge[index].w = w;
        edge[index].next = pre[f]; //将边存入邻接表
        pre[f] = index;
        index++;
    }
    for (int i = 1; i <= n; i++){
        final[i] = 0; //final数组初始化为0,表示所有边都未被放入集合S
        D[i] = MAX; //辅助数组的值初始化为一个极大值
    }
    index = pre[s]; //获得起始点出发的第一条边在edges中的位置
    while (edge[index].next != -1){
        if (edge[index].w < D[edge[index].v]) //找到从起始点出发,到周围各点直接相连的最短路径,作为辅助数组D的初始值
            D[edge[index].v] = edge[index].w;
        index = edge[index].next;
    }
    D[edge[index].v] = edge[index].w;
    D[s] = 0; //起始点到起始点的距离当然设为0了
    final[s] = 1; //起始点加入集合S
    v = s;

Dijkstra:

 for (int i = 1; i < n; i++){
        min = MAX;
        for (int j = 1; j <= n; j++){ //寻找辅助数组D中连接起始点到未被加入集合S的点的弧中长度最小的弧
            if (final[j] == 0){
                if (D[j] < min){
                    v = j;
                    min = D[j];
                }
            }
        }
        final[v] = 1; //将这条弧连接的点放入集合S
        index = pre[v]; //获得从这个点出发的第一条边在edges中的下标
        if (index == -1) //等于-1意味着没有以这个节点为起点的弧
            continue;
        while (edge[index].next != -1){ //更新辅助数组D中的值
            if (final[edge[index].v] == 0){
                if ((min + edge[index].w) < D[edge[index].v]){
                    D[edge[index].v] = min + edge[index].w;
                }
            }
            index = edge[index].next;
        }
        if (final[edge[index].v] == 0){
            if ((min + edge[index].w) < D[edge[index].v]){
                D[edge[index].v] = min + edge[index].w;
            }
        }
    }
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值