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;
}
posted @ 2025-02-09 21:05  小超手123  阅读(6)  评论(0编辑  收藏  举报