C语言:L2-001 紧急救援 (25 分) — Dijkstra

一、题目

作为一个城市的应急救援队伍的负责人,你有一张特殊的全国地图。在地图上显示有多个分散的城市和一些连接城市的快速道路。每个城市的救援队数量和每一条连接两个城市的快速道路长度都标在地图上。当其他城市有紧急求助电话给你的时候,你的任务是带领你的救援队尽快赶往事发地,同时,一路上召集尽可能多的救援队。

输入格式:

输入第一行给出4个正整数N、M、S、D,其中N(2≤N≤500)是城市的个数,顺便假设城市的编号为0 ~ (N−1);M是快速道路的条数;S是出发地的城市编号;D是目的地的城市编号。
第二行给出N个正整数,其中第i个数是第i个城市的救援队的数目,数字间以空格分隔。随后的M行中,每行给出一条快速道路的信息,分别是:城市1、城市2、快速道路的长度,中间用空格分开,数字均为整数且不超过500。输入保证救援可行且最优解唯一。

输出格式:

第一行输出最短路径的条数和能够召集的最多的救援队数量。第二行输出从S到D的路径中经过的城市编号。数字间以空格分隔,输出结尾不能有多余空格。

输入样例:

4 5 0 3
20 30 40 10
0 1 1
1 3 2
0 3 3
0 2 2
2 3 2

输出样例:

2 60
0 1 3

二、方法1

1、相关知识点

(1)图论

  • 是由若干顶点以及连接两点之间的边所构成的图形,这种图形通常用来描述某些事物之间的某种特定关系;
  • 表示事物,连接两点的线表示相应两个事物之间的关系;
  • 使用图来描述事物的关系时,对于连接两顶点的边,往往还需要一个数值来描述(比如:距离,耗费、时间等等),这个值称为权值
  • 带权值的图称为带权图

(2)迪杰斯特拉算法(Dijkstra):

旅游这一类问题一般被称为单源最短路径:即从一个顶点出发,计算从该顶点到其他所有顶点的最短路径长度,这里的长度即路上各边权值之和。
迪杰斯特拉算法就是为解决这类问题而生的。

①概念

Dijkstra算法是基于贪心思想,从起始位置出发,每次寻找与起点距离最近且未访问过的顶点,以该顶点作为中间节点,更新从起点到其他顶点的距离,直到全部顶点都作为了中间节点,并完成路径更新,算法结束。

②图解

以本题为例:
我们希望求出0号城市到3号城市的最短距离,0号城市即为单源最短路径中的单源,使用迪杰斯特拉算法,可以求出0号城市到其他城市的全部的最短路径。

Ⅰ 先做出图
在这里插入图片描述

Ⅱ 用橙色标记起始位置,蓝色标记未被访问的节点,绿色标记已经作为中间节点并完成了其他顶点的路径更新的节点。
在这里插入图片描述

城市a城市b距离
011
022
033

由于0号城市到1号城市的距离最短,所以首先将1号城市作为中间节点,更新0号城市到其他城市的距离。

Ⅲ 以1号城市作为中间节点
从1号城市可以到达3号城市,距离为2,所以从0号城市到达3号城市的距离更新为3(样例比较特殊,刚好不用更新),完成更新以后,1号城市就被标记为已访问了。

城市a城市b距离
12
132

在这里插入图片描述

Ⅳ 以2号城市为中间节点
从2号城市可以到达3号城市,距离为2;从0号城市到达3号城市的距离为4(不用更新),2号城市也被标记为已访问。

城市a城市b距离
232

在这里插入图片描述
Ⅳ 以3号城市为中间节点
由于3号城市为最后一个节点,路径更新就结束了。
同时我们得出了下表:

城市a城市b最短距离
011
022
033

即求出了0号城市到3号城市的最短距离。

③代码
// s:起始顶点
// n:储存图中顶点的个数
// visit数组:标记顶点是否已经作为中间节点完成相应的路径更新
// greph数组:领接矩阵graph
// INF:1e9 + 7;
void Dijkstra()
{
    dis[s] = 0;
	
    for (i = 0; i < n; i++)
    {
        dis[i] = graph[s][i];// 将dis数组初始化为最初图中的长度
    }
	visit[s] = 1;// 标记起始顶点
    // 更新最短路径的循环,循环进行n次
    for (i = 0; i < n; i++)
    {
        int min_dist = INF; // 存储从起点到其他未被访问节点中的最短距离
        int mid = -1; // 存储最短距离节点的编号

        // 遍历n个顶点,寻找当前未被访问的顶点中的距离起始顶点最短距离的节点编号
        for (j = 0; j < n; j++)
        {
            // 如果visit[j] == 0,说明顶点j未被访问,且min_dist > dis[j]时
            if (visit[j] == 0 && min_dist > dis[j])
            {
                min_dist = dis[j]; // 更新最短距离
                mid = j; // 更新顶点编号mid为j
            }
        }
        // 以mid为中间节点,再循环遍历其他所有节点
        for (j = 0; j < n; j++)
        {
            // 如果当前遍历的节点j未曾作为过中间节点
            // 并且从起始节点到j的距离dis[j]大于从起始节点到mid与从mid到j的距离之和
            if (visit[j] == 0 && dis[j] > dis[mid] + graph[mid][j])
            {
                // 更新起始节点到j的距离dis[j],更新为起始到mid与mid到j的距离之和
                dis[j] = dis[mid] + graph[mid][j];
            }
        }
        visit[mid] = 1;
    }
}

