You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

二十一点站定后胜负平概率计算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};
        }
    }
}

修正说明

  1. 软牌处理:递归开始时检查庄家总和是否超21且持有软牌,若则调整总和并取消软牌标记,符合庄家规则。
  2. A牌逻辑修正:抽到A时直接按11计算并标记软牌,后续通过软牌调整处理爆牌情况,不再重复计算两种分支。
  3. 缓存Key优化:使用各点数计数数组生成Key,避免牌组顺序影响,有效复用缓存结果。
  4. 明确胜负判定:拆分出getFinalResult方法,清晰处理Blackjack和平局的各种场景,逻辑更严谨。
  5. 牌组表示优化:用计数数组代替List存储剩余牌,减少内存开销,提高计算效率。

内容的提问来源于stack exchange,提问作者notmemaybe

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.23 14:09:52