L2-001 紧急救援

        实践大于理论,孰能生巧。本道题是对多个任务同时松弛。本质来说还是最短路径。


        寒假回来后的第一道题,对算法的熟练度太低,不看之前写的代码,已经记不清算法内容了。这道题看完题目就想到了最短路径,但最短路径选哪一种,和它们之间的区别已经忘完了。

        相比较我之前写的最短路径只需要得到最短路径的长度即可,没有考虑如何最短路径经过的顶点,没有考虑多条最短路径怎么办这些问题。而这道题就考了这些内容,我在借鉴了大佬的写法和AI之后,总结自己的问题。

思路

        我现在写代码一般就是分为三个模块:输入、输出和算法主体(再细就没尝试了)

输入部分:

这里创建的变量为:

n:城市的个数

m:快速道路的条数

s:出发地的城市编号

d:目的地的城市编号

init_num:初始每个城市救援队数目

matrices:邻接矩阵

第一行、第二行和第3~(3+M)行。 input函数来包括:

vector<vector<int>> input(int &n,int &m,int &s,int &d,vector<int> &init_num)
{
    scanf("%d%d%d%d",&n,&m,&s,&d);//第一行
    vector<vector<int>> matrices(n,vector<int>(n,-1));
    for (int i = 0,flag=0; i < n; i++)//第二行
    {
        scanf("%d",&flag);
        init_num.push_back(flag);
        matrices[i][i]=0;//邻接矩阵
    }
    for (int i = 0,x,y,flag; i < m; i++)//第3到M-3行
    {
        scanf("%d%d%d",&x,&y,&flag);
        matrices[x][y]=flag;//邻接矩阵
        matrices[y][x]=flag;//邻接矩阵
    }
        return matrices;
}

        这一部分没什么好讲的,主要是如何做到模块化,条理清楚,我尽量减少全局变量,养成保证数据安全的习惯。

算法部分:

变量:

    int* book=new int[n] ();//标记已经松驰过的顶点

    int* dis=new int[n] ();//记录最短路径

    int* team_number=new int[n] ();//对应每一个dis的成员总人数。

    int* pre=new int[n] ();//记录每一个节点的前驱节点。

    int* num=new int[n] ();//记录每个顶点最短路径个数

        首先,肯定能看出来是用最短路径,我使用Dijkstra算法,通过需要输出的内容分析,需要得到最短路径的条数、最短路径上最多的救援人数,以及最短路径上的顶点;过一遍例题也会发现,出现了路径相同,救援人数不同的情况。相比较我之前只能求最短路径的长度,这一道题难度有所上升。

        Dijkstra不再多讲,先来看如何求最短路径个数,我们通过num数组来更新顶点s到其它任意顶点的最短路径条数(与dis一样),通过动态规划调整,与dis的松弛同步进行。设num[s]=1,其余为0;不同级关系更新,同级关系相加(松弛)。

if (dis[i]>dis[min_u]+matrices[min_u]:i]) -> num[i]=num[min_u];//更新

if (dis[i]==dis[min_u]+matrices[min_u][i]) -> num[i]+=num[min_u];//相加

        每找到一条与前驱节点相连的最短路径,便把该节点的最短路径条数更新为前驱节点的条数。在找到相同长度的路径,便把该节点的最短路径更新为本身与该前驱节点相加。

        我们知道,通过book标记已经 每一轮的最短路径,避免了源点到两个点距离一样的情况发生错误,我们现在面临的问题是源点到终点,有两条长度一样的路径。那么我们如何解决呢?

        答案是再只有最短路径长度单关键字问题中是没办法解决的,因为它们的dis一模一样。所以这道题中出现了第二关键字救援队人数。在最短路径长度相同的情况下,我们选择多的救援队数量的那条路。

        如何得到最终的路径的救援队人数呢?第一个可能想到的是松弛,但是每条最短路径的救援队人数是唯一,不会重复,所以我们只需要更新,不需要相加就行。

if (dis[i]>dis[min_u]+matrices[min_u][i])
{
    team_number[i]=team_number[min_u]+init_num[i];
}

