QOJ #6406. Stage Clear 题解

Description

陈教授非常喜欢玩电脑游戏。他现在正在游戏中与可怕的怪物战斗。

战场由 nn 个交叉点组成,编号为 1 到 nn。这些交叉点之间有 mm 条有向边,因此整个战场可以被看作一个有向无环图(DAG)。玩家现在处在交叉点 1,并拥有 XX 点生命值(HP)。

除了交叉点 1,每个交叉点上都有一个怪物。当玩家第一次移动到某个交叉点时,必须与该交叉点上的怪物战斗。在战斗中,玩家会损失 aia_i 点 HP,而在击败怪物后,他会获得 bib_i 点 HP。注意:一旦 HP 变成负数(< 0),游戏就会失败,所以必须确保这种情况绝不会发生

如果玩家多次访问同一个交叉点,战斗只会在第一次发生——怪物不会复活。玩家只能沿着给定的 mm 条有向边移动,或者通过魔法飞回交叉点 1。飞行可以进行多次。

此外,从交叉点 1 总是可以到达任意一个交叉点

当所有怪物都被击败后,陈教授就可以通过这一关卡。

请你编写一个程序,计算出通关所需的最小初始生命值 HP(即最小的 XX

n+m72,mn1n+m\leq 72,m\geq n-1

Solution

首先 n25n\leq 25 时可以暴力状压 dp 做到 O(2nn)O(2^nn)

对于 n>25n>25m72n=46m\leq 72-n=46,此时 mnm-n 比较小,很容易想到这是 dfs 树上非树边的数量。

注意到操作的过程类似一棵树,即一个点的父亲选了,这个点才能选,所以考虑暴力枚举出每个点的父亲,这是 O(2mn+1)O(2^{m-n+1}) 级别的。

剩下的部分形如:·给定一棵树,一个点的父亲选了,这个点才能选,问最小初始生命值。

这是个比较经典的贪心问题,先不管父亲的限制,通过排序得到一个最优的顺序。

设最优顺序中第一个是 uu,那么当 uu 的父亲选了后一定贪心地选 uu,所以将 uufaufa_u 合并后就转化为了一个规模为 n1n-1 的问题。实现时用 set 维护。

具体排序方式为临项交换,分讨一下两个点是当前顺序优还是交换后优即可。

这部分时间复杂度:O(2mn+1nlogn)O(2^{m-n+1}n\log n)

Code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
#include <bits/stdc++.h>

#define int int64_t

const int kMaxN = 75;

int n, m;
int a[kMaxN], b[kMaxN], gs[kMaxN];
bool g[kMaxN][kMaxN];

namespace Sub1 {
int f[1 << 25];

void solve() {
std::fill_n(f, 1 << n, 1e18);
f[1] = 1;
for (int s = 1; s < (1 << n); s += 2) {
int gss = 0, sum = 0;
for (int i = 1; i <= n; ++i)
if (s >> (i - 1) & 1)
gss |= gs[i], sum += b[i] - a[i];
for (int i = 1; i <= n; ++i) {
if ((gss >> (i - 1) & 1) && (~s >> (i - 1) & 1)) {
int t = (s | (1 << (i - 1)));
f[t] = std::min(f[t], std::max(f[s], a[i] - sum));
}
}
}
std::cout << f[(1 << n) - 1] << '\n';
}
} // namespace Sub1

namespace Sub2 {
struct Node {
int a, b;
friend bool operator <(Node n1, Node n2) {
// return std::max(n1.a, n2.a - (n1.b - n1.a)) < std::max(n2.a, n1.a - (n2.b - n2.a));
if ((n1.a < n1.b) != (n2.a < n2.b)) return n1.a < n1.b;
if (n1.a < n1.b) return n1.a < n2.a;
else return n1.b > n2.b;
}
friend Node operator +(Node n1, Node n2) {
static Node ret;
ret.a = std::max(n1.a, n1.a - n1.b + n2.a);
ret.b = n1.b - n1.a + n2.b - n2.a + ret.a;
return ret;
}
} t[kMaxN];

int ans = 1e18, p[kMaxN], fa[kMaxN];

int find(int x) { return x == fa[x] ? x : fa[x] = find(fa[x]); }
void unionn(int x, int y) {
int fx = find(x), fy = find(y);
if (fx != fy) fa[fx] = fy;
}

void check() {
std::set<std::pair<Node, int>> st;
t[1] = {0, 0}, std::iota(fa + 1, fa + 1 + n, 1);
for (int i = 2; i <= n; ++i)
st.emplace(t[i] = {a[i], b[i]}, i);
for (int c = 1; c <= n - 1; ++c) {
int i = st.begin()->second, pp = find(p[i]); st.erase(st.begin());
fa[i] = pp;
if (pp != 1) st.erase({t[pp], pp});
t[pp] = t[pp] + t[i];
if (pp != 1) st.emplace(t[pp], pp);
}
// for (int i = 2; i <= n; ++i) std::cerr << p[i] << " \n"[i == n];
ans = std::min(ans, t[1].a);
}

void dfs(int u) {
if (u == n + 1) return check();
for (int i = 1; i < u; ++i)
if (g[i][u])
p[u] = i, dfs(u + 1);
}

void solve() {
dfs(2);
std::cout << ans << '\n';
}
} // namespace Sub2

void dickdreamer() {
std::cin >> n >> m;
for (int i = 2; i <= n; ++i) {
std::cin >> a[i] >> b[i];
}
for (int i = 1; i <= m; ++i) {
int u, v;
std::cin >> u >> v;
g[u][v] = 1;
if (n <= 25) gs[u] |= (1 << (v - 1));
}
if (n <= 25) Sub1::solve();
else Sub2::solve();
}

int32_t main() {
#ifdef ORZXKR
freopen("in.txt", "r", stdin);
freopen("out.txt", "w", stdout);
#endif
std::ios::sync_with_stdio(0), std::cin.tie(0), std::cout.tie(0);
int T = 1;
// std::cin >> T;
while (T--) dickdreamer();
// std::cerr << 1.0 * clock() / CLOCKS_PER_SEC << "s\n";
return 0;
}

QOJ #6406. Stage Clear 题解
https://sobaliuziao.github.io/2025/04/14/post/c171e966.html
作者
Egg_laying_master
发布于
2025年4月14日
许可协议