【问题标题】:Generating all 5 card poker hands生成所有 5 张牌扑克手
【发布时间】:2011-04-19 06:30:39
【问题描述】:

这个问题乍看起来很简单,但实际上比看起来要复杂得多。这让我一时难过。

有 52c5 = 2,598,960 种方法可以从 52 张牌组中选择 5 张牌。然而,由于花色在扑克中是可以互换的,因此其中许多是等价的——手牌 2H 2C 3H 3S 4D 等价于 2D 2S 3D 3C 4H——只需将花色互换即可。根据wikipedia 的说法,一旦考虑到可能的花色重新着色,就有 134,459 个不同的 5 张牌手。

问题是,我们如何有效地生成所有这些可能的牌?我不想生成所有手牌,然后消除重复,因为我想将问题应用于更多数量的牌,以及评估快速螺旋失控的手牌数量。我目前的尝试集中在生成深度优先,并跟踪当前生成的卡片以确定哪些花色和等级对下一张卡片有效,或者广度优先,生成所有可能的下一张卡片,然后通过转换每个卡片来删除重复项通过重新着色交给“规范”版本。这是我在 Python 中尝试的广度优先解决方案:

# A card is represented by an integer. The low 2 bits represent the suit, while
# the remainder represent the rank.
suits = 'CDHS'
ranks = '23456789TJQKA'

def make_canonical(hand):
  suit_map = [None] * 4
  next_suit = 0
  for i in range(len(hand)):
    suit = hand[i] & 3
    if suit_map[suit] is None:
      suit_map[suit] = next_suit
      next_suit += 1
    hand[i] = hand[i] & ~3 | suit_map[suit]
  return hand

def expand_hand(hand, min_card):
  used_map = 0
  for card in hand:
    used_map |= 1 << card

  hands = set()
  for card in range(min_card, 52):
    if (1 << card) & used_map:
      continue
    new_hand = list(hand)
    new_hand.append(card)
    make_canonical(new_hand)
    hands.add(tuple(new_hand))
  return hands

def expand_hands(hands, num_cards):
  for i in range(num_cards):
    new_hands = set()
    for j, hand in enumerate(hands):
      min_card = hand[-1] + 1 if i > 0 else 0
      new_hands.update(expand_hand(hand, min_card))
    hands = new_hands
  return hands

不幸的是,这会产生太多手牌:

>>> len(expand_hands(set([()]), 5))
160537

谁能提出一个更好的方法来生成不同的手,或者指出我在尝试中出错的地方?

【问题讨论】:

  • 好问题,你需要它做什么?如果你想用它来计算一只手对二手的机会,你可以使用蒙特卡罗方法
  • 我正在预先计算所有单挑对决。蒙特卡洛适用于没有足够计算能力的人。 ;)
  • 这是一个非常有趣的问题。我现在没有时间玩它,但是我想到了一些可能有用也可能没用的想法。首先是从高到低工作——即每张牌的等级必须小于或等于前一张牌的等级(例如A-9-9-8-2)。其次,我相信第一张牌只能抽梅花,第二张牌只能抽梅花或方块,第三张牌只能抽黑桃。 (我对你的按位代码一无所知,所以你可能已经在做这些事情了。)
  • 您是否关心手牌的重要排名或实际排列?例如,如果您的前两张牌的花色不同,那么您就不可能形成同花,并且可以忽略该子树其余部分的花色。我认为只有大约 7500 个通过仙人掌 Kev 排名的扑克牌。让我看看能不能找到链接。
  • @davidchambers 你说得对,按排名顺序生成它们是正确的——这是消除手排列的最简单方法。正如您所描述的,每张新牌的有效套装确实增加了,除了有效套装取决于所有以前的牌 - 例如,如果前两张牌的花色相同,则第三张牌只有两种可能性。这是我最初使用 DFS 的方式,但我最终还是生成了同构的手。 ://

标签: python algorithm permutation combinatorics poker


【解决方案1】:

