二十一点站定后胜负平概率计算Java代码错误排查
二十一点(Blackjack)概率计算代码问题排查
你的代码在计算玩家赢/平/输概率时,部分测试用例结果偏差(如示例2预期39.9%赢、0%平、60.0%输,实际返回41.8%赢、0%平、58.1%输),核心原因是几个关键逻辑错误,以下是问题分析和修正方案:
核心问题分析
1. 未处理庄家软牌(A牌点数切换)逻辑
庄家规则中,当手牌包含A时(软牌),若总和超过21,会将A的点数从11切换为1避免爆牌。但你的代码直接将dealer > 21判定为爆牌,忽略了软牌调整的可能性:
- 例如庄家初始为11(A),抽一张10后总和变为22,此时应调整为1+10=11而非直接判定爆牌。
- 这种错误会导致代码高估玩家赢的概率,是示例2结果偏差的主要原因。
2. A牌分支处理错误
抽到A时,你同时计算加1和加11的两种情况并累加结果,不符合庄家实际决策逻辑:
- 庄家拿到A时优先按11计算(只要不爆牌),只有加11会爆牌时才按1计算,而非同时考虑两种情况。
- 重复计算导致
count变量错误增加,概率计算的分母偏大,进一步扭曲结果。
3. 记忆化缓存Key设计不合理
使用deck.toString()作为缓存Key的一部分,会导致:
- 相同牌组成但不同顺序的牌组被视为不同状态,增加不必要的重复计算。
- 字符串拼接的Key容易出现冗余或冲突,无法有效复用缓存结果。
4. 平局场景逻辑遗漏
虽然玩家Blackjack对庄家非Blackjack的判定正确,但受前面的错误影响,其他平局场景的概率计算出现偏差。
修正后的代码
import java.util.*; public class Main { // 用各点数计数作为缓存Key,避免牌组顺序影响 private static final Map<String, double[]> memo = new HashMap<>(); public static void main(String[] args) { Scanner scanner = new Scanner(System.in); String playerTotalString = scanner.next(); int playerTotal; boolean isPlayerBlackjack = playerTotalString.equals("B"); playerTotal = isPlayerBlackjack ? 21 : Integer.parseInt(playerTotalString); int dealerInitialCard = scanner.nextInt(); // 统计各点数剩余数量(1=A,10=10/J/Q/K) int[] cardCounts = new int[11]; int totalCards = 0; for (int i = 1; i <= 10; i++) { cardCounts[i] = scanner.nextInt(); totalCards += cardCounts[i]; } scanner.close(); // 庄家初始状态:A牌按11计算,标记软牌 int dealerSum = dealerInitialCard == 1 ? 11 : dealerInitialCard; boolean dealerHasAce = dealerInitialCard == 1; int dealerCardCount = 1; double[] result = calculateProbability(cardCounts, totalCards, playerTotal, isPlayerBlackjack, dealerSum, dealerHasAce, dealerCardCount); // 格式化输出赢、平、输概率 System.out.printf("%.1f%%\n", Math.floor(result[0] * 1000) / 10); System.out.printf("%.1f%%\n", Math.floor(result[1] * 1000) / 10); System.out.printf("%.1f%%\n", Math.floor(result[2] * 1000) / 10); } /** * 递归计算概率 * @param cardCounts 各点数剩余数量 * @param totalCards 剩余总牌数 * @param playerSum 玩家手牌总和 * @param isPlayerBlackjack 玩家是否为Blackjack * @param dealerSum 庄家当前总和 * @param dealerHasAce 庄家是否持有未切换的A(软牌标记) * @param dealerCardCount 庄家当前手牌数量 * @return [赢概率, 平概率, 输概率] */ private static double[] calculateProbability(int[] cardCounts, int totalCards, int playerSum, boolean isPlayerBlackjack, int dealerSum, boolean dealerHasAce, int dealerCardCount) { // 处理软牌爆牌:总和超21且有未切换的A,将A从11改为1 if (dealerSum > 21 && dealerHasAce) { dealerSum -= 10; dealerHasAce = false; } // 终止条件1:庄家爆牌 if (dealerSum > 21) { return new double[]{1.0, 0.0, 0.0}; } // 终止条件2:庄家停止抽牌(>=17) if (dealerSum >= 17) { return getFinalResult(playerSum, isPlayerBlackjack, dealerSum, dealerCardCount); } // 生成缓存Key:用各点数计数+玩家/庄家状态,避免顺序影响 StringBuilder keyBuilder = new StringBuilder(); for (int count : cardCounts) { keyBuilder.append(count).append(","); } keyBuilder.append(playerSum).append(",") .append(isPlayerBlackjack ? 1 : 0).append(",") .append(dealerSum).append(",") .append(dealerHasAce ? 1 : 0).append(",") .append(dealerCardCount); String memoKey = keyBuilder.toString(); if (memo.containsKey(memoKey)) { return memo.get(memoKey); } double winProb = 0.0; double tieProb = 0.0; double loseProb = 0.0; for (int card = 1; card <= 10; card++) { int count = cardCounts[card]; if (count == 0) { continue; } // 复制牌组计数 int[] newCardCounts = Arrays.copyOf(cardCounts, cardCounts.length); newCardCounts[card]--; int newTotalCards = totalCards - 1; int newDealerSum = dealerSum + card; boolean newDealerHasAce = dealerHasAce; if (card == 1) { // 抽到A,优先按11计算并标记软牌 newDealerSum = dealerSum + 11; newDealerHasAce = true; } // 递归计算当前分支概率 double[] branchResult = calculateProbability(newCardCounts, newTotalCards, playerSum, isPlayerBlackjack, newDealerSum, newDealerHasAce, dealerCardCount + 1); // 按当前牌的权重累加概率 double weight = (double) count / totalCards; winProb += branchResult[0] * weight; tieProb += branchResult[1] * weight; loseProb += branchResult[2] * weight; } double[] result = new double[]{winProb, tieProb, loseProb}; memo.put(memoKey, result); return result; } /** * 庄家停止抽牌后,计算最终胜负结果 */ private static double[] getFinalResult(int playerSum, boolean isPlayerBlackjack, int dealerSum, int dealerCardCount) { boolean isDealerBlackjack = dealerSum == 21 && dealerCardCount == 2; if (isPlayerBlackjack) { if (isDealerBlackjack) { return new double[]{0.0, 1.0, 0.0}; // 双方Blackjack,平局 } else { return new double[]{1.0, 0.0, 0.0}; // 玩家Blackjack,庄家不是,玩家赢 } } if (isDealerBlackjack) { return new double[]{0.0, 0.0, 1.0}; // 庄家Blackjack,玩家不是,玩家输 } if (playerSum > dealerSum) { return new double[]{1.0, 0.0, 0.0}; } else if (playerSum == dealerSum) { return new double[]{0.0, 1.0, 0.0}; } else { return new double[]{0.0, 0.0, 1.0}; } } }
修正说明
- 软牌处理:递归开始时检查庄家总和是否超21且持有软牌,若则调整总和并取消软牌标记,符合庄家规则。
- A牌逻辑修正:抽到A时直接按11计算并标记软牌,后续通过软牌调整处理爆牌情况,不再重复计算两种分支。
- 缓存Key优化:使用各点数计数数组生成Key,避免牌组顺序影响,有效复用缓存结果。
- 明确胜负判定:拆分出
getFinalResult方法,清晰处理Blackjack和平局的各种场景,逻辑更严谨。 - 牌组表示优化:用计数数组代替List存储剩余牌,减少内存开销,提高计算效率。
内容的提问来源于stack exchange,提问作者notmemaybe
相关产品推荐
相关产品推荐

