1. 程式人生 > >NOIP2017逛公園 記憶化搜尋+判環+最短路

NOIP2017逛公園 記憶化搜尋+判環+最短路

主要思路:

由於這道題K的數值範圍較小,故可以直接列舉K來求答案。答案就是起點到終點的為d,d+1,d+2,...,d+K的路徑的條數總合。

           聯想到之前一些在路徑上有消耗的圖論題,我們可以把這個K也當做是一個被消耗的東西,就像錢一樣(以下就當錢講好了)。故我們可以定義dp[i][j]為從i這個點出發走比他到終點的最短距離大j的路徑有多少條。接下來就是狀態轉移的問題了 。            記你的當前所在的點X到終點的最短距離為Dis1,現在你要走到點Y,他到終點的最短距離為Dis2,X到Y的邊長為D。那麼你從X走Y到終點的最短距離為Dis2+D,而你走點X到終點的最短路徑(不用管有沒有經過Y)為Dis1,故消耗的錢就是Dis2+D-Dis1,即為cost。故dp[X][j]=∑dp[Y][j-cost](若錢不夠是不能走的,但是錢剛好花完是可以走的,也就是不要欠錢就可以了一直走下去)終止狀態就是終點且剩餘錢數為0,這樣只有一種方案。(到達終點時若策策還有錢,就讓策策去把錢正好花完,花不完是不能計數的)。            那麼就能發現如果能走到一個0環上且手上的錢不為負數,那麼他可以沿著0環隨便走多少步再到終點(0環上的點到終點的最短距離都是一樣的),那麼這樣就得輸出-1,但是前提條件是他能用手上的錢走到0環。            判0環就讓記錄在這條路上你上次到達這個點還剩多少錢,若與這個點一樣就說明這個點在0環上。(可能有些人會問,從0環走出來沒辦法正好把錢花完走向終點怎麼辦,出門少帶錢就可以了(如果你沒有和我一樣的疑問可以忽略)) 每個點到終點的最短距離就建一個方向圖跑最短路就可以了。

AC程式碼:

#include<cstdio>
#include<cstring>
#include<queue>
#define M 200005
using namespace std;
int dis[M];
struct Graph {
	struct E {
		int to,nx,d;
	} edge[M];
	int tot,head[M];
	void Init() {memset(head,0,sizeof(head)),tot=0;}
	void Addedge(int a,int b,int d) {
		edge[++tot].to=b;
		edge[tot].nx=head[a];
		edge[tot].d=d;
		head[a]=tot;
	}
} G1,G2;
struct Dijstra {
	struct node {
		int id,d;
		bool operator <(const node &x)const {
			return d>x.d;
		}
		node(int id,int d):id(id),d(d) {}
	};
	priority_queue<node>Q;
	int cnt[M];
	bool mark[M];
	void solve(int st) {
		Q.push(node(st,0));
		memset(dis,63,sizeof(dis));
		memset(mark,0,sizeof(mark));
		dis[st]=0;
		while(!Q.empty()) {
			int now=Q.top().id;
			Q.pop();
			if(mark[now])continue;
			mark[now]=1;
			for(int i=G2.head[now]; i; i=G2.edge[i].nx) {//在反向圖上跑每個點到終點的最短路 
				int nxt=G2.edge[i].to;
				if(dis[now]+G2.edge[i].d<dis[nxt]) {
					dis[nxt]=dis[now]+G2.edge[i].d;
					Q.push(node(nxt,dis[nxt]));
				}
			}
		}
	}
} S;
int P,K,n;
bool have_ring;
bool in_ring[M][55];
int dp[M][55];//dp[i][j]表示從i出發,比最短路多走的路程為j的方案總數
int dfs(int now,int now_k) {
	if(in_ring[now][now_k]) {
		have_ring=1;//判是否存在0環 
		return 0;
	}
	if(dp[now][now_k]>=0)return dp[now][now_k];//記憶化 
	in_ring[now][now_k]=1;
	int& res=dp[now][now_k];
	res=0;
	for(int i=G1.head[now]; i; i=G1.edge[i].nx) {
		int nxt=G1.edge[i].to,nxtd=now_k-(G1.edge[i].d-(dis[now]-dis[nxt]));//nxtd為剩下的錢 
		if(nxtd<0||nxtd>K)continue;//超出邊界,不能走 
		res=(res+dfs(nxt,nxtd))%P;
		dfs(nxt,nxtd);
		if(have_ring)return 0;//如果有環就直接退出 
	}
	in_ring[now][now_k]=0;
	if(now==n&&now_k==0)res=1;//到達終點放在這裡判,防止終點在0環上。注意只有錢花完且到達終點才停止。 
	return res;
}
void Init() {
	have_ring=0;
	memset(in_ring,0,sizeof(in_ring));
	memset(dp,-1,sizeof(dp));
	G1.Init(),G2.Init();
}
int main() {
	int T;
	scanf("%d",&T);
	while(T--) {
		Init();
		int m;
		scanf("%d%d%d%d",&n,&m,&K,&P);
		for(int i=1; i<=m; i++) {
			int a,b,d;
			scanf("%d%d%d",&a,&b,&d);
			G1.Addedge(a,b,d);//建正向邊 
			G2.Addedge(b,a,d);//建反向邊 
		}
		S.solve(n);//預處理終點到每個點的最短距離 
		int ans=0;
		for(int i=0; i<=K; i++) {//迴圈從0開始 
			ans=(ans+dfs(1,i))%P;
			if(have_ring)break;
		}
		if(have_ring)puts("-1");
		else printf("%d\n",ans);
	}
	return 0;
}