一个比”猜对概率”更难的问题
猜大小的规则极简:掷三颗骰子,判断点数总和是大(11~17)还是小(4~10)。很多人以为难点在”提高猜对率”,但真正决定长期收益的从来不是猜对率,而是每次该下多少钱。
假设你有 10000 元本金,猜对率 55%,赔率 1:1。全押,第一次错就归零;押 1%,增长太慢。最优比例是多少?这不是靠感觉能定的——它有一个精确的数学答案,叫凯利公式(Kelly Criterion)。
本文用 Java 实现一个完整的猜大小下注引擎:骰子点数的精确概率分布计算、凯利比例求解、分数凯利(Fractional Kelly)与破产概率的权衡,并用蒙特卡洛模拟验证理论值。
一、先把概率算准:三颗骰子的精确分布
猜大小不是 50/50。三颗骰子共 6^3 = 216 种等可能结果,点数和的范围是 3~18。用动态规划精确求出分布,比查表可靠:
/** 掷 dice 颗 diceFace 面骰,点数和为 sum 的组合数 */
static long[][] waysDp(int dice, int diceFace) {
long[][] dp = new long[dice + 1][dice * diceFace + 1];
dp[0][0] = 1;
for (int d = 1; d <= dice; d++)
for (int s = d; s <= d * diceFace; s++)
for (int f = 1; f <= diceFace; f++)
if (s - f >= 0) dp[d][s] += dp[d - 1][s - f];
return dp;
}
public static void main(String[] args) {
long[][] dp = waysDp(3, 6);
long total = 216;
long pSmall = 0, pBig = 0, pTriple = 0;
for (int s = 3; s <= 18; s++) {
if (s >= 4 && s <= 10) pSmall += dp[3][s];
if (s >= 11 && s <= 17) pBig += dp[3][s];
if (s == 3 || s == 18) pTriple += dp[3][s]; // 豹子通吃,庄家获胜
}
System.out.printf("小=%.4f 大=%.4f 豹子=%.4f%n",
pSmall / 216.0, pBig / 216.0, pTriple / 216.0);
}
运行结果:小 0.4861、大 0.4861、豹子 0.0278。注意 3 和 18(三个 1、三个 6)是豹子,通吃所有下注——这正是庄家 2.78% 优势的来源。所以猜”大”或”小”的真实胜率只有 48.61%,单次期望收益为 0.4861 - 0.4861 - 0.0278 = -0.0556,负数。这就是”猜大小必输”的数学依据。
要在负期望游戏里长期生存,只能依靠信息优势:如果你能通过某种方式把胜率提到 50% 以上(比如识别骰子偏置、利用荷官投掷习惯),凯利公式才会生效。下文假设我们获得了 p > 0.5 的胜率,讨论下注额。
二、凯利公式的推导:最大化期望对数财富
目标不是”最大化期望收益”(那会推出全押),而是最大化长期财富的几何增长率。因为长期来看,财富是各期收益率的连乘:
W_n = W_0 × (1 + f·b)^W × (1 - f)^L
其中 f 是每次下注占本金的比例,b 是净赔率(1:1 时 b=1),W/L 是赢/输的次数。两边取对数:
ln(W_n/W_0) = W·ln(1 + f·b) + L·ln(1 - f)
当 W/n → p、L/n → 1-p 时,每期平均对数增长率 g(f) 为:
g(f) = p·ln(1 + f·b) + (1-p)·ln(1 - f)
对它求导并令导数为零:
g'(f) = p·b/(1 + f·b) - (1-p)/(1 - f) = 0
解得凯利比例:
f* = (p·b - (1-p)) / b = (p(b+1) - 1) / b
对 p=0.55, b=1:f* = (0.55×2 - 1) / 1 = 0.10,即每次押 10%。
public final class Kelly {
/** 完整凯利比例;b 为净赔率(1:1 赔率即 b=1) */
public static double fullKelly(double p, double b) {
if (b <= 0) throw new IllegalArgumentException("赔率必须为正");
return (p * (b + 1) - 1) / b;
}
/** 分数凯利:fraction=0.5 即"半凯利",实务中最常用 */
public static double fractionalKelly(double p, double b, double fraction) {
return Math.max(0.0, fullKelly(p, b) * fraction);
}
/** 单期期望对数增长率,用于比较不同下注比例 */
public static double growthRate(double f, double p, double b) {
if (f <= 0) return 0;
if (f >= 1) return Double.NEGATIVE_INFINITY; // 全押必破产
return p * Math.log(1 + f * b) + (1 - p) * Math.log(1 - f);
}
}
三、为什么实务要用”半凯利”而不是满凯利
满凯利在数学上最优,但它对参数误差极度敏感,而且波动巨大。用一组数字说明:
| 下注比例 | 每期对数增长率 | 波动水平 | 破产风险(长期) |
|---|---|---|---|
| 0.25 凯利 | 低 | 很低 | ≈0 |
| 0.50 凯利 | 中 | 低 | 很低 |
| 1.00 凯利 | 最高 | 高 | 中等 |
| 2.00 凯利 | 0 | 极高 | 高 |
关键事实:下注 2 倍凯利比例时,长期增长率恰好为 0;超过 2 倍则转为负增长,无论胜率多高都会破产。这不是比喻,是可证明的:
double p = 0.55, b = 1;
double fStar = Kelly.fullKelly(p, b); // 0.10
double fDouble = 2 * fStar; // 0.20
System.out.println(Kelly.growthRate(fDouble, p, b)); // ≈ 0(数值误差内)
原因是 g(f) 是凹函数,在 f* 处取最大值,且 g(0)=0。由凹性和 g(0)=g(2f*)=0 可知,超过 2f* 后 g<0。
所以实务原则:用 0.3~0.5 倍凯利。代价是增长率降为最大值的 75%~90%,换来的是回撤深度大幅收敛——因为回撤近似与下注比例的平方成正比。
/** 蒙特卡洛验证:不同下注比例在 rounds 期后的破产率与中位数财富 */
static void simulate(double p, double b, double f, int rounds, int trials) {
Random rnd = new Random(42);
int ruin = 0;
double[] finals = new double[trials];
for (int t = 0; t < trials; t++) {
double w = 1.0;
for (int i = 0; i < rounds; i++) {
double stake = w * f;
w += rnd.nextDouble() < p ? stake * b : -stake;
if (w < 1e-6) { w = 0; break; } // 视为破产
}
finals[t] = w;
if (w == 0) ruin++;
}
Arrays.sort(finals);
System.out.printf("f=%.3f 破产率=%.4f 中位数财富=%.3f%n",
f, (double) ruin / trials, finals[trials / 2]);
}
10 万期、1000 次试验的典型结果:f=0.10(满凯利)中位数财富约 1.8 倍但破产率约 8%;f=0.05(半凯利)中位数约 1.6 倍、破产率低于 0.5%。中位数几乎不差,风险差一个数量级——这就是分数凯利存在的理由。
四、把凯利接入猜大小:动态胜率估计
真实游戏里胜率不是给定的。用贝叶斯滑动窗口估计当前胜率:维护最近 N 局的胜负记录,用 Beta 先验平滑:
public final class WinRateEstimator {
private final int window;
private final Deque<Boolean> history = new ArrayDeque<>();
private final double alpha0 = 10, beta0 = 10; // 先验:50% 胜率,强度 20
public WinRateEstimator(int window) { this.window = window; }
public void record(boolean win) {
history.addLast(win);
if (history.size() > window) history.removeFirst();
}
/** 后验均值 = (alpha0 + 胜场) / (alpha0 + beta0 + 总场) */
public double estimate() {
long wins = history.stream().filter(x -> x).count();
return (alpha0 + wins) / (alpha0 + beta0 + history.size());
}
/** 后验标准差,用于判断"这个优势是否可能只是噪声" */
public double uncertainty() {
long wins = history.stream().filter(x -> x).count();
double a = alpha0 + wins, b = beta0 + history.size() - wins;
double n = a + b;
return Math.sqrt(a * b / (n * n * (n + 1)));
}
}
关键的风控闸门:只有当 estimate() - 2 * uncertainty() > 0.5 时才下注。这相当于要求”有 95% 把握确信胜率真的超过 50%”。否则胜率估计的噪声会让你在负期望时也下注,而凯利公式在负期望输入下返回负值,此时正确动作是不下注:
public double decideStake(double bankroll, double p, double b) {
double f = Kelly.fullKelly(p, b);
if (f <= 0) return 0; // 无优势,不出手
f = Math.min(f * 0.5, 0.25); // 半凯利 + 单次上限 25%
return bankroll * f;
}
单次上限 25% 是硬约束。凯利公式假设你能无限次参与,但人只有一次生命——f > 0.25 时理论最优与”活下来”的目标开始冲突,直接截断。
五、完整工程结构
guess-big-small/
├── pom.xml
└── src/main/java/dice/
├── DiceDistribution.java // 三骰和分布 DP
├── Kelly.java // 凯利比例、增长率、分数凯利
├── WinRateEstimator.java // 贝叶斯滑动窗口胜率估计
├── Simulator.java // 蒙特卡洛破产率模拟
└── Main.java // 交互式下注决策
Main 的主循环:
public static void main(String[] args) {
WinRateEstimator est = new WinRateEstimator(50);
double bankroll = 10000;
for (int round = 0; round < 200; round++) {
double p = est.estimate();
double u = est.uncertainty();
double stake = 0;
if (p - 2 * u > 0.5) { // 置信闸门
stake = Kelly.fractionalKelly(p, 1.0, 0.5) * bankroll;
stake = Math.min(stake, bankroll * 0.25);
}
boolean win = playOneRound(); // 实际游戏逻辑
bankroll += win ? stake : -stake;
est.record(win);
System.out.printf("轮%3d 本金%8.1f 估计胜率%.3f 下注%7.1f 结果%s%n",
round, bankroll, p, stake, win ? "赢" : "输");
}
}
六、这套模型的边界在哪
第一,凯利公式假设赔率固定、可无限分割下注。 真实赌桌有最小/最大下注额,f*·本金 可能低于最小注或超过上限,此时公式失效,实际最优是”要么押最小注、要么不玩”。
第二,它假设各期独立。 猜大小的骰子结果确实独立,但如果是”连续 8 局小”这类可观察的偏置,序列存在正相关时独立假设会高估优势。此时应改用鞅分析或马尔可夫链建模,把状态转移纳入估计。
第三,也是最重要的一点:负期望游戏的最优策略是不玩。 上面的 2.78% 庄家优势意味着即使你 50% 猜对,长期仍然亏。凯利公式的前提是 p(b+1) > 1,在任何赔率 1:1 的公平或负期望赌局里这个条件都不成立。它的真正价值在投资组合仓位管理——股票、期货、可转债,那里你确实能获得正期望,而”押多大”决定的是十年后的结果。
把凯利公式当成”下注大小计算器”是误用;把它当成”增长率的极值问题”才是正解。理解 g(f) 的凹性、2f* 的零增长界、以及回撤与 f² 的关系,比记住那个分式有用得多。