引言
“海盗分金”是博弈论中最经典的逻辑推理谜题之一:五名海盗抢到100枚金币,他们按等级从高到低依次提出分配方案。如果方案被半数或以上海盗接受,就按方案分配;否则提出者被扔下海,由下一位海盗继续提案。假设所有海盗都绝顶聪明、理性且优先追求保命,其次多捞金币,再次乐于看到别人被扔下海。
这个看似简单的规则设定,隐藏着深刻的博弈论原理:逆向归纳法(Backward Induction)。本文将用Java实现这一经典推理过程,展示如何通过从终局向起点逆向推导,得出反直觉的最优分配方案。
问题建模
玩家属性
每名海盗有三个决策优先级(按重要性降序):
1. 保命:活下去是最高优先级
2. 多金:在保命前提下,尽可能多获得金币
3. 嗜血:在前两者相同的情况下,倾向于把人扔下海
投票规则
对于 N 名海盗的方案,需要至少 ⌈N/2⌉ 票(包括提案者自己)才能通过。
核心思想:逆向归纳
不从第一名海盗开始思考,而是从只剩最后一名海盗的”终局”开始,逐步向前推导。因为在每个子博弈中,理性玩家的选择是确定的。
算法设计
状态表示
用 (pirates, coins, startIndex) 表示状态:
– pirates:当前存活的海盗列表(按等级从高到低排序)
– coins:剩余金币数
– startIndex:当前轮到提案的海盗在列表中的索引
逆向归纳递推
对于每个状态,当前提案者需要决定如何分配金币,使得足够数量的海盗投赞成票。
关键观察:每个海盗投赞成票的条件是——接受该方案后的收益 > 拒绝该方案后的收益。如果收益相等,由于嗜血属性,他们会选择拒绝。
因此,提案者需要精确计算每个海盗在”自己死后”的子博弈中能获得的收益,然后用更优的报价收买他们。
Java实现
import java.util.*;
/**
* 海盗分金博弈求解器
* 使用逆向归纳法计算N名海盗分M枚金币的最优分配方案
*/
public class PirateGoldSolver {
/**
* 海盗类,包含等级、姓名等属性
*/
static class Pirate {
final int rank; // 等级,数字越小等级越高(0为最高)
final String name; // 海盗名称
public Pirate(int rank, String name) {
this.rank = rank;
this.name = name;
}
@Override
public String toString() {
return name + "(等级" + rank + ")";
}
}
/**
* 分配方案结果
*/
static class Allocation {
final int[] distribution; // 每个海盗分得的金币
final boolean passes; // 方案是否通过
final String reasoning; // 推理过程描述
public Allocation(int[] distribution, boolean passes, String reasoning) {
this.distribution = distribution;
this.passes = passes;
this.reasoning = reasoning;
}
}
private final Map<String, Allocation> memo = new HashMap<>();
/**
* 求解海盗分金问题
* @param numPirates 海盗数量
* @param totalCoins 金币总数
* @return 最优分配方案
*/
public Allocation solve(int numPirates, int totalCoins) {
List<Pirate> pirates = new ArrayList<>();
for (int i = 0; i < numPirates; i++) {
pirates.add(new Pirate(i, "海盗" + (i + 1)));
}
return solveRecursive(pirates, totalCoins, 0);
}
/**
* 递归求解子博弈
* @param pirates 当前存活的海盗列表(按等级排序)
* @param coins 剩余金币
* @param proposerIndex 当前提案者的索引
* @return 最优分配方案
*/
private Allocation solveRecursive(List<Pirate> pirates, int coins, int proposerIndex) {
// 状态编码用于记忆化
String stateKey = encodeState(pirates, coins, proposerIndex);
if (memo.containsKey(stateKey)) {
return memo.get(stateKey);
}
int n = pirates.size();
// 终局情况:只剩一名海盗
if (n == 1) {
int[] dist = new int[]{coins};
String reason = pirates.get(0).name + "独揽全部" + coins + "枚金币";
Allocation result = new Allocation(dist, true, reason);
memo.put(stateKey, result);
return result;
}
// 计算需要的赞成票数(至少半数,向上取整)
int requiredVotes = (n + 1) / 2;
// 当前提案者
Pirate proposer = pirates.get(proposerIndex);
// 计算如果当前提案者被扔下海,每个海盗在子博弈中的收益
Map<Pirate, Integer> futurePayoff = new HashMap<>();
for (int i = 0; i < n; i++) {
if (i != proposerIndex) {
// 模拟移除当前提案者后的子博弈
List<Pirate> remaining = new ArrayList<>(pirates);
remaining.remove(proposerIndex);
// 下一个提案者是当前列表中索引为 proposerIndex 的海盗
// 如果proposerIndex是最后一个,则下一个是0;否则保持proposerIndex
int nextProposer = proposerIndex >= remaining.size() ? 0 : proposerIndex;
Allocation future = solveRecursive(remaining, coins, nextProposer);
// 找到该海盗在子博弈中的收益
// 注意:remaining列表中索引可能变化,需要按rank匹配
Pirate p = pirates.get(i);
int futureIdx = -1;
for (int j = 0; j < remaining.size(); j++) {
if (remaining.get(j).rank == p.rank) {
futureIdx = j;
break;
}
}
int payoff = (futureIdx >= 0) ? future.distribution[futureIdx] : 0;
futurePayoff.put(p, payoff);
}
}
// 提案者需要收买 (requiredVotes - 1) 名海盗
// 策略:收买那些"未来收益最低"的海盗,因为收买他们的成本最低
// 每个被收买者需要获得:futurePayoff + 1(严格大于未来收益)
// 按未来收益升序排序(优先收买未来收益低的)
List<Map.Entry<Pirate, Integer>> sorted = new ArrayList<>(futurePayoff.entrySet());
sorted.sort(Comparator.comparingInt(Map.Entry::getValue));
// 尝试构造方案:收买刚好足够的海盗
int[] distribution = new int[n];
Arrays.fill(distribution, 0);
// 提案者自己 always 投赞成票
int votes = 1;
int remainingCoins = coins;
// 需要收买的海盗列表
List<Pirate> toBribe = new ArrayList<>();
int bribeCost = 0;
for (int i = 0; i < sorted.size() && votes < requiredVotes; i++) {
Pirate p = sorted.get(i).getKey();
int future = sorted.get(i).getValue();
int bribe = future + 1; // 必须严格大于未来收益
if (bribeCost + bribe <= coins) {
toBribe.add(p);
bribeCost += bribe;
votes++;
} else {
// 金币不足以收买这名海盗,尝试下一个
continue;
}
}
// 如果无法凑够票数,方案不通过
if (votes < requiredVotes) {
String reason = proposer.name + "无法凑够" + requiredVotes + "票,被扔下海";
Allocation result = new Allocation(new int[n], false, reason);
memo.put(stateKey, result);
return result;
}
// 构造分配方案
for (Pirate p : toBribe) {
int idx = -1;
for (int i = 0; i < n; i++) {
if (pirates.get(i).rank == p.rank) {
idx = i;
break;
}
}
if (idx >= 0) {
int future = futurePayoff.get(p);
distribution[idx] = future + 1;
remainingCoins -= distribution[idx];
}
}
// 提案者拿走剩余金币
distribution[proposerIndex] = remainingCoins;
// 构建推理说明
StringBuilder reason = new StringBuilder();
reason.append(proposer.name).append("提出方案:");
for (int i = 0; i < n; i++) {
reason.append(pirates.get(i).name).append("=").append(distribution[i]).append(" ");
}
reason.append("\n收买逻辑:");
for (Pirate p : toBribe) {
reason.append(p.name).append("未来只能得").append(futurePayoff.get(p))
.append(",现给").append(futurePayoff.get(p) + 1).append(";");
}
Allocation result = new Allocation(distribution, true, reason.toString());
memo.put(stateKey, result);
return result;
}
/**
* 编码状态用于记忆化
*/
private String encodeState(List<Pirate> pirates, int coins, int proposerIndex) {
StringBuilder sb = new StringBuilder();
sb.append(coins).append("_").append(proposerIndex).append("_");
for (Pirate p : pirates) {
sb.append(p.rank).append(",");
}
return sb.toString();
}
/**
* 打印完整的逆向推理过程
*/
public void printReasoning(int numPirates, int totalCoins) {
System.out.println("=== 海盗分金逆向归纳推理 ===\n");
System.out.println("海盗数量:" + numPirates);
System.out.println("金币总数:" + totalCoins);
System.out.println("投票规则:至少半数赞成(含提案者)\n");
// 从终局开始逐步展示
for (int n = 1; n <= numPirates; n++) {
System.out.println("--- 当剩余 " + n + " 名海盗时 ---");
Allocation alloc = solve(n, totalCoins);
System.out.println(alloc.reasoning);
System.out.println("方案通过:" + alloc.passes);
if (alloc.passes) {
System.out.print("分配结果:");
for (int i = 0; i < alloc.distribution.length; i++) {
System.out.print("海盗" + (i + 1) + "=" + alloc.distribution[i] + " ");
}
}
System.out.println("\n");
}
}
/**
* 主程序:运行经典5海盗100金币问题
*/
public static void main(String[] args) {
PirateGoldSolver solver = new PirateGoldSolver();
// 经典问题:5名海盗,100枚金币
int numPirates = 5;
int totalCoins = 100;
System.out.println("海盗分金问题求解器\n");
System.out.println("规则:");
System.out.println("1. 按等级从高到低依次提案");
System.out.println("2. 需要至少半数(含提案者)赞成");
System.out.println("3. 优先级:保命 > 多金 > 嗜血\n");
solver.printReasoning(numPirates, totalCoins);
// 最终方案
Allocation finalAlloc = solver.solve(numPirates, totalCoins);
System.out.println("=== 最终最优方案 ===");
System.out.println(finalAlloc.reasoning);
System.out.print("金币分配:");
for (int i = 0; i < finalAlloc.distribution.length; i++) {
System.out.print("海盗" + (i + 1) + "=" + finalAlloc.distribution[i] + " ");
}
}
}
经典结论分析
对于5名海盗分100枚金币的经典问题,逆向归纳的推理链如下:
终局分析
- 1人局:海盗5独得100枚
- 2人局:海盗4提案,需要2票(全票)。海盗5知道若海盗4死则自己得100,因此会反对任何不给100的方案。海盗4无法收买海盗5,方案不通过,海盗4被扔下海。海盗5得100。
- 3人局:海盗3提案,需要2票。海盗4知道若海盗3死则自己也会死(2人局分析),因此只要给海盗4 1枚(大于0),海盗4就会支持。海盗3得99,海盗4得1,海盗5得0。
- 4人局:海盗2提案,需要2票。海盗3在3人局中得99,海盗4得1,海盗5得0。海盗2只需收买海盗5(给1枚)即可凑够2票。分配:海盗2得99,海盗3得0,海盗4得0,海盗5得1。
- 5人局:海盗1提案,需要3票。海盗2在4人局中得99,海盗3得0,海盗4得0,海盗5得1。海盗1只需收买海盗3和海盜4(各给1枚)即可凑够3票。分配:海盗1得98,海盗2得0,海盗3得1,海盗4得1,海盗5得0。
反直觉的结论
最高等级的海盗1不仅活了下来,还拿走了98枚金币!这个结果完全违背直觉,却是理性博弈的必然结果。关键在于: proposal者利用了自己”先下手为强”的优势,以及对下游海盗命运的掌控力。
复杂度分析
- 状态数:
O(N!)种海盗存活组合,但由于本题中海盗按固定顺序被淘汰,实际状态数为O(N^2)(子问题规模从1到N)。 - 单次决策:对于N名海盗,需要分析N-1名海盗的子博弈,并排序
O(N log N)。 - 总时间复杂度:
O(N^3),对于小规模问题(N < 100)瞬间完成。 - 空间复杂度:
O(N^2)用于记忆化存储。
扩展思考
- 非线性效用:如果海盗的金币效用不是线性的(例如边际效用递减),收买策略会如何变化?
- 联盟与背叛:允许多轮谈判和临时联盟时,核心(Core)和沙普利值(Shapley Value)如何应用?
- 信息不完全:如果海盗不知道彼此的优先级排序,问题变为贝叶斯博弈。
- 动态海盗:如果新海盗可以不断加入,形成无限重复博弈,稳定的分配规则是什么?
总结
海盗分金问题以极简的规则展示了博弈论的精髓:理性参与者通过预测他人的理性行为,逆向推导出当前的最优策略。Java实现中的递归结构和记忆化技术,完美映射了逆向归纳法的数学本质。
这个结果也提醒我们:在层级结构中,高位者拥有不成比例的议价能力——不是因为他们更强壮,而是因为他们掌握了”提出方案”的结构性优势。这在公司治理、政治博弈乃至日常谈判中都有深刻的现实映照。