洛谷 P3953 逛公园

该博客讨论了洛谷P3953题目,涉及逛公园的路线选择问题。题目描述了一个有向图,策策希望每天的路线不重复且时间不超过最短路径的d+k。博客分析了解题思路,指出问题关键在于dp和记忆化搜索,并提供了反向跑图的优化方法,最后给出了状态转移方程和代码实现的概述。

原题链接

题目描述

策策同学特别喜欢逛公园。公园可以看成一张 NN 个点 MM 条边构成的有向图,且没有 自环和重边。其中 11 号点是公园的入口,NN 号点是公园的出口,每条边有一个非负权值, 代表策策经过这条边所要花的时间。

策策每天都会去逛公园,他总是从 11 号点进去,从 NN 号点出来。

策策喜欢新鲜的事物,它不希望有两天逛公园的路线完全一样,同时策策还是一个 特别热爱学习的好孩子,它不希望每天在逛公园这件事上花费太多的时间。如果 11 号点 到 NN 号点的最短路长为 dd,那么策策只会喜欢长度不超过 d + Kd+K 的路线。

策策同学想知道总共有多少条满足条件的路线,你能帮帮它吗?

为避免输出过大,答案对 PP 取模。

如果有无穷多条合法的路线,请输出 -1−1。

输入格式

第一行包含一个整数 TT, 代表数据组数。

接下来 TT 组数据,对于每组数据: 第一行包含四个整数 N,M,K,PN,M,K,P,每两个整数之间用一个空格隔开。

接下来 MM 行,每行三个整数 a_i,b_i,c_iai​,bi​,ci​,代表编号为 a_i,b_iai​,bi​ 的点之间有一条权值为 c_ici​ 的有向边,每两个整数之间用一个空格隔开。

输出格式

输出文件包含 TT 行,每行一个整数代表答案。

解题思路

        虽然这题看上去是最短路径(因为题目说了),但实际上于最短路径关系不大(因为只求一次单源就足够了),核心在于 DP。

        首先分析已知变量, n、m、k 是有用变量 (p:是我高攀了),对于 n、m 由于其数据范围过大,不适宜用 DP ,而 k≤50 的限制,使得 k 成为了这道题目的关键(对于时间复杂度而言)。

        直觉说:这题是记忆化搜索(题目标签上有)。对于 0−k 中的每一个数,进行一次记忆化搜索。

        为了进一步优化时间复杂度,记忆化搜索时可以采用反向跑图的方法。因为反向跑图可以避免走那些到不了 n 的边。

        再定义一些变量,分别记录单源最短路 dis[i] 、判 00 环 vis[i][j] 、记录方案数 f[i][j] ……

        讲一下状态转移,设一个数组 f[i][j] 记录从 n 到 i 的距离 ≤ 最短路径 + k 的方案数,其中 j 表示每次搜索剩余的 k 的大小,对于每一条边都尝试走(假设这个状态为 u−>v ,其转移方程就是: dis[u]−dis[v]+k−w[u−>v] ,其实中 w[u][v] 表示从 u−>vu−>v 的距离花费)

        好像就做完了……

        注:本体采用 vectorvector 存图,原文中会有各种函数的说明。

        (详细说明见代码 OvOOvO)

【code】

#include<iostream>
#include<cstdio>
#include<cmath>
#include<algorithm>
#include<map>
#include<vector>//用vector必须先有该头文件
#include<string>
#include<cstring>
#include<queue>
using namespace std;
inline int read()
{
    int num=0,w=1;char ch=getchar();
    while(ch<'0'||ch>'9') {if(ch=='-') w=-1;ch=getchar();}
    while(ch>='0'&&ch<='9') {num=(num<<1)+(num<<3)+ch-'0';ch=getchar();}
    return num*w;
}
int T;
int n,m,k,p,ans;
int u,v,w,qwq;
int dis[100001];//记录单源最短路
bool vis[100001][101];//用于判0环
int f[100001][101];//记录最大方案数
struct node
{
    int to;
    int dis;
};
vector<node> head[100001];//正向存图
vector<node> h[100001];//反向存图
queue<int> q;
inline void intt()//初始化
{
    for(register int i=1;i<=n;i++)
    {
        head[i].clear();//.clear() 用于清空vector
        h[i].clear();//对于每一个head[i]、h[i]都要清空
    }
    return;
}
inline void spfa()//spfa模板,用于求单源最短路
{
    memset(dis,0x3f,sizeof(dis));
   q.push(1);
    dis[1]=0;
   while (!q.empty()) 
    {
          int x=q.front();
          q.pop();
          int len=head[x].size();//.size()求动态数组内存储的数的个数
       for(int i=0;i<len;i++) 
          {
           if(dis[head[x][i].to]>dis[x]+head[x][i].dis) 
                {
                dis[head[x][i].to]=dis[x]+head[x][i].dis;
                q.push(head[x][i].to);
           }
        }
    }
    return;
}
//接下来就是重头戏——DP了
inline int dp(int now,int gs)//now表示到那个点了,gs表示当前k还剩余多少
{
    int sum=0;
    if(gs<0||gs>k) return 0;//如果出现
    if(vis[now][gs])//如果出现0环的现象,则直接结束本次搜索
    {
        vis[now][gs]=false;
        return -1;
    }
    if(f[now][gs]!=-1) return f[now][gs];//同其他记忆化搜索一样,搜过的边没有必要再搜一次
    vis[now][gs]=true;
    int len=h[now].size();
    for(register int i=0;i<len;i++)
    {
        node to=h[now][i];
        int dep=dp(to.to,dis[now]+gs-dis[to.to]-to.dis);
      //尝试当前这条边替换其中最短路径里的一条边,以求得合法解
        if(dep==-1)
        {
            vis[now][gs]=false;
            return -1;
        }
        sum=(sum+dep)%p;
    }    
    vis[now][gs]=false;
    if(now==1&&gs==0) sum++;//很明显,当now=1,gs=0时,是一个合法解
    f[now][gs]=sum;//记忆化一下
    return sum;
}
inline void work()
{
    scanf("%d%d%d%d",&n,&m,&k,&p);
    intt();//初始化
    for(register int i=1;i<=m;i++)
    {
        scanf("%d%d%d",&u,&v,&w);
        head[u].push_back((node){v,w});//正向存图
        h[v].push_back((node){u,w});//反向存图
      //.push_back()在该动态数组后开辟新空间,并将数据存入
    }
    spfa();//求最短路
    memset(f,-1,sizeof(f));//要初始化为-1,因为有边权为0的时候
    ans=0;
    for(register int i=0;i<=k;i++)//对于0-k的每个数都要搜索
    {
        qwq=dp(n,i);
        if(qwq==-1) {printf("-1\n");return;}//是否出现0环的现象
        ans=(ans+qwq)%p;//不要忘记对p取模
    }
    printf("%d\n",ans);
    return;
}
int main()//精简的主函数 QwQ
{
    //freopen(".in","r",stdin);
    //freopen(".out","w",stdout);
    scanf("%d",&T);
    while(T--) work(); 
    //fclose(stdin);
    //fclose(stdout);        
    return 0;
}

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值