每日算法 — 使用java实现十三水:牌型组合评估与贪心最优分墩策略

一、游戏背景与规则

十三水(Chinese Poker / 十三张)是起源于中国福建的经典扑克游戏。每位玩家将13张手牌分成三墩:前墩3张、中墩5张、后墩5张。分墩完成后,两两比较对应墩的大小,要求后墩 ≥ 中墩 ≥ 前墩(牌型大小顺序),否则称为”倒水”直接判负。

牌型大小从低到高依次为:单张 < 对子 < 两对 < 三条 < 顺子 < 同花 < 葫芦 < 铁支 < 同花顺。比较时先比牌型,同牌型比最大牌的点数。

十三水的核心算法挑战在于:给定13张牌,如何在满足不”倒水”的约束下,将牌分成三墩使得期望收益最大?这是一个典型的组合优化问题。

二、核心算法原理

2.1 问题建模

13张牌分成3+5+5的三墩,本质上是从13张牌中选3张作为前墩,再从剩余10张中选5张作为中墩,最后5张为后墩。总组合数为 C(13,3) × C(10,5) = 286 × 252 = 72072 种。这个规模完全可以通过暴力枚举求解。

2.2 牌型识别与评估

首先建立牌型识别器,对任意5张或3张牌判断其牌型并计算强度值。强度值设计为一个整数,高位表示牌型等级,低位表示具体牌面大小,便于直接比较。

2.3 贪心优化策略

虽然72072种组合可暴力枚举,但实际游戏中需要快速决策。贪心策略的核心思想是:
1. 优先保证后墩最强(因为它对胜负影响最大)
2. 在剩余牌中优化中墩
3. 最后确定前墩,检查是否倒水

通过预计算所有可能的5张牌型(从13张中选5张有1287种),建立查表机制,可将单次决策时间压缩到毫秒级。

三、Java实现

3.1 牌面表示与基础工具

import java.util.*;

/**
 * 十三水核心实现:牌型识别、组合枚举与最优分墩
 */
public class ThirteenWater {

    // 牌面点数:2-10, J=11, Q=12, K=13, A=14
    public static final String[] RANK_NAMES = {"", "", "2", "3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K", "A"};
    public static final String[] SUIT_NAMES = {"♠", "♥", "♣", "♦"}; // 黑桃、红桃、梅花、方块

    // 牌型等级常量(用于强度值高位)
    public static final int HIGH_CARD = 1;
    public static final int ONE_PAIR = 2;
    public static final int TWO_PAIR = 3;
    public static final int THREE_KIND = 4;
    public static final int STRAIGHT = 5;
    public static final int FLUSH = 6;
    public static final int FULL_HOUSE = 7;
    public static final int FOUR_KIND = 8;
    public static final int STRAIGHT_FLUSH = 9;

    /**
     * 单张牌:用整数表示,低4位为点数(2-14),第4-5位为花色(0-3)
     */
    public static class Card {
        public final int value; // 编码值
        public final int rank;  // 点数 2-14
        public final int suit;  // 花色 0-3

        public Card(int rank, int suit) {
            this.rank = rank;
            this.suit = suit;
            this.value = (suit << 4) | rank;
        }

        public static Card fromValue(int v) {
            return new Card(v & 0x0F, v >> 4);
        }

        @Override
        public String toString() {
            return SUIT_NAMES[suit] + RANK_NAMES[rank];
        }
    }

    /**
     * 牌型结果:包含牌型等级和用于比较的关键牌点数
     */
    public static class HandRank {
        public final int type;      // 牌型等级
        public final int[] kickers; // 关键牌点数(从大到小),用于同牌型比较

        public HandRank(int type, int... kickers) {
            this.type = type;
            this.kickers = kickers;
        }

        /**
         * 将牌型转换为可比较的强度值
         * 高16位:牌型等级
         * 低48位:关键牌点数(每8位一张)
         */
        public long toStrength() {
            long strength = ((long) type) << 48;
            for (int i = 0; i < kickers.length && i < 6; i++) {
                strength |= ((long) kickers[i]) << (40 - i * 8);
            }
            return strength;
        }