if (dis[i]==dis[min_u]+matrices[min_u][i])
{
    team_number[i]=team_number[min_u]+init_num[i];
}

        最后要得到的是最短路径的顶点,这个也是我认为最不同的部分:

 题目中已经规定输入保证救援可行且最优解唯一,所以有且仅有一条路径是长度最短,救援队人数最多的。这里我还想了一下用Floyd-Warshall算法求多源最短路径,然后再通过将顶点之间路径相加来求s到d的最短路径,但很快就被我否决了,因为局部最优!=全局最优。

        不过每一轮中dis数组中最小的顶点不一定在最短路径中,但是最短路径中的顶点一定在is数组中最小的顶点里。我们在有终点和起点的情况下, 可以通过寻炸每个点的前驱节点和后继节点来进行回溯,那么我们如何选择呢?

        相比较下,寻找前驱节点更方便:在算法执行过程中,确定后继节点并不直观。Dijkstra算法为例,它基于贪心策略,每次选择距离源点最近的未确定节点进行扩展,在这个过程中,一个节点可能会被多个节点更新距离,很难直接确定哪个节点是它的后继节点。如果要记录后继节点,就需要额外复杂的逻辑判断,在每次更新距离时,都要比较新的路径和原路径,来确定是否更新后继节点信息。

        当通过某个节点 u 更新另一个节点 v 的距离时,很自然地就能将 u 记录为 v 的前驱节点。后续回溯路径时,从终点开始,根据记录的前驱节点不断向前追溯,直至起点,就能得到完整路径。这种方式逻辑清晰,代码实现简单。

输出部分:

         输出部分不再赘述,我是直接在算法函数的末尾便加了输出函数:

void output(int num,int team_number,int d,int *pre)
{
    printf("%d %d\n",num,team_number);
    int flag=d;
    vector<int> path;
    while(1)
    {
        path.push_back(flag);
        if(flag == pre[flag])//到起点了
            break;
        flag=pre[flag]; 
    }
    for (int i = path.size()-1; i > 0; i--)
        printf("%d ",path[i]);
    printf("%d\n",path[0]);
}

注意(含测试点猜测)

  •         起终点,注意起终点是s和d,不是0和n-1。
  •         new创建数组,memset赋值造成运行时报错。

                原因:用new进行创建数组时,使用()没使用[]。(如error: int* vertex=new int(n); )

  • 测试点2为 与人数相关,会出现路径相同,人数不同的情况。  
// if (team_number[i]<city_road[i]+team_number[min_u])
// {
    pre[i]=min_u;
    team_number[i]=team_number[min_u]+city_road[i];
// }
    num[i]+=num[min_u];
  • 测试点4与最短路径条数有关。最短路径中多条最短路径的完整计数。

 路径回溯逻辑错误

error代码:

        if(dis[d]!=Last_road||team_number[d]!=Last_number)
        vertex[num++]=min_u;

  考虑情况不全:

  1. 新路径大于原路径:
  2. 新路径等于原路径,但救援队人数小于原路径
  3. 新路径等于原路径,但救援队人数大于原路径

        少考虑情况二。 (error代码)

else if (start_end_city[min_u][i]!=-1&&dis[i]==dis[min_u]+start_end_city[min_u][i]&&team_number[i]<team_number[i]+team_number[min_u]&&min_u!=i)//距离相同比较,消防人员人数。
 {
    pre[i]=min_u;
    team_number[i]=team_number[min_u]+city_road[i];
    num[i]+=num[min_u];
}

无向图邻接矩阵问题

        测试点3与无向图邻接矩阵的无向性有关。有意思的是,在我第一套代码中,消防人数的计算和没有考虑无向图邻接矩阵,居然过了第三个测试点,我猜测属于负负得正了。

    for (int i = 0,x,y,flag; i < m; i++)//第3到M-3行
    {
        scanf("%d%d%d",&x,&y,&flag);
        matrices[x][y]=flag;//邻接矩阵
        matrices[y][x]=flag;//邻接矩阵
    }

消防成员人数的计算

        多次相加,加的方法不对。

team_number[i]+=city_road[min_u];//error
vertex[num++]=min_u;//error

