洛谷P3953 [NOIP 2017 提高组] 逛公园
洛谷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 1→5,1→2→4→5,1→2→3→5 为 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 1≤P≤109, 1 ≤ a i , b i ≤ N 1 \le a_i,b_i \le N 1≤ai,bi≤N, 0 ≤ c i ≤ 1000 0 \le c_i \le 1000 0≤ci≤1000。
数据保证:至少存在一条合法的路线。
- 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(否则可能计算不完全),采用记忆化深搜求解。具体步骤如下:
- 定义:记忆化数组 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)同理
- 边界条件: f n , 0 = 1 f_{n,0}=1 fn,0=1
- 状态转移:令 d = q − ( d i s u + w i − d i s v ) d=q-(dis_{u}+w_{i}-dis_{v}) d=q−(disu+wi−disv)即消耗了当前的 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,将其赋值为0for(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+wi−disu),因为原来是 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,将其赋值为0for(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与hmemset(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;
}