石像鬼与随机说话
这是杂谈系列,一般是我看到一些数学娱乐视频做出的信息学扩展与推导,不一定有具体对应题目。
经典回顾

考虑一个这样的问题,有三座石像,分别只会说真话、假话、以及会随机说话。
石像是全知的,或者说,至少它知道每一座石像的具体类型。
你每次可以询问一个石像一个命题,该命题必须有明确的是或否作为回答,该石像会遵从它的逻辑给你回答。
回答形如信息 $ 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\)。不难发现这个次数很劣。

浙公网安备 33010602011771号