把追逃过程按第一次传送拆成两段:传送前 Pang 固定在 k,是一次带权最短路;传送后在某点 v 沿 v→n 的最短路走,伤害成周期为 d 的等差数列,用公式 O(1) 结算。
OJ: luogu
题目 ID: P9724
难度:提高
标签:图论最短路BFSDijkstra等差数列贪心
创建: 2026-10-02 15:23
更新: 2026-10-07 12:15
形式化题目
给定一张
过程按“秒”推进,每一秒:
- Shou 先选择一个相邻顶点并走过去;
- 到达之后才结算伤害。设此刻 Shou 在
、Pang 在 , 为图上最短路长度: :Pang 留在 ,Shou 受到 点伤害; :Pang 传送到 ,Shou 受到 点伤害。
到达
暴力解法
思路
题目把两个对象的位置都摆在了明面上,最忠实的建模就是把它们都放进状态:
状态
表示 Pang 在 、Shou 在 ,初始状态为 。
对 Shou 从
- 若
:迁移到 ,花费 ; - 否则:Pang 传送到
,迁移到 ,花费 。
所有边权都非负,于是在
以样例 1(
| 步 | Shou 移动 | 到达后状态 |
本步伤害 | |
|---|---|---|---|---|
| 1 | ||||
| 2 |
总伤害
代码
/**
* 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-03 13:05
* update_at: 2026-10-03 13:05
*/
// brute.cpp:小数据暴力解,用来帮助理解题意并辅助对拍。
// 做法:直接把题目里的过程建模成状态图。
// 状态 = (Pang 所在点 p, Shou 所在点 u);
// Shou 从 u 走到邻居 v 后结算伤害:
// dist(p, v) < d -> 伤害 d - dist(p, v),Pang 不动;
// dist(p, v) >= d -> 伤害 d,Pang 传送到 v。
// 边权非负,所以在 n*n 个状态上跑一次最短路即可。
// 复杂度 O(n*m + n^2 log n),只适合 n 很小的数据。
#include <bits/stdc++.h>
using namespace std;
const int MAXB = 65; // 暴力只支持 n <= 64
const long long INF = (1LL << 60);
int n, m, k, d;
vector<int> g[MAXB];
int dist_all[MAXB][MAXB]; // dist_all[s][v]:图上 s 到 v 的最短路
long long best[MAXB][MAXB]; // best[p][u]:Pang 在 p、Shou 在 u 时的最小累计伤害
bool done[MAXB][MAXB];
struct State {
int p; // Pang 当前位置
int u; // Shou 当前位置
long long cost;
bool operator < (const State &other) const {
return cost > other.cost;
}
};
// 无权图最短路,起点 s,距离写入 dist[]。
void bfs(int s, int dist[]) {
for (int i = 1; i <= n; i++) {
dist[i] = -1;
}
queue<int> q;
dist[s] = 0;
q.push(s);
while (!q.empty()) {
int u = q.front();
q.pop();
for (int i = 0; i < (int)g[u].size(); i++) {
int v = g[u][i];
if (dist[v] == -1) {
dist[v] = dist[u] + 1;
q.push(v);
}
}
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n >> m >> k >> d;
for (int i = 1; i <= m; i++) {
int a, b;
cin >> a >> b;
g[a].push_back(b);
g[b].push_back(a);
}
for (int s = 1; s <= n; s++) {
bfs(s, dist_all[s]);
}
for (int p = 1; p <= n; p++) {
for (int u = 1; u <= n; u++) {
best[p][u] = INF;
done[p][u] = false;
}
}
best[k][1] = 0;
priority_queue<State> pq;
State start;
start.p = k;
start.u = 1;
start.cost = 0;
pq.push(start);
while (!pq.empty()) {
int p = pq.top().p;
int u = pq.top().u;
long long cost = pq.top().cost;
pq.pop();
if (done[p][u]) {
continue;
}
done[p][u] = true;
for (int i = 0; i < (int)g[u].size(); i++) {
int v = g[u][i];
int dis = dist_all[p][v];
long long nc;
int np;
if (dis < d) {
nc = cost + (d - dis);
np = p;
}
else {
nc = cost + d;
np = v;
}
if (nc < best[np][v]) {
best[np][v] = nc;
State next;
next.p = np;
next.u = v;
next.cost = nc;
pq.push(next);
}
}
}
// Shou 可能在 Pang 的任意位置上到达终点 n。
long long ans = INF;
for (int p = 1; p <= n; p++) {
if (best[p][n] < ans) {
ans = best[p][n];
}
}
cout << ans << "\n";
return 0;
}复杂度
状态数
瓶颈
瓶颈是显式枚举 Pang 的位置:
但回顾规则会发现,Pang 的位置其实没有多少自由度:他要么还没有动过,仍在
正解
思路
以第一次传送为分界线,把过程切成两段。
第一段:还没有发生传送。 只要每次判定都满足 dist_shou[u]。
第二段:第一次传送之后。 设传送发生在点
从
这一步只和
于是正解只需:
- 以
为源 BFS,得 dist_k[v] = dist(k, v); - 以
为源 BFS,得 dist_n[v] = dist(v, n); - 在“Pang 仍在
”的阶段跑带权 Dijkstra,遍历邻居 时: dist_k[v] >= d:可以作为首次传送点,用 dist_shou[u] + teleport_cost(dist_n[v] + 1)更新答案;- 否则用
dist_shou[u] + (d - dist_k[v])松弛dist_shou[v]。
最后答案取 min(ans, dist_shou[n]),后者覆盖一次传送都没发生的情况。
下面用样例 1(dist_shou[3] = 1),其余邻居一律按“传送”立即结算:
| 从 |
到 |
处理方式 | 候选答案 | |
|---|---|---|---|---|
| 1 | 2 | 2 | 传送 | |
| 1 | 3 | 0 | 松弛 dist_shou[3] = 0+(1-0) = 1 |
— |
| 3 | 1 | 1 | 传送 | |
| 3 | 5 | 1 | 传送 |
最小值
代码
/**
* 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-03 13:05
* update_at: 2026-10-03 13:05
*/
// main.cpp:[EC Final 2022] Chase Game 的正解。
// 思路:把追逃过程分成“传送之前 / 第一次传送之后”两段。
// - 传送之前 Pang 一直待在 k,Shou 走到每个点的最小伤害是一次带权最短路。
// - 一旦 Pang 在某点 v 传送,之后两人的相对位置重新由 v 决定,
// 剩余伤害只和 dist(v, n) 有关,可以用等差数列公式直接算出。
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 100005;
const long long INF = (1LL << 62);
int n, m, k, d;
vector<int> g[MAXN]; // 邻接表
long long dist_k[MAXN]; // dist_k[v]:k 到 v 的图上最短路(Pang 不动的依据)
long long dist_n[MAXN]; // dist_n[v]:n 到 v 的图上最短路(传送后还要走多远)
long long dist_shou[MAXN]; // dist_shou[u]:Pang 仍停在 k 时,Shou 走到 u 的最小累计伤害
bool vis[MAXN];
struct PQNode {
int u;
long long dist;
bool operator < (const PQNode &other) const {
return dist > other.dist;
}
};
// 无权图最短路,起点 s,距离写入 dist[]。
void bfs(long long dist[], int s) {
for (int i = 1; i <= n; i++) {
dist[i] = -1;
}
queue<int> q;
dist[s] = 0;
q.push(s);
while (!q.empty()) {
int u = q.front();
q.pop();
for (int i = 0; i < (int)g[u].size(); i++) {
int v = g[u][i];
if (dist[v] == -1) {
dist[v] = dist[u] + 1;
q.push(v);
}
}
}
}
// 等差数列求和:l + (l+1) + ... + r。
long long range_sum(long long l, long long r) {
return (l + r) * (r - l + 1) / 2;
}
// 从传送发生的那一刻算起,连续 x 次攻击的伤害总和。
// 伤害序列为 d, d-1, ..., 1, d, d-1, ...(每 d 次打出一个完整循环)。
long long teleport_cost(long long x) {
long long full = x / d; // 完整循环个数
long long rem = x % d; // 末尾不足一个循环的项数
// 每个完整循环的和是 1 + 2 + ... + d;
// 剩余 rem 项是 d, d-1, ..., d-rem+1。
return full * range_sum(1, d) + range_sum(d - rem + 1, d);
}
// 传送之前(Pang 停在 k)的带权最短路,同时把每个“第一次传送”的答案统计进来。
long long solve() {
for (int i = 1; i <= n; i++) {
dist_shou[i] = INF;
vis[i] = false;
}
dist_shou[1] = 0;
priority_queue<PQNode> pq;
PQNode start;
start.u = 1;
start.dist = 0;
pq.push(start);
long long ans = INF;
while (!pq.empty()) {
int u = pq.top().u;
pq.pop();
if (vis[u]) {
continue;
}
vis[u] = true;
for (int i = 0; i < (int)g[u].size(); i++) {
int v = g[u][i];
if (dist_k[v] >= d) {
// Shou 走到 v 后 dist(k, v) >= d,Pang 传送过来并打出 d 点伤害。
// 此后 Pang 停在 v,Shou 沿最短路走向 n,剩余伤害用公式算出。
long long cand = dist_shou[u] + teleport_cost(dist_n[v] + 1);
if (cand < ans) {
ans = cand;
}
}
else {
// dist(k, v) < d,Pang 留在 k,这一秒伤害为 d - dist_k[v]。
long long nd = dist_shou[u] + (d - dist_k[v]);
if (nd < dist_shou[v]) {
dist_shou[v] = nd;
PQNode node;
node.u = v;
node.dist = nd;
pq.push(node);
}
}
}
}
// 一次传送都没发生就走到 n 的情况。
if (dist_shou[n] < ans) {
ans = dist_shou[n];
}
return ans;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n >> m >> k >> d;
for (int i = 1; i <= m; i++) {
int a, b;
cin >> a >> b;
g[a].push_back(b);
g[b].push_back(a);
}
bfs(dist_k, k);
bfs(dist_n, n);
cout << solve() << "\n";
return 0;
}复杂度
两次 BFS 为
总结
本题的关键不是某个数据结构,而是看出 Pang 的位置没有独立自由度:
- 没传送时他固定在
,第一段退化为一张固定权图上的最短路; - 传送发生后他直接跳到 Shou 脚下,第二段的最优策略是沿最短路冲向
,伤害由周期为 的等差数列完全确定。
把“第一次传送”作为唯一的分界点枚举,就把