洛谷题单指南-树与图上的动态规划-P3953 [NOIP 2017 提高组] 逛公园
原题链接:https://www.luogu.com.cn/problem/P3953
题意解读:设dist[n]表示1到n的最短路径,求1~n的所有路径中长度不超过dist[n] + k的条数。
解题思路:
由于k的范围0~50,可以考虑枚举所有路径长度的数量:dist[n]、dist[n]+1、dist[n]+2……
对于某一种路径长度的路线数量怎么求?如果没有环,直接通过拓扑序来DP就可以。
但是有环,因为0环才会导致路线有无数条!
可以用记忆化搜索的方式来DP。
1、先通过Dijikstra计算最短路
2、枚举所有的dist[n]+k
设f[u][y]表示从1到u路径长度是dist[u]+y的路线条数
假设有v1、v2、v3…到u有边,建图时考虑建反向边,这样对于所有u->v
设f[v][x]使从1到v的路径长度是dist[v]+x的路线条数,又有dist[v]+x+w[u->v] = dist[u]+y,因此x=dist[u]+y-dist[v]-w[u->v]
如此则有状态转移:f[u][y] += ∑ f[v][dist[u]+y-dist[v]-w[u->v]]
3、通过记忆化搜索来求dp(u, y),初始值f[1][0] = 1
4、用bool vis2[u][y]记录搜索过程中在栈中的节点(dfs开始时置true、回溯时置false,这样回溯之前如果碰到true说明还在递归栈中,重复了),如果重复搜索,则表示存在环,且一定是0环,因为不是0环会导致路径边长,不可能在最短路形成的拓扑图中。
5、这里DFS的本质就是按照最短路的拓扑序DP的过程
6、建图时,用链式前向星建双向边,idx奇数是正向边,偶数是反向边
100分代码:
#include <bits/stdc++.h>
using namespace std;
typedef pair<int, int> PII;
const int N = 100005, M = 400005, K = 55;
int head[N], to[M], w[M], nxt[M], idx;
int dist[N];
bool vis[N];
int f[N][K]; //f[u][y]表示从1到u的路径长度是dist[u]+y的路线条数
bool vis2[N][K]; //vis2[u][y]表示在递归时,是否访问过节点u和路径长度y
bool flag; //是否存在环
int t, n, m, k, p;
void add(int a, int b, int c)
{
to[++idx] = b;
w[idx] = c;
nxt[idx] = head[a];
head[a] = idx;
}
void dijikstra()
{
memset(dist, 0x3f, sizeof(dist));
memset(vis, false, sizeof(vis));
dist[1] = 0;
priority_queue<PII, vector<PII>, greater<PII>> pq;
pq.push({0, 1});
while(pq.size())
{
PII item = pq.top(); pq.pop();
int u = item.second;
if(vis[u]) continue;
vis[u] = true;
for(int i = head[u]; ~i; i = nxt[i])
{
if(i % 2 == 0) continue; //只考虑正向边
int v = to[i];
if(dist[v] > dist[u] + w[i])
{
dist[v] = dist[u] + w[i];
pq.push({dist[v], v});
}
}
}
}
int dp(int u, int y)
{
//存在环,dfs到重复节点
if(vis2[u][y])
{
flag = true;
return 0;
}
//如果已经计算过,直接返回
if(f[u][y] != -1) return f[u][y];
vis2[u][y] = true; //标记当前节点和路径长度
f[u][y] = 0; //初始化当前状态的路径条数为0
for(int i = head[u]; ~i; i = nxt[i])
{
if(i % 2 == 1) continue; //只考虑反向边
int v = to[i];
int x = dist[u] - dist[v] + y - w[i];
if(x < 0 || x > k) continue; //如果x不在[0, k]范围内,跳过
f[u][y] = (f[u][y] + dp(v, x)) % p; //递归计算
if(flag) break;
}
if(u == 1 && y == 0) f[u][y] = 1; //从1到1的路径长度为0的路线条数为1
vis2[u][y] = false; //回溯
return f[u][y];
}
int main()
{
cin >> t;
while(t--)
{
memset(head, -1, sizeof(head));
idx = 0;
cin >> n >> m >> k >> p;
while(m--)
{
int a, b, c;
cin >> a >> b >> c;
add(a, b, c), add(b, a, c); //双向边
}
dijikstra();
memset(f, -1, sizeof(f));
memset(vis2, false, sizeof(vis));
flag = false;
int ans = 0;
for(int i = 0; i <= k; i++)
{
ans = (ans + dp(n, i)) % p; //计算从1到n的路径条数
if(flag) break; //如果存在环,跳出循环
}
if(flag) ans = -1;
cout << ans << endl;
}
return 0;
}