        @Override
        public String toString() {
            String[] typeNames = {"", "高牌", "一对", "两对", "三条", "顺子", "同花", "葫芦", "铁支", "同花顺"};
            StringBuilder sb = new StringBuilder(typeNames[type] + "[");
            for (int k : kickers) sb.append(RANK_NAMES[k]).append(" ");
            return sb.toString().trim() + "]";
        }
    }

    /**
     * 解析字符串为牌列表,如 "AS KH QD JC 10S"
     */
    public static List<Card> parseHand(String handStr) {
        List<Card> cards = new ArrayList<>();
        Map<Character, Integer> rankMap = new HashMap<>();
        for (int i = 2; i <= 10; i++) rankMap.put((char)('0' + i), i);
        rankMap.put('J', 11); rankMap.put('Q', 12); rankMap.put('K', 13); rankMap.put('A', 14);
        rankMap.put('j', 11); rankMap.put('q', 12); rankMap.put('k', 13); rankMap.put('a', 14);
        Map<Character, Integer> suitMap = Map.of('S', 0, 'H', 1, 'C', 2, 'D', 3,
                                                    's', 0, 'h', 1, 'c', 2, 'd', 3,
                                                    '♠', 0, '♥', 1, '♣', 2, '♦', 3);

        String[] tokens = handStr.split("\\s+");
        for (String token : tokens) {
            if (token.isEmpty()) continue;
            char suitChar = token.charAt(token.length() - 1);
            String rankStr = token.substring(0, token.length() - 1);
            int rank = rankMap.getOrDefault(rankStr.charAt(0), Integer.parseInt(rankStr));
            int suit = suitMap.get(suitChar);
            cards.add(new Card(rank, suit));
        }
        return cards;
    }

3.2 牌型识别器

    /**
     * 识别5张牌的牌型(核心函数)
     */
    public static HandRank evaluate5Cards(List<Card> cards) {
        if (cards.size() != 5) throw new IllegalArgumentException("需要5张牌");

        int[] ranks = cards.stream().mapToInt(c -> c.rank).sorted().toArray();
        int[] suits = cards.stream().mapToInt(c -> c.suit).toArray();

        // 统计点数频率
        Map<Integer, Integer> rankCount = new HashMap<>();
        for (int r : ranks) rankCount.merge(r, 1, Integer::sum);

        // 按频率分组
        List<int[]> groups = new ArrayList<>(); // [点数, 频率]
        for (Map.Entry<Integer, Integer> e : rankCount.entrySet()) {
            groups.add(new int[]{e.getKey(), e.getValue()});
        }
        groups.sort((a, b) -> b[1] != a[1] ? b[1] - a[1] : b[0] - a[0]); // 频率降序,同频率点数降序

        boolean isFlush = true;
        for (int i = 1; i < 5; i++) if (suits[i] != suits[0]) { isFlush = false; break; }

        boolean isStraight = true;
        for (int i = 1; i < 5; i++) if (ranks[i] != ranks[i-1] + 1) { isStraight = false; break; }
        // 特殊顺子:A-2-3-4-5
        boolean isWheel = (ranks[0] == 2 && ranks[1] == 3 && ranks[2] == 4 && ranks[3] == 5 && ranks[4] == 14);
        if (isWheel) isStraight = true;

        // 同花顺
        if (isFlush && isStraight) {
            int high = isWheel ? 5 : ranks[4];
            return new HandRank(STRAIGHT_FLUSH, high);
        }

        // 铁支
        if (groups.get(0)[1] == 4) {
            return new HandRank(FOUR_KIND, groups.get(0)[0], groups.get(1)[0]);
        }

        // 葫芦
        if (groups.get(0)[1] == 3 && groups.get(1)[1] >= 2) {
            return new HandRank(FULL_HOUSE, groups.get(0)[0], groups.get(1)[0]);
        }

        // 同花
        if (isFlush) {
            return new HandRank(FLUSH, ranks[4], ranks[3], ranks[2], ranks[1], ranks[0]);
        }

        // 顺子
        if (isStraight) {
            int high = isWheel ? 5 : ranks[4];
            return new HandRank(STRAIGHT, high);
        }

        // 三条
        if (groups.get(0)[1] == 3) {
            return new HandRank(THREE_KIND, groups.get(0)[0], groups.get(1)[0], groups.get(2)[0]);
        }

        // 两对
        if (groups.get(0)[1] == 2 && groups.get(1)[1] == 2) {
            return new HandRank(TWO_PAIR, groups.get(0)[0], groups.get(1)[0], groups.get(2)[0]);
        }

        // 一对
        if (groups.get(0)[1] == 2) {
            return new HandRank(ONE_PAIR, groups.get(0)[0], groups.get(1)[0], groups.get(2)[0], groups.get(3)[0]);
        }

        // 高牌
        return new HandRank(HIGH_CARD, ranks[4], ranks[3], ranks[2], ranks[1], ranks[0]);
    }

