洛谷 P10220. 【联合省选 2024】Day2 T1 迷宫守卫 题解
[省选联考 2024] 迷宫守卫
题目描述
Alice 拥有一座迷宫,这座迷宫可以抽象成一棵拥有 $2^n$ 个叶节点的满二叉树,总节点数目为 $(2^{n+1} - 1)$,依次编号为 $1 \sim (2^{n+1} - 1)$。其中编号为 $2^n \sim (2^{n+1} - 1)$ 的是叶节点,编号为 $1 \sim (2^n - 1)$ 的是非叶节点,且非叶节点 $1 \le u \le (2^n - 1)$ 的左儿子编号为 $2u$,右儿子编号为 $(2u + 1)$。
每个非叶节点都有一个石像守卫,初始时,所有石像守卫均在沉睡。唤醒 $u$ 点的石像守卫需要 $w_u$ 的魔力值。
每个叶节点都有一个符文,$v$ 点的符文记作 $q_v$。保证 $q_{2^n}, q_{2^n+1},\cdots, q_{2^{n+1}-1}$ 构成 $1 \sim 2^n$ 的排列。
探险者初始时持有空序列 $Q$,从节点 $1$ 出发,按照如下规则行动:
- 到达叶节点 $v$ 时,将 $v$ 点的符文 $q_v$ 添加到序列 $Q$ 的末尾,然后返回父节点。
- 到达非叶节点 $u$ 时:
- 若该点的石像守卫已被唤醒,则只能先前往左儿子,(从左儿子返回后)再前往右儿子,(从右儿子返回后)最后返回父节点。
- 若该点的石像守卫在沉睡,可以在以下二者中任选其一:
- 先前往左儿子,再前往右儿子,最后返回父节点。
- 先前往右儿子,再前往左儿子,最后返回父节点。
返回节点 $1$ 时,探险结束。可以证明,探险者一定访问每个叶节点各一次,故此时 $Q$ 的长度为 $2^n$。
探险者 Bob 准备进入迷宫,他希望探险结束时的 $Q$ 的字典序越小越好,与之相对,Alice 希望 $Q$ 的字典序越大越好。
在 Bob 出发之前,Alice 可以选择一些魔力值花费之和不超过 $K$ 的石像守卫,并唤醒它们。Bob 出发时,他能够知道 Alice 唤醒了哪些神像。若双方都采取最优策略,求序列 $Q$ 的最终取值。
题解
由于需要使字典序最小,考虑先让第一个数 $Q_1$ 最小。
这里可以使用 二分和 $O(2^n)$ 的 $DP$ 解决。
具体地,设 $dp_u$ 表示使 $u$ 子树内选不到 $\lt mid$ 的数作为第一个数的最小代价。
转移:
$
dp_u =
\begin{cases}
dp_{ls} \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ (mn(rs) \gt mid) \\
dp_{ls} + \min(dp_{rs}, w_u) \ \ \ \ \ \ \ \ \ \ \ \ (mn(rs) \lt mid)
\end{cases}
$
选择完 $Q_1$ 之后,我们需要使用剩下的 $K$ 去调整下面的序列以获得最优解。
我们发现,由于满二叉树的结构性质,接下来的序列一定是 $Q_2 , \{Q_3, Q_4\} , \{Q_5, Q_6, Q_7, Q_8\}, …$
$\{x, y\}$ 表示一个集合,集合内部顺序尚未确定,但是集合之间的顺序显然是已确定的。
而且我们确定的优先级,显然是越往前的集合优先级越高。
我们使用函数 $solve(u, k)$ 计算 $u$ 子树内代价上限 $k$,所能达到的最优解和最小代价。
于是乎可以直接递归求解了。
代码
#include <bits/stdc++.h>
using namespace std;
const int N = (1 << 20) + 15;
const long long INF = 1e15;
int T, n, m, q[N], id[N];
int Min[N]; //子树内最小的 q
vector<int> son[N]; //子树内的所有 q
long long w[N], dp[N], k;
void clear() {
for (int i = 1; i <= m; i++) son[i].clear();
}
void dfs(int u, int lim) { //dp
if (u >= n && u <= m) {
if (q[u] < lim) dp[u] = INF;
else dp[u] = 0;
return;
}
dfs(u << 1, lim); dfs(u << 1 | 1, lim);
if (Min[u << 1 | 1] < lim) dp[u] = dp[u << 1] + min(dp[u << 1 | 1], w[u]);
else dp[u] = dp[u << 1];
}
pair<long long, vector<int> > solve(int u, long long k) {
long long qwq = k;
vector<int> ans;
int l = 0, r = son[u].size() - 1, pos = 0;
while (l <= r) {
int mid = l + r >> 1;
dfs(u, son[u][mid]);
if (dp[u] <= k) pos = mid, l = mid + 1;
else r = mid - 1;
}
dfs(u, pos = son[u][pos]); ans.push_back(pos);
int v = id[pos]; //从最下面往上扣除代价
while (u != v) {
if (v & 1) k -= dp[v ^ 1];
else k -= min(dp[v ^ 1], w[v >> 1]); //兄弟和父节点
v >>= 1;
}
v = id[pos];
while (u != v) {
if (v & 1) k += dp[v ^ 1];
else k += min(dp[v ^ 1], w[v >> 1]); //兄弟和父节点
pair<long long, vector<int> > bro = solve(v ^ 1, k); //兄弟节点的递归
if (bro.second[0] < pos) {
k -= w[v >> 1];
bro = solve(v ^ 1, k); //强制往左
}
k -= bro.first;
v >>= 1;
for (int j : bro.second) ans.push_back(j);
}
return make_pair(qwq - k, ans);
}
int main() {
scanf("%d", &T);
while (T--) {
clear();
scanf("%d%lld", &n, &k); n = 1 << n, m = (n << 1) - 1;
for (int i = 1; i < n; i++) scanf("%lld", &w[i]);
for (int i = n; i <= m; i++) scanf("%d", &q[i]), id[q[i]] = i, Min[i] = q[i], son[i].push_back(q[i]);
for (int i = n - 1; i >= 1; i--) {
Min[i] = min(Min[i << 1], Min[i << 1 | 1]);
son[i] = son[i << 1];
for (int j : son[i << 1 | 1]) son[i].push_back(j);
sort(son[i].begin(), son[i].end());
}
vector<int> awa = solve(1, k).second;
for (int i : awa) printf("%d ", i); putchar('\n');
}
return 0;
}
//不开 long long 见祖宗

浙公网安备 33010602011771号