2、思路

(1)输入

需要注意将graph先进行一次初始化,将下标不相同的值置为INF,相同则置为0;然后再进行输入领接矩阵。

(2)Dijkstra

本题需要进行一定改动,来达成题意,我们需要进行一些改动:
在“以mid为中间节点,再循环遍历其他所有节点“”时,判断 dis[j]dis[mid] + graph[mid][j] 的大小关系,如果前者大于后者,进行值的变换,如果两者相等,我们就需要判断救援队的数目。

(3)输出

用一个数组way来存放前驱节点,以类似链表的方式,用递归来输出经过的城市。

3、代码

#include<stdio.h>
void Dijkstra(); // 迪杰斯特拉算法
void Print(int t); // 递归打印函数

#define max 505
#define MAX 100000
const int INF = 1e9 + 7;
int n, m, s, d; // 题目输入
int a, b, c; // 题目输入
int i, j; // 遍历
int people[max]; // 记录城市的救援队的数目
int graph[max][max]; // 领接矩阵graph
int dis[max]; // 存储从起始节点至其他节点的最短路径的距离
int visit[max]; // 标记顶点是否已经作为中间节点完成相应的路径更新
int num[MAX]; // 存储从起始节点至其他节点召集的消防员最大总数
int cnt[MAX]; // 存储从起始节点至其他节点的最短路径数目
int way[MAX]; // 存储前驱节点

int main()
{
    // 输入
    scanf("%d %d %d %d", &n, &m, &s, &d);
    for (i = 0; i < n; i++)
    {
        scanf("%d", &people[i]);
    }
    for (i = 0; i < n; i++)
    {
        for (j = 0; j < n; j++)
        {
            if (i != j)
                graph[i][j] = INF;
            else
                graph[i][j] = 0;
        }
    }
    for (i = 0; i < m; i++)
    {
        scanf("%d %d %d", &a, &b, &c);
        // 将距离存入领接矩阵graph
        graph[a][b] = c;
        graph[b][a] = c;
    }

    // 迪杰斯特拉算法
    Dijkstra();
    
    printf("%d %d\n", cnt[d], num[d]);
    Print(d);
    return 0;
}
void Dijkstra()
{
    dis[s] = 0;
    num[s] = people[s];
    cnt[s] = 1; // 标记初始节点
    way[s] = s;

    for (i = 0; i < n; i++)
    {
        dis[i] = graph[s][i];// 将dis数组初始化为最初图中的长度
    }

    // 更新最短路径的循环,循环进行n次
    for (i = 0; i < n; i++)
    {
        int min_dist = INF; // 存储从起点到其他未被访问节点中的最短距离
        int mid = -1; // 存储最短距离节点的编号

        // 遍历n个顶点,寻找当前未被访问的顶点中的距离起始顶点最短距离的节点编号
        for (j = 0; j < n; j++)
        {
            // 如果visit[j] == 0,说明顶点j未被访问,且min_dist > dis[j]时
            if (visit[j] == 0 && min_dist > dis[j])
            {
                min_dist = dis[j]; // 更新最短距离
                mid = j; // 更新顶点编号mid为j
            }
        }
        if (mid == -1)
            break;
        visit[mid] = 1;
        // 以mid为中间节点,再循环遍历其他所有节点
        for (j = 0; j < n; j++)
        {
            // 如果当前遍历的节点j未曾作为过中间节点
            // 并且从起始节点到j的距离dis[j]大于从起始节点到mid与从mid到j的距离之和
            if (visit[j] == 0 && dis[j] > dis[mid] + graph[mid][j])
            {
                // 更新起始节点到j的距离dis[j],更新为起始到mid与mid到j的距离之和
                dis[j] = dis[mid] + graph[mid][j];
                // 节点mid到节点j存在一条更短的路径,
                cnt[j] = cnt[mid];
                // 更新节点j救援队的数目
                num[j] = num[mid] + people[j];
                // 记录节点mid
                way[j] = mid;
            }
            // 如果当前遍历的节点j未曾作为过中间节点
            // 并且从起始节点到j的距离dis[j]等于从起始节点到mid与从mid到j的距离之和
            else if (visit[j] == 0 && dis[j] == dis[mid] + graph[mid][j])
            {
                // 到j的最短路径数目需加上到min的最短路径数目
                cnt[j] += cnt[mid];
                if (num[j] < num[mid] + people[j])
                {
                    // 更新节点j救援队的数目
                    num[j] = num[mid] + people[j];
                    // 记录节点mid
                    way[j] = mid;
                }
            }
        }
    }
}
void Print(int t)
{
    if (t == s)
    {
        printf("%d", s);
        return;
    }
    Print(way[t]);
    printf(" %d", t);
}
  • 22
    点赞
  • 49
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

WE-ubytt

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值