    /**
     * 识别3张牌的牌型(前墩特殊规则)
     */
    public static HandRank evaluate3Cards(List<Card> cards) {
        if (cards.size() != 3) throw new IllegalArgumentException("需要3张牌");

        int[] ranks = cards.stream().mapToInt(c -> c.rank).sorted().toArray();
        Map<Integer, Integer> rankCount = new HashMap<>();
        for (int r : ranks) rankCount.merge(r, 1, Integer::sum);

        List<int[]> groups = new ArrayList<>();
        for (Map.Entry<Integer, Integer> e : rankCount.entrySet()) {
            groups.add(new int[]{e.getKey(), e.getValue()});
        }
        groups.sort((a, b) -> b[1] != a[1] ? b[1] - a[1] : b[0] - a[0]);

        if (groups.get(0)[1] == 3) {
            return new HandRank(THREE_KIND, groups.get(0)[0]);
        }
        if (groups.get(0)[1] == 2) {
            return new HandRank(ONE_PAIR, groups.get(0)[0], groups.get(1)[0]);
        }
        return new HandRank(HIGH_CARD, ranks[2], ranks[1], ranks[0]);
    }

3.3 最优分墩求解器

    /**
     * 三墩分法结果
     */
    public static class Division {
        public final List<Card> front;   // 前墩3张
        public final List<Card> middle;  // 中墩5张
        public final List<Card> back;    // 后墩5张
        public final HandRank frontRank;
        public final HandRank middleRank;
        public final HandRank backRank;
        public final long totalStrength; // 综合强度(用于比较)
        public final boolean isValid;    // 是否不倒水

        public Division(List<Card> front, List<Card> middle, List<Card> back) {
            this.front = front;
            this.middle = middle;
            this.back = back;
            this.frontRank = evaluate3Cards(front);
            this.middleRank = evaluate5Cards(middle);
            this.backRank = evaluate5Cards(back);
            this.isValid = backRank.toStrength() >= middleRank.toStrength() && 
                          middleRank.toStrength() >= frontRank.toStrength();
            // 综合强度:后墩权重最高,中墩次之,前墩最低
            this.totalStrength = (backRank.toStrength() << 32) | 
                                 (middleRank.toStrength() << 16) | 
                                 (frontRank.toStrength() & 0xFFFF);
        }

        @Override
        public String toString() {
            StringBuilder sb = new StringBuilder();
            sb.append(isValid ? "【有效分墩】" : "【倒水!无效】").append("\n");
            sb.append("前墩(3张): ").append(front).append(" -> ").append(frontRank).append("\n");
            sb.append("中墩(5张): ").append(middle).append(" -> ").append(middleRank).append("\n");
            sb.append("后墩(5张): ").append(back).append(" -> ").append(backRank).append("\n");
            sb.append(String.format("综合强度: %016X\n", totalStrength));
            return sb.toString();
        }
    }