最短路径上顶点计算错误

        我之前的方法是在进行松弛时每一轮的最短路径当作最终路径上的顶点,但正如我在最短路径(Floyd-Warshall、Dijkstra、Bellman-Ford)-CSDN博客所说,这不是一定的。这种方法考虑不全(如图所示,详细点击博客):

        这个时候我是通过了第一个测试点十二分, 所以我猜测测试点1保证了每次松弛的点都在最短路径上。


《啊哈!算法》最短路径优化 

        《啊哈!算法》中的思路很好理解,但并不简洁。dis数组可以直接从dis[0]=0,其余为无穷大开始。

还有一些问题已经记不清,暂时这个样子吧,如果大家遇到其它问题,可以给我留言一起讨论。

完整代码

#include <bits/stdc++.h>
using namespace std;
#define inf 0x3f3f3f3f/*无穷大*/

vector<vector<int>> input(int &n,int &m,int &s,int &d,vector<int> &init_num)
{
    scanf("%d%d%d%d",&n,&m,&s,&d);//第一行
    vector<vector<int>> matrices(n,vector<int>(n,-1));
    for (int i = 0,flag=0; i < n; i++)//第二行
    {
        scanf("%d",&flag);
        init_num.push_back(flag);
        matrices[i][i]=0;//邻接矩阵
    }
    for (int i = 0,x,y,flag; i < m; i++)//第3到M-3行
    {
        scanf("%d%d%d",&x,&y,&flag);
        matrices[x][y]=flag;//邻接矩阵
        matrices[y][x]=flag;//邻接矩阵
    }
        return matrices;
}

void output(int num,int team_number,int d,int *pre)
{
    printf("%d %d\n",num,team_number);
    int flag=d;
    vector<int> path;
    while(1)
    {
        path.push_back(flag);
        if(flag == pre[flag])//到起点了
            break;
        flag=pre[flag]; 
    }
    for (int i = path.size()-1; i > 0; i--)
        printf("%d ",path[i]);
    printf("%d\n",path[0]);
}

void  Dijkstra(int n,int s,int d,vector<int> &init_num,vector<vector<int>> &matrices)
{
    
    int* book=new int[n] ();
    int* dis=new int[n] ();
    memset(dis,inf,sizeof(int)*n),dis[s]=0;
    int* team_number=new int[n] ();//对应每一个dis的成员总人数。
    int* pre=new int[n] ();//记录每一个节点的前驱节点。
    int* num=new int[n] ();//记录每个顶点最短路径个数
    num[s]=1;
    for (int i = 0; i < n; i++)
        team_number[i]=init_num[i];
    for (int j = 1,min,min_u; j <= n-1 ; j++)
    {
        min=inf;
        min_u=0;
        for (int i = 0; i < n; i++)/*选出未更新中最小的*/
        {
            if (book[i]==0&&dis[i]<min)//如果有且仅有一个最短路径
            {
                min=dis[i];
                min_u=i;       
            }
        }
        book[min_u]=1;//标记本轮最短的。
        for (int i = 0; i < n; i++)/*松弛*/
        {
            if(book[i]!=1&&matrices[min_u][i]!=-1)
            {
                if (dis[i]>dis[min_u]+matrices[min_u][i])
                {
                    pre[i]=min_u;
                    dis[i]=dis[min_u]+matrices[min_u][i];
                    team_number[i]=team_number[min_u]+init_num[i];
                    num[i]=num[min_u];//最短路径个数
                }
                else if (dis[i]==dis[min_u]+matrices[min_u][i])//距离相同比较,消防人员人数。
                {
                    if (team_number[i]<init_num[i]+team_number[min_u])
                    {
                    pre[i]=min_u;
                    team_number[i]=team_number[min_u]+init_num[i];
                    }
                    num[i]+=num[min_u];
                }
            }
        } 
    }
    output(num[d],team_number[d],d,pre);//输出
}

int main()
{
    int n=0,m=0,s=0,d=0;
    vector<int> init_num;
    vector<vector<int>> matrices=input(n,m,s,d,init_num);
    Dijkstra(n,s,d,init_num,matrices);
    return 0;
}

 参考文献

L2-001 紧急救援 (25 分)(最短路)-CSDN博客

PTA题解 L2-001 紧急救援(C++)-CSDN博客

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值