洛谷P3953 [NOIP 2017 提高组] 逛公园

发布于:2025-06-22 ⋅ 阅读:(16) ⋅ 点赞:(0)

洛谷P3953 [NOIP 2017 提高组] 逛公园

洛谷题目传送门

题目背景

NOIP2017 D1T3

题目描述

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

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

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

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

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

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

输入格式

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

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

接下来 M M M 行,每行三个整数 a i , b i , c i a_i,b_i,c_i ai,bi,ci,代表编号为 a i , b i a_i,b_i ai,bi 的点之间有一条权值为 c i c_i ci 的有向边,每两个整数之间用一个空格隔开。

输出格式

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

输入输出样例 #1

输入 #1

2
5 7 2 10
1 2 1
2 4 0
4 5 2
2 3 2
3 4 1
3 5 2
1 5 3
2 2 0 10
1 2 0
2 1 0

输出 #1

3
-1

说明/提示

【样例解释1】

对于第一组数据,最短路为 3 3 3 1 → 5 , 1 → 2 → 4 → 5 , 1 → 2 → 3 → 5 1\to 5, 1\to 2\to 4\to 5, 1\to 2\to 3\to 5 15,1245,1235 3 3 3 条合法路径。

【测试数据与约定】

对于不同的测试点,我们约定各种参数的规模不会超过如下

测试点编号 T T T N N N M M M K K K 是否有 0 0 0
1 1 1 5 5 5 5 5 5 10 10 10 0 0 0
2 2 2 5 5 5 10 3 10^3 103 2 × 10 3 2\times 10^3 2×103 0 0 0
3 3 3 5 5 5 10 3 10^3 103 2 × 10 3 2\times 10^3 2×103 50 50 50
4 4 4 5 5 5 10 3 10^3 103 2 × 10 3 2\times 10^3 2×103 50 50 50
5 5 5 5 5 5 10 3 10^3 103 2 × 10 3 2\times 10^3 2×103 50 50 50
6 6 6 5 5 5 10 3 10^3 103 2 × 10 3 2\times 10^3 2×103 50 50 50
7 7 7 5 5 5 10 5 10^5 105 2 × 10 5 2\times 10^5 2×105 0 0 0
8 8 8 3 3 3 10 5 10^5 105 2 × 10 5 2\times 10^5 2×105 50 50 50
9 9 9 3 3 3 10 5 10^5 105 2 × 10 5 2\times 10^5 2×105 50 50 50
10 10 10 3 3 3 10 5 10^5 105 2 × 10 5 2\times 10^5 2×105 50 50 50

对于 100 % 100\% 100% 的数据, 1 ≤ P ≤ 10 9 1 \le P \le 10^9 1P109 1 ≤ a i , b i ≤ N 1 \le a_i,b_i \le N 1ai,biN 0 ≤ c i ≤ 1000 0 \le c_i \le 1000 0ci1000

数据保证:至少存在一条合法的路线。


  • 2019.8.30 增加了一组 hack 数据 by @skicean
  • 2022.7.21 增加了一组 hack 数据 by @djwj233

思路详解

最短路

首先由于它要求最短路径,由于害怕被卡,所以保险起见我们采用dijkstra求解最短路。

void dij(ll o){//dijkstra标准模板,o为起点
	memset(dis,0x3f,sizeof(dis));//记得赋初值!!!
	memset(vis,0,sizeof(vis));
	queue<ll>q;q.push(o);vis[o]=1;dis[o]=0;
	while(!q.empty()){
		ll u=q.front();q.pop();vis[u]=0;
		for(ll i=h[u];i;i=ne[i]){
			ll v=to[i];
			if(dis[v]>dis[u]+w[i]){
				dis[v]=dis[u]+w[i];
				if(!vis[v]){
					vis[v]=1;q.push(v);
				}
			}
		}
	}
}

深搜求解

我们最多有 k k k的相差,考虑依次枚举实际相差的 q q q(否则可能计算不完全),采用记忆化深搜求解。具体步骤如下:

  1. 定义:记忆化数组 f u , j f_{u,j} fu,j为到 u u u点, q q q剩下 j j j的方案数。 d f s ( u , q ) dfs(u,q) dfs(u,q)同理
  2. 边界条件: f n , 0 = 1 f_{n,0}=1 fn,0=1
  3. 状态转移:令 d = q − ( d i s u + w i − d i s v ) d=q-(dis_{u}+w_{i}-dis_{v}) d=q(disu+widisv)即消耗了当前的 q q q后的值,若 d < 0 d<0 d<0则直接退出。
    否则累加答案: f u , j + = d f s ( v , d ) f_{u,j}+=dfs(v,d) fu,j+=dfs(v,d)

