设 f[i] 表示杀死一只 i 号怪兽的最小体力,满足 f[i]=min(K_i, S_i+Σf[spawn])。先把法术攻击代价当作初值,再从已确定更小代价的子怪兽反向更新父怪兽。
OJ: luogu
题目 ID: P4042
难度:提高+/省选-
标签:图论最短路思维
日期: 2026-06-20 05:14
题意
面对一种怪兽
- 法术攻击:花
,直接彻底消灭 - 普通攻击:花
,但它会变出若干只新的怪兽
这些新怪兽也必须继续被清理。
现在只有一只
思路
先看一个更直观的小数据暴力:
#include <bits/stdc++.h>
using namespace std;
using i128 = __int128_t;
int n;
i128 normal_cost[25], magic_cost[25];
vector<int> spawn[25];
i128 dist_arr[25];
string to_string_i128(i128 x) {
if (x == 0) {
return "0";
}
bool neg = false;
if (x < 0) {
neg = true;
x = -x;
}
string s;
while (x > 0) {
int digit = (int) (x % 10);
s.push_back(char('0' + digit));
x /= 10;
}
if (neg) {
s.push_back('-');
}
reverse(s.begin(), s.end());
return s;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n;
for (int i = 1; i <= n; i++) {
long long s, k;
int r;
cin >> s >> k >> r;
normal_cost[i] = (i128) s;
magic_cost[i] = (i128) k;
spawn[i].clear();
for (int j = 1; j <= r; j++) {
int child;
cin >> child;
spawn[i].push_back(child);
}
}
for (int i = 1; i <= n; i++) {
dist_arr[i] = magic_cost[i];
}
// 更直接的小数据做法:
// 反复使用方程
// f[i] = min(法术攻击, 普通攻击 + 生成怪兽的总代价)
// 直到没有任何值继续下降。
bool changed = true;
while (changed) {
changed = false;
for (int i = 1; i <= n; i++) {
i128 cand = normal_cost[i];
for (size_t j = 0; j < spawn[i].size(); j++) {
cand += dist_arr[spawn[i][j]];
}
if (cand < dist_arr[i]) {
dist_arr[i] = cand;
changed = true;
}
}
}
cout << to_string_i128(dist_arr[1]) << '\n';
return 0;
}暴力其实就是把题意直接写成方程:
表示杀死一只 号怪兽的最小体力
那么:
- 如果用法术攻击,代价是
- 如果用普通攻击,代价是
生成出来的所有怪兽代价之和
所以有:
brute.cpp 就是反复用这个式子去松弛,直到所有值都不再下降。
这个写法很好理解,但大数据下反复全扫会太慢。
关键观察
把式子换个方向看:
- 子怪兽的代价确定之后,父怪兽用普通攻击的总代价也就确定了
也就是说,信息传播方向其实是:
- 从"被生成的怪兽"反向流回"生成它的怪兽"
初始化为什么可以从法术攻击开始
对每种怪兽来说,法术攻击永远是一种合法方案。
所以一开始我们就知道一个上界:
于是可以先把:
当成初值。
如果后来发现:
更小,就再把
为什么像 Dijkstra 一样做
普通攻击方案的总代价是:
注意这里
所以一个怪兽通过普通攻击得到的总代价,一定不会比它那些子怪兽的代价更小。
这说明"更小的答案"会先在子怪兽处出现,再逐步传回父怪兽。
因此可以像 Dijkstra 那样:
- 先确定当前代价最小的怪兽
- 用它去反向更新所有父怪兽
维护什么量
对每个怪兽
:当前最优答案 :如果走普通攻击,目前已经累计出的总代价 :这个普通攻击方案里,还有多少个子怪兽的最终代价没确认
当某个子怪兽
如果某个父怪兽的所有子怪兽都处理完了,那么它的普通攻击方案总代价也就完整了,可以拿来更新答案。
这个过程可以用下面这张图来理解:
flowchart LR
A["child 的最优代价确定"] --> B["加到 parent 的 sum_cost 里"]
B --> C{"remain_need 是否归零?"}
C -- 是 --> D["用 sum_cost 更新 dist[parent]"]
图里真正要看的,不是道路或最短路边,而是"一个普通攻击方案何时才算完整可用"。
只有它生成出来的所有怪兽代价都已经确定,这个方案才能真正参与比较。
代码
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 200000 + 5;
const int MAXR = 1000000 + 5;
using i128 = __int128_t;
struct HeapNode {
int u;
i128 dist;
bool operator < (const HeapNode &other) const {
return dist > other.dist;
}
};
int n;
i128 normal_cost[MAXN], magic_cost[MAXN];
i128 dist_arr[MAXN]; // 杀死一只 i 号怪兽的最小体力
i128 sum_cost[MAXN]; // 如果选择普通攻击,当前已经知道的总代价
int remain_need[MAXN]; // 这个普通攻击方案里,还有多少只后继怪兽代价没汇总完
bool vis[MAXN];
int head_rev[MAXN], to_rev[MAXR], nxt_rev[MAXR], rev_cnt;
void add_rev_edge(int child, int parent) {
rev_cnt++;
to_rev[rev_cnt] = parent;
nxt_rev[rev_cnt] = head_rev[child];
head_rev[child] = rev_cnt;
}
string to_string_i128(i128 x) {
if (x == 0) {
return "0";
}
bool neg = false;
if (x < 0) {
neg = true;
x = -x;
}
string s;
while (x > 0) {
int digit = (int) (x % 10);
s.push_back(char('0' + digit));
x /= 10;
}
if (neg) {
s.push_back('-');
}
reverse(s.begin(), s.end());
return s;
}
void solve() {
priority_queue<HeapNode> pq;
for (int i = 1; i <= n; i++) {
dist_arr[i] = magic_cost[i];
if (remain_need[i] == 0 && sum_cost[i] < dist_arr[i]) {
dist_arr[i] = sum_cost[i];
}
pq.push({i, dist_arr[i]});
}
while (!pq.empty()) {
HeapNode cur = pq.top();
pq.pop();
int u = cur.u;
if (vis[u]) {
continue;
}
vis[u] = true;
// 当子怪兽 u 的最优代价确定后,它会让一批“父怪兽”的普通攻击方案更便宜。
for (int i = head_rev[u]; i != 0; i = nxt_rev[i]) {
int parent = to_rev[i];
remain_need[parent]--;
sum_cost[parent] += dist_arr[u];
if (remain_need[parent] == 0 && sum_cost[parent] < dist_arr[parent]) {
dist_arr[parent] = sum_cost[parent];
pq.push({parent, dist_arr[parent]});
}
}
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n;
rev_cnt = 0;
for (int i = 1; i <= n; i++) {
head_rev[i] = 0;
vis[i] = false;
}
for (int i = 1; i <= n; i++) {
long long s, k;
int r;
cin >> s >> k >> r;
normal_cost[i] = (i128) s;
magic_cost[i] = (i128) k;
sum_cost[i] = normal_cost[i];
remain_need[i] = r;
for (int j = 1; j <= r; j++) {
int child;
cin >> child;
add_rev_edge(child, i);
}
}
solve();
cout << to_string_i128(dist_arr[1]) << '\n';
return 0;
}复杂度
设所有普通攻击生成关系的总数是:
每个生成关系只会被处理一次,优先队列复杂度为:
空间复杂度:
总结
这题表面不像最短路,但本质上很像一类"反向更新"的最优值传播。
最关键的式子只有一个:
一旦把它想清楚,再意识到信息是从子怪兽往父怪兽传,就能顺着写出整题。
一图流解析
这张图把本题的建模、关键转移、实现检查和训练方法压缩到一页,适合读完正文后复盘。
