『图上DP·最短路』「NOIP2017」逛公园

本文探讨了解决最短路计数问题的一种高效方法,通过引入记忆化搜索和状态扩展,实现对特定路径数量的精确计算。文章详细介绍了如何预处理最短路径,利用Dijkstra算法,并通过反向边进行状态转移,最终解决了包含000环的复杂场景。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

题目描述

在这里插入图片描述

题解

不考虑000环如何处理,我们发现这和正常的最短路计数比较相似。

对于最短路计数,我们可以用f[i]f[i]f[i]表示到点i的最短路有多少条,在更新最短路的时候直接更新。这样的做法在实际的数据中能够拿到30分。

发现这里的kkk很小,我们可以尝试在状态上增加,即f[i][j]f[i][j]f[i][j]表示到第i个点,路径大小为最短路+k+k+k的方案数。由于更新的时候你不知道最短路,因此你必须预处理起点到每一个节点的最短路。

Dijstra算法即可,时间复杂度:O(n log n)O(n\ log\ n)O(n log n).

考虑如何状态转移,发现很难找到一种有效的方式进行转移,我们可以使用记忆化搜索的方式进行处理,具体的,我们需要建立反向边。

我们可以在记忆化时直接调用f(n,i),i∈[0,k].f(n,i),i∈[0,k].f(n,i),i[0,k].我们求得的答案就是:∑i=0kf[n][i]\sum_{i=0}^{k} f[n][i]i=0kf[n][i]

对于当前的记忆化DP状态f(i,j)f(i,j)f(i,j),表示在nnniii的路径中,花费了比最短路多jjj的路径的长度的方案。

对于正有向边(x,y,v)(x,y,v)(x,y,v),当前在转移状态f(y,val)f(y,val)f(y,val)而言;本来的路径是dis_y,如果走了这条路进行的方案就是disx+vdis_x+vdisx+v,那么意味着就多花费了t=(disx+v)−disy,t=(dis_x+v)-dis_y,t=(disx+v)disy,再进行对状态f(x,val−t)f(x, val-t)f(x,valt)进行转移即可。

现在考虑如何处理000环,发现只要在记忆化的过程中,在保证每一个状态合法性(可以将不合法的预先剪枝掉)时某一个相同状态走了两遍,说明一定经过了零环。由于知道满足val∈[0,k]val∈[0,k]val[0,k]一定可以通过最短路走到起点,故一定合法;把经过零环的点和val∉[0,k]val∉[0,k]val/[0,k]的状态剪枝即可。

终于肝掉了一道毒瘤题好开心好开心~~

#include <bits/stdc++.h>

using namespace std;
const int N = 300000;

int n, m, k, P, tot = 0;
int Link[N], dis[N], vis[N];

int f[110000][100];
int used[110000][100]; 

vector < pair<int,int> > a[N];

struct edge {
	int y, next, v;
} e[N*2];

int read(void)
{
	int s = 0, w = 0;char c = getchar();
	while (c < '0' || c > '9') w |= c == '-', c = getchar();
	while (c >= '0' && c <= '9') s = s*10+c-48, c = getchar();
	return w ? -s : s;
}

void clear(void)
{
	tot = 0;
	memset(f,-1,sizeof f);
	memset(Link,0,sizeof Link);
	memset(used,0,sizeof used);
	for (int i=0;i<N;++i) a[i].clear();
}

void add(int x,int y,int v)
{
	e[++tot] = {y,Link[x],v};
	Link[x] = tot;
}

void Dijkstra(void)
{
	memset(vis,0,sizeof vis);
	memset(dis,30,sizeof dis);
	priority_queue < pair<int,int> > q;
	q.push(make_pair(0,1)), dis[1] = 0;
	while (q.size())
	{
		int u = q.top().second;q.pop();
		if (vis[u]) continue;vis[u] = 1;
		for (int i=0;i<a[u].size();++i)
		{
			int v = a[u][i].first;
			int val = a[u][i].second;
			if (dis[u]+val < dis[v]) {
				dis[v] = dis[u]+val;
				q.push(make_pair(-dis[v],v));
			}
		}
	}
	return;
}

int dp(int now,int last)
{
	if (last < 0 || last > k) return 0;
	if (f[now][last] >= 0) return f[now][last];
	if (used[now][last] == 1) {
		used[now][last] = 0;
		return -1;
	}
	else used[now][last] = 1;
	int ans = 0;
	for (int i=Link[now];i;i=e[i].next)
	{
		int next = e[i].y, v = e[i].v;
		int cost = dis[next]+v-dis[now];
		int val = dp(next,last-cost);
		if (val == -1) {
			used[now][last] = 0;
			return -1;
		}
		else ans = (ans+val) % P;
	}
	used[now][last] = 0;
	if (now == 1 && last == 0) ans = (ans + 1) % P;
	return f[now][last] = ans;
}

int DP_work(void)
{
	int ans = 0;
	for (int i=0;i<=k;++i) 
	{
		int val = dp(n,i);
		if (val == -1) return -1;
		else ans = (ans+val) % P;
	}
	return ans;
}

int work(void)
{
	n = read(), m = read();
	k = read(), P = read();
	clear();
	for (int i=1;i<=m;++i)
	{
		int x = read(), y = read(), v = read();
		add(y,x,v);
		a[x].push_back(make_pair(y,v));
	}
	Dijkstra();
	return DP_work();
}

int main(void)
{
	int T = read();
	while (T --) printf("%d\n", work());
	return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值