洛谷 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 见祖宗
posted @ 2024-03-23 16:41  Conan15  阅读(13)  评论(0)    收藏  举报  来源