洛谷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;
}