这是一个 Python 解决方案,它利用 numpy 并生成规范交易及其多重性。我使用 Python 的 itertools 模块创建 4 种花色的所有 24 种可能排列,然后迭代所有 2,598,960 种可能的 5 张牌。每笔交易仅用 5 行就被置换并转换为规范的 id。它非常快,因为循环只经过 10 次迭代即可涵盖所有交易,并且只需要管理内存需求。除了使用 itertools.combinations 之外,所有繁重的工作都在 numpy 中高效完成。很遗憾,这在 numpy 中不直接受支持。

import numpy as np
import itertools

# all 24 permutations of 4 items
s4 = np.fromiter(itertools.permutations(range(4)), dtype='i,i,i,i').view('i').reshape(-1,4)

c_52_5 = 2598960 # = binomial(52,5) : the number of 5-card deals in ascending card-value order
block_n = c_52_5/10
def all5CardDeals():
    '''iterate over all possible 5-card deals in 10 blocks of 259896 deals each'''
    combos = itertools.combinations(range(52),5)
    for i in range(0, c_52_5, block_n):
        yield np.fromiter(combos, dtype='i,i,i,i,i', count=block_n).view('i').reshape(-1,5)

canon_id = np.empty(c_52_5, dtype='i')
# process all possible deals block-wise.
for i, block in enumerate(all5CardDeals()):
    rank, suit = block/4, block%4     # extract the rank and suit of each card
    d = rank[None,...]*4 + s4[:,suit] # generate all 24 permutations of the suits
    d.sort(2)                         # re-sort into ascending card-value order
    # convert each deal into a unique integer id
    deal_id = d[...,0]+52*(d[...,1]+52*(d[...,2]+52*(d[...,3]+52*d[...,4])))
    # arbitrarily select the smallest such id as the canonical one 
    canon_id[i*block_n:(i+1)*block_n] = deal_id.min(0)
# find the unique canonical deal ids and the index into this list for each enumerated hand
unique_id, indices = np.unique(canon_id, return_inverse=True)
print len(unique_id) # = 134459
multiplicity = np.bincount(indices)
print multiplicity.sum() # = 2598960 = c_52_5

