石像鬼与随机说话

这是杂谈系列,一般是我看到一些数学娱乐视频做出的信息学扩展与推导,不一定有具体对应题目。

经典回顾

石像鬼

考虑一个这样的问题,有三座石像,分别只会说真话、假话、以及会随机说话。

石像是全知的,或者说,至少它知道每一座石像的具体类型。

你每次可以询问一个石像一个命题,该命题必须有明确的是或否作为回答,该石像会遵从它的逻辑给你回答。

回答形如信息 $ P $ 或者 $ Q $,你初始不知道具体哪个信息代表是,哪个信息代表否。

你需要在最少的次数内区分它们。你可以认为随机说话的石像以阻止你区分为目的选择最优策略。

此问题有经典解法,这里给出一个 B 站链接,不在此赘述。

扩展

现在我们有 \(N=A+B+C\) 个石像鬼,有 \(A\) 只真话,\(B\) 只假话,\(C\) 只随机说话。

问问题的方式与可得到的信息同上。你需要在最少的次数内区分它们。

有解判定

有解当且仅当 \(A+B>C\)

\(A+B\leq C\),则随机说话的石像鬼可以以扮演策略让你永远无法区分。

考虑 \(A=B=1,C=2\),则你永远无法区分 \(\texttt{abcc}\)\(\texttt{ccab}\) 这两条世界线,前者的随机说话可以完全模拟后者对应位置的回答,而后者的随机说话完全模仿前者对应位置的回答。即无论怎么问这两条世界线的回答都一样。

\(A+B>C\),我们在这篇文章的最后给出一个询问 \(2N^2+N\) 次的构造性证明

最少次数

在保证有解的情况下,我们可以用 $ O(3^U) $ 左右的复杂度计算出最少的询问次数及其方案,这里 \(U=2\times \binom{N}{A}\binom{N-A}{B}\),总之这个复杂度不太是人类,我暂时没有思考其的优化。

做法如下:

我们只有 \(2\times \binom{N}{A}\binom{N-A}{B}\) 种可能的世界线(乘 \(2\) 是因为我们不知道信息 \(P\) 对应是/否),我们记集合 \(S\) 表示所有可能的世界线集合。

我们所有有效询问等价于选择一个集合 \(T\),问当前世界线 \(x\) 是否有 \(x\in T\)。而我们根据石像的回答,可以排除一些矛盾的世界线。不断排除最终确定当前世界线。

当然这个做法同样需要一个确定性的解作为支持,即存在一种确定的问法一定能得到答案。我们依然在最后给出。

值得一提的是,我们不需要知道具体的 \(A,B,C\),只需要知道 \(N=A+B+C\) 以及 \(A+B>C\) 即可,虽然会付出一些复杂度上的代价。

实现

比较懒,所以用 DeepSeek 生成了一下参考代码。

注意,我们只想区分石像,不需要区分出 \(P\)\(Q\) 这两种声音的实际含义。故终止状态不是 \(|S|=1\) 而是 \(S\) 中的世界线在石像类型上都一样。

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

int A, B, C, N, U;

struct State {
    vector<int> type; // 0:真话者, 1:假话者, 2:随机者
    int sem;          // 0: P=Yes, 1: P=No
    int id;           // 身份排列的ID (0~5)
};
vector<State> all_states;

void build_all_states() {
    vector<int> base;
    for (int i = 0; i < A; ++i) base.push_back(0);
    for (int i = 0; i < B; ++i) base.push_back(1);
    for (int i = 0; i < C; ++i) base.push_back(2);
    sort(base.begin(), base.end());

    set<vector<int>> unique_perms;
    do {
        unique_perms.insert(base);
    } while (next_permutation(base.begin(), base.end()));

    int id_counter = 0;
    for (const auto &p : unique_perms) {
        for (int s = 0; s < 2; ++s) {
            State st;
            st.type = p;
            st.sem = s;
            st.id = id_counter;
            all_states.push_back(st);
        }
        id_counter++;
    }
    U = (int)all_states.size();
}

// 询问 person,命题 S(状态子集)
// 返回 {回答Yes后的mask, 回答No后的mask}
pair<uint32_t, uint32_t> get_next(uint32_t mask, int person, uint32_t S) {
    uint32_t nxt_yes = 0, nxt_no = 0;
    for (int i = 0; i < U; ++i) {
        if (!(mask & (1u << i))) continue;
        const auto &st = all_states[i];
        bool proposition_true = (S >> i) & 1;

        bool answer_yes = false;
        if (st.type[person] == 0) {          // 真话者
            answer_yes = proposition_true;
        } else if (st.type[person] == 1) {   // 假话者
            answer_yes = !proposition_true;
        } else {                             // 随机者:两个分支都保留
            nxt_yes |= (1u << i);
            nxt_no  |= (1u << i);
            continue;
        }

        if (answer_yes) nxt_yes |= (1u << i);
        else            nxt_no  |= (1u << i);
    }
    return {nxt_yes, nxt_no};
}

// ★★★ 关键:判断是否已经区分出所有人(只看身份,不管语义)★★★
bool is_resolved(uint32_t mask) {
    int id_mask = 0;
    for (int i = 0; i < U; ++i) {
        if (mask & (1u << i)) {
            id_mask |= (1 << all_states[i].id);
        }
    }
    // 如果没有状态 或 只剩一种身份,就表示已经区分完毕
    return (id_mask == 0 || (id_mask & (id_mask - 1)) == 0);
}

map<uint32_t, int> memo;

int dfs(uint32_t mask) {
    if (is_resolved(mask)) return 0;
    if (memo.count(mask)) return memo[mask];

    int best = 1e9;
    for (int person = 0; person < N; ++person) {
        for (uint32_t S = 0; S < (1u << U); ++S) {
            auto pr = get_next(mask, person, S);
            uint32_t nxt_yes = pr.first;
            uint32_t nxt_no  = pr.second;

            // 必须保证两个分支都严格缩小,否则随机者恶意选择不变的分支导致死循环
            if (nxt_yes == mask || nxt_no == mask) continue;

            int cost = 1 + max(dfs(nxt_yes), dfs(nxt_no));
            best = min(best, cost);
        }
    }
    return memo[mask] = best;
}

int main() {
    A = 1; B = 1; C = 1;
    N = A + B + C;
    build_all_states();

    cout << "U = " << U << endl; // 12

    uint32_t full_mask = (1u << U) - 1; // 4095
    int ans = dfs(full_mask);
    cout << "Minimum questions = " << ans << endl; // 预期输出 3

    return 0;
}

构造性证明

考虑先区分出 \(P\)\(Q\) 的语义。

直接做投票,询问石像 \(i\) 命题【\(i\) 是说真话的人】。则只要它不是随机说话,一定会回答“是”。而 \(A+B>C\),回答次数较多的声音就代表“是”。

那么如何区分石像 \(x\) 的类型呢?

答案是继续做投票,询问石像 \(i\) 命题【(\(i\) 是说真话的人) \(\texttt{XOR}\) (\(x\) 是说真话的人)】,取多数声音作为结果,即可知道 \(x\) 是否说真话。

同理询问 \(N\) 次【(\(i\) 是说真话的人) \(\texttt{XOR}\) (\(x\) 是说假话的人)】即可知道 \(x\) 是否说假话。

故我们可以用 \(2N\) 次询问确定一个石像的类型,总询问次数 \(2N^2+N\)。不难发现这个次数很劣。

posted @ 2026-08-16 17:00  Kazemaru  阅读(73)  评论(4)    收藏  举报