    /**
     * 穷举所有合法分墩,返回最优解
     * 时间复杂度:C(13,3) × C(10,5) = 72072 种组合
     */
    public static Division findOptimalDivision(List<Card> hand) {
        if (hand.size() != 13) throw new IllegalArgumentException("需要13张牌");

        Division best = null;
        long bestStrength = -1;
        int validCount = 0;

        // 枚举前墩3张牌的所有组合
        List<int[]> frontCombinations = generateCombinations(13, 3);

        for (int[] frontIdx : frontCombinations) {
            List<Card> front = new ArrayList<>();
            boolean[] used = new boolean[13];
            for (int idx : frontIdx) {
                front.add(hand.get(idx));
                used[idx] = true;
            }

            // 从剩余10张中枚举中墩5张
            List<Integer> remaining = new ArrayList<>();
            for (int i = 0; i < 13; i++) if (!used[i]) remaining.add(i);

            List<int[]> middleCombinations = generateCombinations(10, 5);

            for (int[] midIdx : middleCombinations) {
                List<Card> middle = new ArrayList<>();
                List<Card> back = new ArrayList<>();
                boolean[] midUsed = new boolean[10];

                for (int idx : midIdx) {
                    middle.add(hand.get(remaining.get(idx)));
                    midUsed[idx] = true;
                }

                for (int i = 0; i < 10; i++) {
                    if (!midUsed[i]) back.add(hand.get(remaining.get(i)));
                }

                Division div = new Division(front, middle, back);
                if (div.isValid) {
                    validCount++;
                    if (div.totalStrength > bestStrength) {
                        bestStrength = div.totalStrength;
                        best = div;
                    }
                }
            }
        }

        System.out.println("枚举完成:总组合 " + (frontCombinations.size() * 252) + 
                          ",合法分墩 " + validCount);
        return best;
    }

    /**
     * 生成从n个元素中选k个的所有组合(索引组合)
     */
    private static List<int[]> generateCombinations(int n, int k) {
        List<int[]> result = new ArrayList<>();
        int[] combo = new int[k];
        for (int i = 0; i < k; i++) combo[i] = i;

        while (true) {
            result.add(combo.clone());

            // 找到最右边可以增大的位置
            int i = k - 1;
            while (i >= 0 && combo[i] == n - k + i) i--;
            if (i < 0) break;

            combo[i]++;
            for (int j = i + 1; j < k; j++) combo[j] = combo[j - 1] + 1;
        }

        return result;
    }

3.4 快速贪心算法与对战模拟

    /**
     * 贪心快速分墩:优先构建强后墩,再优化中墩和前墩
     * 适用于需要毫秒级响应的场景
     */
    public static Division greedyDivision(List<Card> hand) {
        List<Card> remaining = new ArrayList<>(hand);

        // Step 1: 从13张中选最强的5张作为后墩候选
        List<Card> bestBack = null;
        HandRank bestBackRank = null;
        long bestBackStr = -1;

        List<int[]> backCombos = generateCombinations(13, 5);
        for (int[] idx : backCombos) {
            List<Card> candidate = new ArrayList<>();
            for (int i : idx) candidate.add(hand.get(i));
            HandRank rank = evaluate5Cards(candidate);
            long str = rank.toStrength();
            if (str > bestBackStr) {
                bestBackStr = str;
                bestBack = candidate;
                bestBackRank = rank;
            }
        }

        // Step 2: 从剩余8张中选最强的5张作为中墩
        List<Integer> backIdx = new ArrayList<>();
        for (Card c : bestBack) backIdx.add(hand.indexOf(c));
        List<Card> afterBack = new ArrayList<>();
        for (int i = 0; i < 13; i++) if (!backIdx.contains(i)) afterBack.add(hand.get(i));

        List<Card> bestMiddle = null;
        HandRank bestMidRank = null;
        long bestMidStr = -1;
        List<int[]> midCombos = generateCombinations(8, 5);
        for (int[] idx : midCombos) {
            List<Card> candidate = new ArrayList<>();
            for (int i : idx) candidate.add(afterBack.get(i));
            HandRank rank = evaluate5Cards(candidate);
            long str = rank.toStrength();
            if (str > bestMidStr) {
                bestMidStr = str;
                bestMiddle = candidate;
                bestMidRank = rank;
            }
        }

        // Step 3: 剩余3张为前墩
        List<Card> front = new ArrayList<>();
        for (Card c : afterBack) if (!bestMiddle.contains(c)) front.add(c);

        Division div = new Division(front, bestMiddle, bestBack);

        // 如果倒水,尝试调整: weakening 后墩或 strengthening 中墩
        if (!div.isValid) {
            return findOptimalDivision(hand); // 回退到精确求解
        }

        return div;
    }

