反向图Dijkstra预处理最短路,将路径长度转化为相对最短路的额外偏差量,结合记忆化搜索与递归栈判0环。
OJ: luogu
题目 ID: P3953
难度:提高+/省选-
标签:最短路Dijkstra动态规划记忆化搜索
创建: 2026-10-23 23:05
更新: 2026-10-26 14:33
形式化题目
给定一张包含
设从点
若存在无穷多条满足条件的路径,则输出
暴力解法
思路
最朴素的方法是从起点
- 当
时回溯; - 当搜索到达终点
时,将答案计数加 ; - 针对可能存在的
权边,如果在当前搜索路径上多次访问同一个点且权值没有增加,说明进入了由 权边构成的环;若从该环出发能到达终点,则判定存在无穷多条合法路线并报告 。
代码
/**
* Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
* rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
* rainboy的学习导航网站: https://idx.roj.ac.cn
* create_at: 2026-10-23 23:38
* update_at: 2026-10-23 23:38
*/
// brute.cpp:小数据暴力解(带严格状态环检测的 DFS,用于对拍验证,仅适用于极小图)
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int MAXN = 100;
const int MAXM = 200;
const ll INF = 0x3f3f3f3f3f3f3f3fLL;
int head[MAXN], to_node[MAXM], nxt_edge[MAXM], edge_weight[MAXM], edge_cnt;
ll dis[MAXN];
bool vis_dijk[MAXN];
ll n, m, k, p;
ll ans_cnt;
bool infinite_paths;
bool in_stack_node[MAXN][500]; // 记录 (u, cur_len) 是否在搜索栈中
void add_edge(int u, int v, int w) {
edge_cnt++;
to_node[edge_cnt] = v;
edge_weight[edge_cnt] = w;
nxt_edge[edge_cnt] = head[u];
head[u] = edge_cnt;
}
// 堆优化 Dijkstra 求 1 到各点的最短路
struct Node {
int id;
ll d;
bool operator>(const Node &other) const { return d > other.d; }
};
void dijkstra() {
for (int i = 1; i <= n; i++) {
dis[i] = INF;
vis_dijk[i] = false;
}
priority_queue<Node, vector<Node>, greater<Node>> pq;
dis[1] = 0;
pq.push({1, 0});
while (!pq.empty()) {
Node top = pq.top();
pq.pop();
int u = top.id;
if (vis_dijk[u]) continue;
vis_dijk[u] = true;
for (int e = head[u]; e; e = nxt_edge[e]) {
int v = to_node[e];
int w = edge_weight[e];
if (dis[v] > dis[u] + w) {
dis[v] = dis[u] + w;
pq.push({v, dis[v]});
}
}
}
}
// 检查从 u 能否走到达 n 且距离 <= dis[n] + k
bool can_reach(int start_u, ll cur_dist) {
queue<pair<int, ll>> q;
q.push({start_u, cur_dist});
bool visited[MAXN];
for (int i = 1; i <= n; i++) visited[i] = false;
visited[start_u] = true;
while (!q.empty()) {
auto [u, d] = q.front();
q.pop();
if (u == n) return true;
for (int e = head[u]; e; e = nxt_edge[e]) {
int v = to_node[e];
int w = edge_weight[e];
if (!visited[v] && d + w <= dis[n] + k) {
visited[v] = true;
q.push({v, d + w});
}
}
}
return false;
}
// 暴力 DFS 搜索所有从 1 出发、长度不超过 dis[n] + k 的路径
void dfs(int u, ll cur_len) {
if (infinite_paths) return;
if (cur_len > dis[n] + k) return;
if (in_stack_node[u][cur_len]) {
// 在同样累计长度下再次到达同一个点 u,说明走了一个 0 环!
// 且只要从 u 出发能够到达终点 n,就意味着有无穷多条合法路线
if (can_reach(u, cur_len)) {
infinite_paths = true;
}
return;
}
in_stack_node[u][cur_len] = true;
if (u == n) {
ans_cnt = (ans_cnt + 1) % p;
}
for (int e = head[u]; e; e = nxt_edge[e]) {
int v = to_node[e];
int w = edge_weight[e];
dfs(v, cur_len + w);
if (infinite_paths) return;
}
in_stack_node[u][cur_len] = false;
}
void solve() {
cin >> n >> m >> k >> p;
edge_cnt = 0;
for (int i = 1; i <= n; i++) head[i] = 0;
for (int i = 1; i <= m; i++) {
int u, v, w;
cin >> u >> v >> w;
add_edge(u, v, w);
}
dijkstra();
if (dis[n] == INF) {
cout << 0 << "\n";
return;
}
ans_cnt = 0;
infinite_paths = false;
memset(in_stack_node, 0, sizeof(in_stack_node));
dfs(1, 0);
if (infinite_paths) {
cout << -1 << "\n";
} else {
cout << ans_cnt % p << "\n";
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int t;
if (cin >> t) {
while (t--) {
solve();
}
}
return 0;
}复杂度
在图结构稠密或环较多时,搜索树的节点规模呈指数级爆炸,最坏时间复杂度为
瓶颈
暴力的核心瓶颈在于:
- 状态重复计算:同一对「当前所在点」与「已走距离」会被大量不同的前缀路径多次遍历;
- 状态值域过大:路径的绝对长度可达
,直接以实际距离作为状态数组下标在空间与时间上均不可行。
正解
思路
1. 状态降维:以「相对最短路的偏差量」作为状态
注意到题目给出了强约束
设各点
- 从
沿该边走到 再走到终点 ,其理论能达到的最小总花费为 ; - 而从
到 本身的最短路为 ; - 由最短路的三角不等式性质,显然有
,即:
这里的
2. 状态定义与转移方程
定义状态:
- 初始基准:当
且 时,自身到达终点且无偏差,提供 种基础方案。 - 状态转移:
遍历点
的所有正向出边 ,计算 。 若 ,则说明转移可行: - 最终答案:
从起点
出发,枚举总额外花费 ,累加所有 。
下图把这条转移式拆成来源, 说明
flowchart LR S["状态 dp[u][j]"] --> E1["边 u 到 v1, 权 w1, 偏差 Δ1"] S --> E2["边 u 到 v2, 权 w2, 偏差 Δ2"] E1 --> D1["读取 dp[v1][j - Δ1], 需要 j 不小于 Δ1"] E2 --> D2["读取 dp[v2][j - Δ2], 需要 j 不小于 Δ2"] D1 --> SUM["求和得到 dp[u][j]"] D2 --> SUM D2 -.->|"j 小于 Δ 时越界"| DROP["该来源整项丢弃"]
图中两条出边各自把
下面用一个
| 边 |
含义 | |||
|---|---|---|---|---|
| 与最短路等长 | ||||
| 与最短路等长 | ||||
| 与最短路等长 | ||||
| 与最短路等长 | ||||
| 比最短路多花 |
四条
读表时先看
in_stack 的原因.
3. 记忆化搜索与判断无穷解(0 权环)
因为转移存在拓扑关系(第二维
如何判断是否存在无穷多条路径并输出 in_stack[u][j] 记录当前状态
- 若进入
dfs(u, j)时发现in_stack[u][j] == true,说明在相同的额外偏差量下回到了自身(走了一圈 的回路); - 此时立即标记发现
环,整道题直接输出 。
代码
/**
* Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
* rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
* rainboy的学习导航网站: https://idx.roj.ac.cn
* create_at: 2026-10-23 23:38
* update_at: 2026-10-23 23:38
*/
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int MAXN = 100005;
const int MAXM = 200005;
const int MAXK = 55;
const ll INF = 0x3f3f3f3f3f3f3f3fLL;
// 正向图与反向图的链式前向星
int head_f[MAXN], to_f[MAXM], nxt_f[MAXM], weight_f[MAXM], edge_cnt_f;
int head_b[MAXN], to_b[MAXM], nxt_b[MAXM], weight_b[MAXM], edge_cnt_b;
// 最短路相关
ll dis_from_1[MAXN]; // 正向图从 1 出发的最短距离
ll dis_to_n[MAXN]; // 反向图从 n 出发的最短距离(即各点到 n 的最短距离)
bool vis_dijk[MAXN];
// 记忆化搜索与判环标记
ll dp[MAXN][MAXK];
bool in_stack[MAXN][MAXK]; // 正在搜索栈中,用于检测 0 环引起的死循环
int n, m, k;
ll p;
bool has_zero_cycle;
void add_edge_forward(int u, int v, int w) {
edge_cnt_f++;
to_f[edge_cnt_f] = v;
weight_f[edge_cnt_f] = w;
nxt_f[edge_cnt_f] = head_f[u];
head_f[u] = edge_cnt_f;
}
void add_edge_backward(int u, int v, int w) {
edge_cnt_b++;
to_b[edge_cnt_b] = v;
weight_b[edge_cnt_b] = w;
nxt_b[edge_cnt_b] = head_b[u];
head_b[u] = edge_cnt_b;
}
struct Node {
int id;
ll d;
bool operator>(const Node &other) const {
return d > other.d;
}
};
// 堆优化 Dijkstra 求正向最短路
void dijkstra_from_1() {
for (int i = 1; i <= n; i++) {
dis_from_1[i] = INF;
vis_dijk[i] = false;
}
priority_queue<Node, vector<Node>, greater<Node>> pq;
dis_from_1[1] = 0;
pq.push({1, 0});
while (!pq.empty()) {
Node top = pq.top();
pq.pop();
int u = top.id;
if (vis_dijk[u]) continue;
vis_dijk[u] = true;
for (int e = head_f[u]; e; e = nxt_f[e]) {
int v = to_f[e];
int w = weight_f[e];
if (dis_from_1[v] > dis_from_1[u] + w) {
dis_from_1[v] = dis_from_1[u] + w;
pq.push({v, dis_from_1[v]});
}
}
}
}
// 堆优化 Dijkstra 求反向最短路
void dijkstra_to_n() {
for (int i = 1; i <= n; i++) {
dis_to_n[i] = INF;
vis_dijk[i] = false;
}
priority_queue<Node, vector<Node>, greater<Node>> pq;
dis_to_n[n] = 0;
pq.push({n, 0});
while (!pq.empty()) {
Node top = pq.top();
pq.pop();
int u = top.id;
if (vis_dijk[u]) continue;
vis_dijk[u] = true;
for (int e = head_b[u]; e; e = nxt_b[e]) {
int v = to_b[e];
int w = weight_b[e];
if (dis_to_n[v] > dis_to_n[u] + w) {
dis_to_n[v] = dis_to_n[u] + w;
pq.push({v, dis_to_n[v]});
}
}
}
}
// 记忆化搜索:dfs(u, j) 表示从 u 走向 n,额外比 dis_to_n[u] 多花 j 的合法路线数
ll dfs(int u, int j) {
if (j < 0 || j > k) return 0;
if (in_stack[u][j]) {
has_zero_cycle = true;
return 0;
}
if (dp[u][j] != -1) {
return dp[u][j];
}
in_stack[u][j] = true;
ll total = 0;
if (u == n && j == 0) {
total = 1;
}
for (int e = head_f[u]; e; e = nxt_f[e]) {
int v = to_f[e];
int w = weight_f[e];
if (dis_to_n[v] == INF) continue; // 无法到达终点
ll extra = dis_to_n[v] + w - dis_to_n[u];
ll next_j = j - extra;
if (next_j >= 0 && next_j <= k) {
total = (total + dfs(v, next_j)) % p;
if (has_zero_cycle) return 0;
}
}
in_stack[u][j] = false;
dp[u][j] = total;
return total;
}
void solve() {
cin >> n >> m >> k >> p;
edge_cnt_f = 0;
edge_cnt_b = 0;
for (int i = 1; i <= n; i++) {
head_f[i] = 0;
head_b[i] = 0;
}
for (int i = 1; i <= m; i++) {
int u, v, w;
cin >> u >> v >> w;
add_edge_forward(u, v, w);
add_edge_backward(v, u, w);
}
dijkstra_from_1();
dijkstra_to_n();
// 无法从 1 到达 n
if (dis_from_1[n] == INF) {
cout << 0 << "\n";
return;
}
for (int i = 1; i <= n; i++) {
for (int j = 0; j <= k; j++) {
dp[i][j] = -1;
in_stack[i][j] = false;
}
}
has_zero_cycle = false;
ll ans = 0;
for (int j = 0; j <= k; j++) {
ans = (ans + dfs(1, j)) % p;
if (has_zero_cycle) {
cout << -1 << "\n";
return;
}
}
cout << ans << "\n";
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int t;
if (cin >> t) {
while (t--) {
solve();
}
}
return 0;
}复杂度
- 时间复杂度:
- 反向图 Dijkstra 预处理各点到
的最短路:时间复杂度为 ; - 记忆化搜索:状态总数为
。由于每个状态只会被完整展开并记忆化一次,所有状态转移遍历的所有出边总次数为 。 - 单组数据总时间复杂度为
。对于 ,总运算量在 级别,运行耗时约 ,能充分在 限制内处理全部数据。
- 反向图 Dijkstra 预处理各点到
- 空间复杂度:
- 链式前向星(正向与反向):
; - 最短路数组与堆:
; - DP 记忆化与递归栈标记数组:
,占用内存约 ,完全满足空间限制要求。
- 链式前向星(正向与反向):
总结
本题的关键在于将无法直接用于 DP 的绝对距离转化为相对最短路的额外增量。利用