2026-04-26 某游戏厂暑期笔试复盘:三进制状态压缩 DP

Published 2026-04-26 04:00 1081 words 6 min read Handwritten Every word here was typed by me.

Zhiheng Wang avatar

Zhiheng Wang

技术文章、个人随笔、项目与作品体验记录

This post is not yet available in English. Showing the original.
2026-04-26 某游戏厂暑期笔试复盘:三进制状态压缩 DP 题目:景点与江湖秘闻 题目大意 有 $N$ 个景点、费用上限 $W$、$K$ 个江湖秘闻。$N\le2000$,$K\le10$,费用上限在 $10^9$ 量级。 每个景点有自己的花费,以及一个秘闻集合。每个景点只能游历一次。某个秘闻收集至少两次才生效,超过两次没有额外奖励。问费用不超过 $W$ 时,最多能让多少个秘闻生效。...

2026-04-26 某游戏厂暑期笔试复盘:三进制状态压缩 DP

题目:景点与江湖秘闻

题目大意

有 NN 个景点、费用上限 WW、KK 个江湖秘闻。N≤2000N\le2000,K≤10K\le10,费用上限在 10910^9 量级。

每个景点有自己的花费,以及一个秘闻集合。每个景点只能游历一次。某个秘闻收集至少两次才生效,超过两次没有额外奖励。问费用不超过 WW 时,最多能让多少个秘闻生效。

按景点可以独立选择的模型来做:费用非负、直接累加,每个景点对集合中的每个秘闻贡献一次。

思路分析

我当时想到用三进制压状态。每一位表示一个秘闻:0 次、1 次、至少 2 次。次数超过 2 没有用,所以截到 2 即可,一共有 3K3^K 个状态。

费用上限很大,不能按费用开数组。用 dp[s] 记录达到状态 ss 的最小费用,初始 dp[0]=0。逐个处理景点,每次选择不去,或付出费用后把集合内各位加 1,并截到 2。最后在费用不超过 WW 的状态里,找三进制位为 2 最多的那个。

如果每次转移都拆开 KK 位,就要做 O(N3KK)O(N3^KK) 次操作。K=10K=10 时已经有 5904959049 个状态,再把除法、取模放到最内层,开销会很大。

可以把“哪些位已经是 2”提前存下来。设 saturated[s] 是状态 ss 中已经为 2 的那些位组成的二进制集合,景点的集合是 mask。这次真正需要增加的位就是

add = mask & ~saturated[s]

再预处理 weight[mask],表示集合内所有位对应的 3b3^b 之和。新状态直接是

next = s + weight[add]

因为只给原来为 0 或 1 的三进制位加 1,不会产生进位。已经为 2 的位又被排除了,所以这就等于逐位加一并截到 2。

这样每次转移只需要位运算和查表,不必保存 trans[mask][state] 那张大表。预处理是 O(3K+2K)O(3^K+2^K),DP 时间是 O(N3K)O(N3^K),空间是 O(3K+2K)O(3^K+2^K)。

每个景点只能用一次,可以按状态编号倒序更新同一个 dp 数组:新状态不会比原状态小,倒序能防止本轮刚更新出的更大状态再次使用同一景点。如果新旧状态相等,非负费用也不会让最小费用变得更小。

参考代码

费用用 long long,只保留不超过预算的转移。下面是优化后的核心函数。

#include <bits/stdc++.h>
using namespace std;

// 每个元素为 {费用, 秘闻集合的二进制 mask}。
// 秘闻编号已转换成 0..K-1;每景点对集合内各秘闻贡献一次。
int max_active_rumors(int K, long long W,
                      const vector<pair<long long, int>>& spots) {
    vector<int> pow3(K + 1, 1);
    for (int b = 0; b < K; ++b) pow3[b + 1] = pow3[b] * 3;
    int S = pow3[K];
    int M = 1 << K;

    // weight[mask] = mask 中各位对应的 3^b 之和。
    vector<int> weight(M, 0);
    for (int mask = 1; mask < M; ++mask) {
        int b = __builtin_ctz((unsigned)mask);
        weight[mask] = weight[mask & (mask - 1)] + pow3[b];
    }

    // saturated[s]:状态 s 中三进制位为 2 的那些秘闻。
    vector<int> saturated(S, 0);
    for (int s = 1; s < S; ++s)
        saturated[s] = (saturated[s / 3] << 1) | (s % 3 == 2);

    const long long INF = (1LL << 62);
    vector<long long> dp(S, INF);
    dp[0] = 0;
    for (auto [cost, mask] : spots) {
        if (cost > W) continue;
        // 选景点后状态编号只会增大;倒序更新避免重复选它。
        for (int s = S - 1; s >= 0; --s) {
            if (dp[s] == INF || dp[s] > W - cost) continue;
            int add = mask & ~saturated[s];
            int next = s + weight[add];
            dp[next] = min(dp[next], dp[s] + cost);
        }
    }

    int answer = 0;
    for (int s = 0; s < S; ++s)
        if (dp[s] <= W)
            answer = max(answer, __builtin_popcount((unsigned)saturated[s]));
    return answer;
}

转移表的空间开销

另一个办法是把 trans[mask][state] 全部预处理出来,之后直接查表。但 K=10K=10 时,这张表有 210×310=604661762^{10}\times3^{10}=60466176 项,用 int 存约占 231MiB。只给实际出现的 mask 建表可以节省一些空间,最坏仍可能出现全部 2K2^K 种。

也可以只保存每个状态的三进制位,转移时遍历 mask 的置位。这样避开了反复除法和取模,但一次转移仍要遍历若干位。上面的 saturated + weight 则把这一步也变成了位运算和查表。

Written by Handwritten

Random Posts

© 2026 Zhiheng Wang @traceonsnow
Powered by theme astro-koharu · Inspired by Shoka
Loading...