但是,我们发现,题目中说可能有无数解的情况,肯定是有0环,那如何判断是否有0环呢?,考虑定于数组 v i u , q vi_{u,q} viu,q,表示是否访问过状态 u , q {u,q} u,q,若在深搜访问状态 u , q {u,q} u,q v i u , q = = 1 vi_{u,q}==1 viu,q==1,那说明跑了一圈跑回来了,那一定有0环,标记并退出。

**int f[N][56],fl=0,vi[N][56];
int dfs(int u,int q){
	if(vi[u][q]){//已经访问过这种状态,有0环
		fl=1;return 0;
	}
	if(f[u][q]!=-1)return f[u][q];//记忆化深搜
	vi[u][q]=1;//标记
	f[u][q]=0;//由于f数组初始值为-1,将其赋值为0
	for(int i=h[u];i;i=ne[i]){
		int v=to[i];
		int d=q-(dis[u]+w[i]-dis[v]);//消耗后的状态
		if(d<0)continue;
		f[u][q]=(f[u][q]+dfs(v,d))%p;//求和,记得取模
		if(fl==1)return 0;//dfs(v,d)中fl变成了1也要退出
	}
	if(u==n&&q==0)f[u][q]=1;//边界
	vi[u][q]=0;//回溯
	return f[u][q];
}**

反向跑图

我们将代码提交上去,发现有些数据是错的。而且错误数据都是错误输出-1,即0环判断错误。这是为何?我们发现,如果一旦有0环我们的深搜便会返回-1,但其实有可能根本不会经过这个0环,那如何规避这个错误呢?我们直接反向跑图,这样乱跑的情况就会被规避掉。但要注意,状态转移变为了: d = q − ( d i s v + w i − d i s u ) d=q-(dis_{v}+w_{i}-dis_{u}) d=q(disv+widisu),因为原来是 v − > u v->u v>u,现在变为了 u − > v u->v u>v

完整代码

#include<bits/stdc++.h>
using namespace std;
#define ll long long
const ll N=2e5+4;
ll n,m,t,k,p;
ll h[N],to[N],w[N],ne[N],tot;
void add(ll u,ll v,ll d){
	tot++;to[tot]=v;w[tot]=d;ne[tot]=h[u];h[u]=tot;
}
struct edge{
	ll x,y,z;
}a[N];
ll dis[N],vis[N];
void dij(ll o){//dijkstra标准模板,o为起点
	memset(dis,0x3f,sizeof(dis));//记得赋初值!!!
	memset(vis,0,sizeof(vis));
	queue<ll>q;q.push(o);vis[o]=1;dis[o]=0;
	while(!q.empty()){
		ll u=q.front();q.pop();vis[u]=0;
		for(ll i=h[u];i;i=ne[i]){
			ll v=to[i];
			if(dis[v]>dis[u]+w[i]){
				dis[v]=dis[u]+w[i];
				if(!vis[v]){
					vis[v]=1;q.push(v);
				}
			}
		}
	}
}
ll ans;
ll f[N][56],fl=0,vi[N][56];
ll dfs(ll u,ll q){
	if(vi[u][q]){//已经访问过这种状态,有0环
		fl=1;return 0;
	}
	if(f[u][q]!=-1)return f[u][q];//记忆化深搜
	vi[u][q]=1;//标记
	f[u][q]=0;//由于f数组初始值为-1,将其赋值为0
	for(ll i=h[u];i;i=ne[i]){
		ll v=to[i];
		ll d=q-(dis[v]+w[i]-dis[u]);//消耗后的状态
		if(d<0)continue;
		f[u][q]=(f[u][q]+dfs(v,d))%p;//求和,记得取模
		if(fl==1)return 0;//dfs(v,d)中fl变成了1也要退出
	}
	if(u==1&&q==0)f[u][q]=1;//边界
	vi[u][q]=0;//回溯
	return f[u][q];
}
int main(){
	ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
	cin>>t;
	for(ll cas=1;cas<=t;cas++){
		cin>>n>>m>>k>>p;fl=0;
		tot=0;//链式前向星清空只需要清空tot与h
		memset(h,0,sizeof(h));
		for(ll i=1;i<=m;i++){
			ll x,y,z;cin>>x>>y>>z;
			a[i]={x,y,z};//将边保存下来
		}
		for(ll i=1;i<=m;i++){
			ll x=a[i].x,y=a[i].y,z=a[i].z;
			add(x,y,z);//正向建边
		}
		dij(1);//正向跑最短路
		ans=0;
		tot=0;//清空
		memset(h,0,sizeof(h));
		for(ll i=1;i<=m;i++){//方向见图
			ll x=a[i].x,y=a[i].y,z=a[i].z;
			add(y,x,z);
		}
		memset(f,-1,sizeof(f));
		memset(vi,0,sizeof(vi));
		for(ll i=0;i<=k;i++){
			ans=(ans+dfs(n,i))%p;
			if(fl)break;
		}
		if(fl)cout<<-1<<'\n';
		else cout<<ans<<'\n';
	}
	return 0;
}

网站公告

今日签到

点亮在社区的每一天
去签到