P10220 [省选联考 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\) 的最终取值。
\(1\le T \le 100\);\(1\le n \le 16\),\(1 \le \sum 2^n \le 10^5\);\(K,w \le 10^{12}\)。
分析:
好有意思的博弈论!去年省选 \(T1\) 都这么难吗?看来今年省选想 AC 两天 \(T1\) 还是有点痴心妄想了。
首先考虑确定 \(Q_{1}\) 的值。由于 \(K\) 达到了 \(10^{12}\),肯定无法放到状态里了。容易发现 \(Alice\) 的操作就是删掉右子树,使得最后剩下的叶子的最小值最大。题做的多的同学(不包括我)就会迅速设计出一个 dp 状态,\(f_{u,j}\) 表示使 \(u\) 子树内最小值 \(\ge j\) 的最小魔力值花费。由于是棵满二叉树,总状态数只有 \(n2^{n}\) 个。转移显然是 \(f_{u,j}=f_{ls,j}+\min(f_{rs,j},w_{u})\)。求出 \(f\) 后,找到最大 \(j\) 满足 \(f_{1,j} \le K\),\(Q_{1}\) 就等于 \(j\)。
接下来思考如何计算 \(Q\) 的所有值。尝试模拟 Bob 的行走。设计函数 dfs(u,K)
表示 Bob 现在在 \(u\),Alice 还有 \(K\) 的魔力值,函数返回 Alice 在使 \(u\) 子树内字典序尽可能大消耗的魔力值。
同样先找到最大 \(j\) 满足 \(f_{u,j} \le K\)。
如果 \(j\) 在右子树,Alice 一定不会选择 \(w_{u}\)。为了让 Bob 首先会进入右子树,Alice 在左子树至少要花费 \(f_{ls,j}\) 的魔力值(这样 Bob 如果先走左子树一定会更劣)。留给 Alice 在右子树进行的魔力值仅有 \(K-f_{ls,j}\)。所以先调用 g=dfs(rs,K-f[ls][j])
。然后 Bob 会走左子树,再调用 dfs(ls,K-g)
。由于给 Alice 在左子树留够了预算,所以这个 dfs(ls,K-g)
跑下来第一个访问的叶子一定大于 \(j\)。算法的正确性成立。
如果 \(j\) 在左子树,为了让 Bob 首先会进入左子树,Alice 在右子树和 \(u\) 至少要花费 \(\min(f_{rs,j},w_{u})\)。所以先调用 g=dfs(ls,K-min(f[rs][j], w[u]))
。然后考虑唤不唤醒 \(u\),显然能不唤醒就不唤醒。由于留给 Alice 在右子树的预算为 \(K-g\),如果 \(K-g < f_{rs,j}\),说明 Alice 无法保证 Bob 在右子树访问的第一个叶子大于 \(j\),也就无法保证 Bob 会首先进入左子树。此时就只能唤醒,调用 dfs(rs,K-g-w[u])
。否则调用 dfs(rs,K-g)
。
预处理 \(f\) 时间复杂度 \(O(n2^{n})\)(利用归并排序)。计算答案时间复杂度 \(O(n2^{n})\)。总时间复杂度 \(O(n2^{n})\)。
代码:
#include<bits/stdc++.h>
#define int long long
#define N 200005
#define ls (u * 2)
#define rs (u * 2 + 1)
using namespace std;
int n, T, K;
int w[N], q[N];
struct node {
int j, Num, opt;
};
vector<node>f[N], h;
vector<int>ans;
int Get_f(int u, int j) {
int res = 1e18;
for(auto x : f[u]) {
if(x.j >= j) {
res = x.Num;
break;
}
}
return res;
}
void dfs1(int u) {
if(u >= (1 << n)) {
f[u].push_back((node){q[u], 0, 0});
return;
}
dfs1(ls); dfs1(rs);
int z = -1, lst0 = -1, lst1 = -1; h.clear();
for(auto x : f[rs]) {
while(z + 1 < f[ls].size() && f[ls][z + 1].j <= x.j) h.push_back((node){f[ls][z + 1].j, f[ls][z + 1].Num, 0}), z++;
h.push_back((node){x.j, x.Num, 1});
}
for(int i = z + 1; i < f[ls].size(); i++) h.push_back((node){f[ls][i].j, f[ls][i].Num, 0});
for(int i = h.size() - 1; i >= 0; i--) {
if(h[i].opt == 0) f[u].push_back((node){h[i].j, h[i].Num + min((lst1 != -1 ? h[lst1].Num : (int)1e18), w[u]), 0});
else if(lst0 != -1) f[u].push_back((node){h[i].j, h[lst0].Num + min(h[i].Num, w[u]), 1});
if(h[i].opt == 0) lst0 = i;
else lst1 = i;
}
reverse(f[u].begin(), f[u].end());
}
int dfs2(int u, int K) {
if(u >= (1 << n)) {
ans.push_back(q[u]);
return 0;
}
int j = 0, v = 0, g = 0;
for(auto x : f[u]) {
if(x.Num > K) break;
j = x.j; v = x.opt;
}
if(v == 0) {
int Get = Get_f(rs, j);
g = dfs2(ls, K - min(Get, w[u]));
if(K - g < Get) g += dfs2(rs, K - g - w[u]) + w[u];
else g += dfs2(rs, K - g);
}
else {
int Get = Get_f(ls, j);
g = dfs2(rs, K - Get);
g += dfs2(ls, K - g);
}
return g;
}
void Sol() {
cin >> n >> K;
for(int i = 1; i <= (1 << n) - 1; i++) cin >> w[i];
for(int i = (1 << n); i < (1 << (n + 1)); i++) cin >> q[i];
dfs1(1); int u = dfs2(1, K);
for(auto x : ans) cout << x << " "; cout << endl;
for(int i = 1; i < (1 << (n + 1)); i++) f[i].clear(); ans.clear();
}
signed main() {
ios::sync_with_stdio(0);
cin.tie(0); cout.tie(0);
cin >> T;
while(T--) Sol();
return 0;
}