    /**
     * 两玩家对战:比较三墩,计算胜负
     * @return 净胜墩数(正表示div1赢)
     */
    public static int compareDivisions(Division d1, Division d2) {
        int score = 0;
        long f1 = d1.frontRank.toStrength(), f2 = d2.frontRank.toStrength();
        long m1 = d1.middleRank.toStrength(), m2 = d2.middleRank.toStrength();
        long b1 = d1.backRank.toStrength(), b2 = d2.backRank.toStrength();

        if (f1 > f2) score++; else if (f1 < f2) score--;
        if (m1 > m2) score++; else if (m1 < m2) score--;
        if (b1 > b2) score++; else if (b1 < b2) score--;

        // 特殊奖励:三墩全赢(打枪)额外+3
        if (score == 3) score += 3;
        if (score == -3) score -= 3;

        return score;
    }

    public static void main(String[] args) {
        System.out.println("=== 十三水最优分墩求解器 ===\n");

        // 测试手牌1:强牌
        String hand1 = "AS KS QS JS 10S 9H 9D 9C 8H 7D 6C 5S 4D";
        List<Card> cards1 = parseHand(hand1);
        System.out.println("手牌1: " + cards1);
        System.out.println("--- 精确最优解 ---");
        Division opt1 = findOptimalDivision(cards1);
        System.out.println(opt1);

        // 测试手牌2:中等牌力
        String hand2 = "AH KH QD JC 10H 9S 8D 7C 6H 5D 4C 3H 2D";
        List<Card> cards2 = parseHand(hand2);
        System.out.println("\n手牌2: " + cards2);
        System.out.println("--- 精确最优解 ---");
        Division opt2 = findOptimalDivision(cards2);
        System.out.println(opt2);

        // 测试手牌3:随机生成对比
        System.out.println("\n--- 贪心 vs 精确对比 ---");
        long start1 = System.nanoTime();
        Division greedy = greedyDivision(cards1);
        long time1 = System.nanoTime() - start1;

        long start2 = System.nanoTime();
        Division exact = findOptimalDivision(cards1);
        long time2 = System.nanoTime() - start2;

        System.out.println("贪心算法耗时: " + (time1 / 1_000_000.0) + "ms");
        System.out.println("精确算法耗时: " + (time2 / 1_000_000.0) + "ms");
        System.out.println("贪心结果: " + greedy.backRank + " / " + greedy.middleRank + " / " + greedy.frontRank);
        System.out.println("精确结果: " + exact.backRank + " / " + exact.middleRank + " / " + exact.frontRank);

        // 对战模拟
        if (opt1 != null && opt2 != null) {
            System.out.println("\n--- 对战模拟:手牌1 vs 手牌2 ---");
            int result = compareDivisions(opt1, opt2);
            System.out.println("手牌1净胜墩数: " + result);
        }
    }
}

四、复杂度分析

模块 时间复杂度 说明
5张牌型识别 O(1) 固定5张,常数时间
3张牌型识别 O(1) 固定3张,常数时间
精确最优分墩 O(C(13,3)·C(10,5)) = O(72072) 枚举所有分法
贪心快速分墩 O(C(13,5)+C(8,5)) = O(1300) 分阶段枚举
两玩家对战 O(1) 三墩比较

精确算法在普通PC上执行时间约10-50ms,贪心算法在1ms以内,均可满足实时游戏需求。

五、总结

本文完整实现了十三水扑克游戏的Java版核心算法,涵盖牌型识别、组合枚举与最优分墩策略:

  1. 牌型编码:用紧凑整数编码表示牌和牌型,支持O(1)时间比较大小
  2. 牌型识别器:完整支持9种牌型的精确识别,包括A-2-3-4-5特殊顺子
  3. 精确求解:枚举72072种分墩组合,保证找到全局最优不”倒水”方案
  4. 贪心加速:分阶段优先构建强后墩,将复杂度降低两个数量级
  5. 对战引擎:支持两玩家分墩结果比较,含”打枪”特殊计分规则

读者可在此基础上接入随机发牌模块、多玩家循环对战和牌力统计分析,构建完整的十三水AI训练平台。贪心策略也可进一步改进为带剪枝的启发式搜索,在保持速度的同时更接近精确最优解。