【讨论】:

    【解决方案2】:

    在非等价意义上,您可能真的想要生成不同手牌的数量。在这种情况下,根据维基百科的文章,有 7462 手可能。这是一个python sn-p,它将枚举它们。

    逻辑很简单:每 5 组等级有一只手;此外,如果所有等级都不同,则可以通过使所有花色匹配来形成另一种不同的手。

    count = 0 
    
    for i in range(0,13):
        for j in range (i,13):
            for k in range(j,13):
                for l in range(k,13):
                    for m in range(l,13):
                        d = len(set([i,j,k,l,m])) # number of distinct ranks
                        if d == 1: continue    # reject nonsensical 5-of-a-kind
                        count += 1
                        # if all the ranks are distinct then 
                        # count another hand with all suits equal
                        if d == 5: count += 1
    
    print count   # 7462
    

    【讨论】:

      【解决方案3】:

      这是一个简单直接的算法,用于根据花色排列将手牌简化为标准牌。

      1. 将手牌转换为四个位组,每个花色一个代表花色的牌
      2. 对位集进行排序
      3. 将位集转换回手

      这就是算法在 C++ 中的样子,带有一些隐含的 Suit 和 CardSet 类。请注意,return 语句通过连接位串来转换手。

      CardSet CardSet::canonize () const
      {
        int smasks[Suit::NUM_SUIT];
        int i=0;
        for (Suit s=Suit::begin(); s<Suit::end(); ++s)
          smasks[i++] = this->suitMask (s);
      
        sort (smasks, smasks+Suit::NUM_SUIT);
      
        return CardSet(
          static_cast<uint64_t>(smasks[3])                        |
          static_cast<uint64_t>(smasks[2]) << Rank::NUM_RANK      |
          static_cast<uint64_t>(smasks[1]) << Rank::NUM_RANK*2    |
          static_cast<uint64_t>(smasks[0]) << Rank::NUM_RANK*3);
      }
      

      【讨论】:

        【解决方案4】:

        如果您只对导致不同手牌排名的牌感兴趣,那么实际上只有 7462 个不同的牌类需要考虑(请参阅Wikipedia)。

        通过为每个类别及其相应的多重性创建一个包含示例的表格,您可以非常快速地检查所有相关手牌的概率加权。也就是说,假设没有牌是已知的,因此事先已经固定。

        【讨论】:

          【解决方案5】:

          看这里:

          http://specialk-coding.blogspot.com/

          http://code.google.com/p/specialkpokereval/

          这些将 5 张牌(和 7 张牌)视为一个整数,即各张牌的总和,与花色无关。正是您需要的。

          这是用 Objective-C 和 Java 编写的快速排名 7 和 5 手牌方案的一部分。

          【讨论】:

            【解决方案6】:

            初始输入:

            H 0 0 0 0 0 0 0 0 0 0 0 0 0
            C 1 0 0 0 0 0 0 0 0 0 0 0 0
            D 1 0 0 0 0 0 0 0 0 0 0 0 0
            S 1 1 0 0 0 0 0 0 0 0 0 0 0
            + A 2 3 4 5 6 7 8 9 T J Q K
            

            第 1 步:对于大于或等于使用的最高等级的每个等级,将该等级中的所有花色设置为 0。您可以只检查较高的牌,因为较低的组合将由较低的起点检查。

            H 0 0 0 0 0 0 0 0 0 0 0 0 0
            C 1 0 0 0 0 0 0 0 0 0 0 0 0
            D 1 0 0 0 0 0 0 0 0 0 0 0 0
            S 1 0 0 0 0 0 0 0 0 0 0 0 0
            + A 2 3 4 5 6 7 8 9 T J Q K
            

            第 2 步:折叠到不同的行

            0 0 0 0 0 0 0 0 0 0 0 0 0
            1 0 0 0 0 0 0 0 0 0 0 0 0
            A 2 3 4 5 6 7 8 9 T J Q K
            

            第 3 步:爬回确定与每个不同行匹配的第一个花色,并选择与不同行匹配的花色(由 * 标识)

            H 0 * 0 0 0 0 0 0 0 0 0 0 0
            C 1 0 0 0 0 0 0 0 0 0 0 0 0
            D 1 * 0 0 0 0 0 0 0 0 0 0 0
            S 1 1 0 0 0 0 0 0 0 0 0 0 0
            + A 2 3 4 5 6 7 8 9 T J Q K
            

            现在显示排名 3 的重复

            H 0 0 0 0 0 0 0 0 0 0 0 0 0
            C 1 0 0 0 0 0 0 0 0 0 0 0 0
            D 1 0 0 0 0 0 0 0 0 0 0 0 0
            S 1 1 0 0 0 0 0 0 0 0 0 0 0
            + A 2 3 4 5 6 7 8 9 T J Q K
            
            0 0 0 0 0 0 0 0 0 0 0 0 0
            1 0 0 0 0 0 0 0 0 0 0 0 0
            1 1 0 0 0 0 0 0 0 0 0 0 0
            A 2 3 4 5 6 7 8 9 T J Q K
            
            H 0 0 * 0 0 0 0 0 0 0 0 0 0
            C 1 0 0 0 0 0 0 0 0 0 0 0 0
            D 1 0 * 0 0 0 0 0 0 0 0 0 0
            S 1 1 * 0 0 0 0 0 0 0 0 0 0
            + A 2 3 4 5 6 7 8 9 T J Q K
            

            第 4 步:一旦有 5 个单元格设置为 1,将可能的花色抽象手数加 1 并向上递归。

            可能的花色抽象手总数为 134,459。这是我为测试它而编写的代码:

            using System;
            using System.Collections.Generic;
            using System.Linq;
            
            namespace ConsoleApplication20
            {
                struct Card
                {
                    public int Suit { get; set; }
                    public int Rank { get; set; }
                }
                
                class Program
                {
                    static int ranks = 13;
                    static int suits = 4;
                    static int cardsInHand = 5;
            
                    static void Main(string[] args)
                    {
                        List<Card> cards = new List<Card>();
                        //cards.Add(new Card() { Rank = 0, Suit = 0 });
                        int numHands = GenerateAllHands(cards);
                
                        Console.WriteLine(numHands);
                        Console.ReadLine();
                    }
              
                    static int GenerateAllHands(List<Card> cards)
                    {
                        if (cards.Count == cardsInHand) return 1;
                
                        List<Card> possibleNextCards = GetPossibleNextCards(cards);
                
                        int numSubHands = 0;
                
                        foreach (Card card in possibleNextCards)
                        {
                            List<Card> possibleNextHand = cards.ToList(); // copy list
                            possibleNextHand.Add(card);
                            numSubHands += GenerateAllHands(possibleNextHand);
                        }
                
                        return numSubHands;
                    }
                
                    static List<Card> GetPossibleNextCards(List<Card> hand)
                    {
                        int maxRank = hand.Max(x => x.Rank);
                        
                        List<Card> result = new List<Card>();
                
                        // only use ranks >= max
                        for (int rank = maxRank; rank < ranks; rank++)
                        {
                            List<int> suits = GetPossibleSuitsForRank(hand, rank);
                            var possibleNextCards = suits.Select(x => new Card { Rank = rank, Suit = x });
                            result.AddRange(possibleNextCards);
                        }
                
                        return result;
                    }
                
                    static List<int> GetPossibleSuitsForRank(List<Card> hand, int rank)
                    {
                        int maxSuit = hand.Max(x => x.Suit);
                
                        // select number of ranks of different suits
                        int[][] card = GetArray(hand, rank);
                
                        for (int i = 0; i < suits; i++)
                        {
                            card[i][rank] = 0;
                        }
                
                        int[][] handRep = GetArray(hand, rank);
                
                        // get distinct rank sets, then find which ranks they correspond to
                        IEnumerable<int[]> distincts = card.Distinct(new IntArrayComparer());
                
                        List<int> possibleSuits = new List<int>();
                
                        foreach (int[] row in distincts)
                        {
                            for (int i = 0; i < suits; i++)
                            {
                                if (IntArrayComparer.Compare(row, handRep[i]))
                                {
                                    possibleSuits.Add(i);
                                    break;
                                }
                            }
                        }
                
                        return possibleSuits;
                    }
                
                    class IntArrayComparer : IEqualityComparer<int[]>
                    {
                        #region IEqualityComparer<int[]> Members
                
                        public static bool Compare(int[] x, int[] y)
                        {
                            for (int i = 0; i < x.Length; i++)
                            {
                                if (x[i] != y[i]) return false;
                            }
                
                            return true;
                        }
                
                        public bool Equals(int[] x, int[] y)
                        {
                            return Compare(x, y);
                        }
                
                        public int GetHashCode(int[] obj)
                        {
                            return 0;
                        }
            
                        #endregion
                    }
            
                    static int[][] GetArray(List<Card> hand, int rank)
                    {
                        int[][] cards = new int[suits][];
                        for (int i = 0; i < suits; i++)
                        {
                            cards[i] = new int[ranks];
                        }
            
                        foreach (Card card in hand)
                        {
                            cards[card.Suit][card.Rank] = 1;
                        }
                
                        return cards;
                    }
                }
            }
            

            希望它被分解得足够容易理解。

            【讨论】:

            • 你的号码与维基百科和我链接的论文不一致 - 你肯定产生的手太少了。
            • 你说得对,我用最低的牌播种它,一旦空手播种,它会产生 134,459。
            • 旧线程,我知道,但此代码按原样使用会产生错误,因为未填充初始卡(第 49 行的无效操作)。然后,如果您填写该卡,您会得到错误的号码。
            • 我复制了这段代码,但无法获得正确的 134,459 计数。反而得到了 175,123
            【解决方案7】:

            您的整体方法是合理的。我很确定问题出在您的 make_canonical 函数上。您可以尝试打印将 num_cards 设置为 3 或 4 的手牌,然后查找您错过的等价物。

            我找到了一个,但可能还有更多:

            # The inputs are equivalent and should return the same value
            print make_canonical([8, 12 | 1]) # returns [8, 13]
            print make_canonical([12, 8 | 1]) # returns [12, 9]
            

            作为参考,以下是我的解决方案(在查看您的解决方案之前开发)。我使用深度优先搜索而不是广度优先搜索。此外,我没有编写将手转换为规范形式的函数,而是编写了一个函数来检查手是否是规范的。如果它不是规范的,我会跳过它。我定义了 rank = card % 13 和 suit = card / 13。这些差异都不重要。

            import collections
            
            def canonical(cards):
                """
                Rules for a canonical hand:
                1. The cards are in sorted order
            
                2. The i-th suit must have at least many cards as all later suits.  If a
                   suit isn't present, it counts as having 0 cards.
            
                3. If two suits have the same number of cards, the ranks in the first suit
                   must be lower or equal lexicographically (e.g., [1, 3] <= [2, 4]).
            
                4. Must be a valid hand (no duplicate cards)
                """
            
                if sorted(cards) != cards:
                    return False
                by_suits = collections.defaultdict(list)
                for suit in range(0, 52, 13):
                    by_suits[suit] = [card%13 for card in cards if suit <= card < suit+13]
                    if len(set(by_suits[suit])) != len(by_suits[suit]):
                        return False
                for suit in range(13, 52, 13):
                    suit1 = by_suits[suit-13]
                    suit2 = by_suits[suit]
                    if not suit2: continue
                    if len(suit1) < len(suit2):
                        return False
                    if len(suit1) == len(suit2) and suit1 > suit2:
                        return False
                return True
            
            def deal_cards(permutations, n, cards):
                if len(cards) == n:
                    permutations.append(list(cards))
                    return
                start = 0
                if cards:
                    start = max(cards) + 1
                for card in range(start, 52):
                    cards.append(card)
                    if canonical(cards):
                        deal_cards(permutations, n, cards)
                    del cards[-1]
            
            def generate_permutations(n):
                permutations = []
                deal_cards(permutations, n, [])
                return permutations
            
            for cards in generate_permutations(5):
                print cards
            

            它生成正确数量的排列:

            Cashew:~/$ python2.6 /tmp/cards.py | wc
            134459
            

            【讨论】:

            • 谢谢!您能否简要说明规范测试的工作原理?我想我明白了基本的想法,但描述会很棒。
            • @Nick:当然,我已经在我的回答中为canonical 函数添加了一个文档字符串。
            • 很好的答案,谢谢!实际上,生成规范手而不是仅仅对其进行测试是有原因的:有些手比其他手具有更多的同构,并且知道有多少是很重要的,以便知道每个手对应多少“真实”手。跨度>
            【解决方案8】:

            你的问题听起来很有趣,所以我简单地尝试通过以排序方式遍历所有可能的手来实现它。我没有详细查看您的代码,但似乎我的实现与您的完全不同。猜猜我的脚本找到的手数:160537

            • 我的手总是被分类的,例如2 3 4 4 天
            • 如果有2张相等的牌,颜色也排序(颜色就叫0,1,2,3)
            • 第一张卡片总是颜色 0,第二张颜色总是 0 或 1
            • 一张牌只能有前一张牌的颜色或下一个更大的数字,例如如果卡片 1+2 的颜色为 0,则卡片 3 的颜色只能为 0 或 1

            你确定维基百科上的数字是正确的吗?

            count = 0
            for a1 in range(13):
                c1 = 0
                for a2 in range(a1, 13):
                    for c2 in range(2):
                        if a1==a2 and c1==c2:
                            continue
                        nc3 = 2 if c1==c2 else 3
                        for a3 in range(a2, 13):
                            for c3 in range(nc3):
                                if (a1==a3 and c1>=c3) or (a2==a3 and c2>=c3):
                                    continue
                                nc4 = nc3+1 if c3==nc3-1 else nc3
                                for a4 in range(a3, 13):
                                    for c4 in range(nc4):
                                        if (a1==a4 and c1>=c4) or (a2==a4 and c2>=c4) or (a3==a4 and c3>=c4):
                                            continue
                                        nc5 = nc4+1 if (c4==nc4-1 and nc4!=4) else nc4
                                        for a5 in range(a4, 13):
                                            for c5 in range(nc5):
                                                if (a1==a5 and c1>=c5) or (a2>=a5 and c2>=c5) or (a3==a5 and c3>=c5) or (a4==a5 and c4>=c5):
                                                    continue
                                                #print([(a1,c1),(a2,c2),(a3,c3),(a4,c4),(a5,c5)])
                                                count += 1
            print("result: ",count)
            

            【讨论】:

            • 相当肯定。这篇论文,枚举起始扑克手 (math.sfu.ca/~alspach/comp42.pdf) 使用一些我不假装完全理解的组合得出与 Wikipedia 相同的数字。 (参见五张抽牌部分)
            • 我真的很喜欢这个配色方案,好想!
            • 我发现了一个我产生重复的案例:(B0,B1,B2,B3,D0)和(B0,B1,B2,B3,D1)相等
            【解决方案9】:

            您可以简单地给所有手牌值的规范排序(A 到 K),然后根据它们在该顺序中首次出现的顺序分配抽象花色字母。

            示例:JH 4C QD 9C 3D 将转换为 3a 4b 9b Jc Qa。

            生成应该最适合作为动态编程:

            • 从一组空的单手开始,
            • 制作新套装:
              • 对于旧套牌中的每一手牌,通过添加剩余的一张牌来生成每个可能的手牌
              • 规范所有新手
              • 删除重复项

            【讨论】:

            • 你描述的过程正是我在我的 sn-p 中所做的——但它仍然产生了比应该存在的更多的手,所以我显然做错了什么。
            • 我在你的 sn-p 中没有看到任何排序过程,通过位摆弄来做所有事情的决定确实对代码没有好处。我的猜测是您没有正确规范等值卡,因此3D 4H 5D 5H 可以同时成为3a 4b 5a 5b3a 4b 5b 5a
            • 排序过程在 expand_hand 的 'min_card' 参数中表示 - 它不会生成低于该值的排名,从而确保它按排名排序。不过,您似乎对规范化是正确的 - 但是,您能提出一种解决方法吗?
            【解决方案10】:

            为 5 张牌生成等价类并非易事。 当我需要这个时,我通常使用http://www.vpgenius.com/ 网页。在http://www.vpgenius.com/video-poker/games/,您可以选择您需要的扑克游戏种类,在“编程选项卡”中,您有一个关于“独特花色模式”的部分。因此,仅将其复制并加载到程序中可能比尝试生成自己的更容易。

            【讨论】:

            • 不过,这一切都是为了视频扑克 - 与简单地生成独特的手牌截然不同。
            • 如果您使用 Jacks-or-better 品种,您将拥有相同的不同手牌。
            • 因此,这似乎是一种合理的方法。不过,我更愿意看到如何生成所有花色等价类的描述 - 之后,生成每个类中的所有手牌应该相当简单。
            【解决方案11】:

            我不是扑克玩家,所以手牌优先的细节超出了我的范围。但问题似乎在于,您正在通过从牌组生成套牌来穿越“5张牌组”的空间,而您应该穿越“不同的扑克手”的空间。

            不同手的空间需要新的语法。重要的是准确捕获与手牌优先级相关的信息。例如,只有 4 手牌是皇家同花顺,所以这些手牌可以用符号“RF”加上花色指示符来描述,如梅花中的皇家同花顺“RFC”。 10 高的心脏冲洗可能是“FLH10”(不确定冲洗是否还有其他优先特征,但我认为这就是您需要知道的全部)。如果我不理解您最初的问题陈述,“2C 2S AH 10C 5D”的牌将是一个更长的表达方式,例如“PR2 A 10 5”。

            一旦定义了不同手的语法,您就可以将其表达为正则表达式,这将告诉您如何生成不同手的整个空间。听起来很有趣!

            【讨论】:

            • 您误会了——我不是要列举结果(对、两对等),而是要列举所有不同的 5 张牌组,对花色进行模重新分配。排名手是一个完全独立的问题。 AC AD 2C 3C 4C 和 AC AD 2H 3H 4H 排名相同,但它们是不同的手,因为你不能通过交换花色来转换另一手。
            • 我明白了。无论如何,同样的技术适用,但我的例子并不好。首先,识别你要遍历的空间中唯一项的语法;把它写成正则表达式;然后通过展开正则表达式遍历空间。
            • 当然——但找出一种方法来枚举独特元素是我问题的重点。
            猜你喜欢
            • 2021-05-17
            • 1970-01-01
            • 2016-02-24
            • 1970-01-01
            • 2017-07-11
            • 1970-01-01
            • 1970-01-01
            • 2011-10-22
            • 1970-01-01
            相关资源
            最近更新 更多