华为OD机试真题解析:德州扑克牌型比较算法与多语言实现 1. 项目概述与核心价值最近在准备华为OD机试的朋友如果刷题刷到“德州扑克”这道题估计第一反应是有点懵。这听起来像是个游戏题跟传统的算法题画风不太一样。我当初在准备机试时也碰到了这道题它属于E卷的复用真题考察点非常综合绝不是简单地让你模拟发牌和比大小。这道题的核心是将现实世界中复杂的德州扑克牌型比较规则抽象成一个严谨的、可被程序化判定的逻辑问题。它要求你具备扎实的字符串处理、数据结构尤其是排序和映射、逻辑分支控制能力以及对边界条件的周密考虑。这道题适合所有正在备战华为OD机试尤其是目标岗位对逻辑思维和代码实现严谨性要求较高的同学。无论是用C追求极致性能用Java构建清晰的对象模型还是用Python快速实现原型这道题都能很好地检验你的基本功。通过拆解这道题你不仅能掌握一道高频真题的解法更能学会如何将复杂的业务规则转化为清晰的算法步骤这种能力在未来的开发工作中至关重要。下面我就结合自己的实战经验从思路到代码为你完整拆解这道“德州扑克”题。2. 题目核心需求与规则解析2.1 问题场景还原题目通常会给出多组输入每组输入代表两个玩家也可能是多个但常见简化版为两人对战的手牌。每手牌由5张牌组成牌面表示如“SA SK SQ SJ S10”或“HA H3 D3 C3 S3”。这里的字母代表花色和点数。我们需要根据德州扑克的牌型大小规则判断哪一手牌获胜或者是否平局。这是题目的最外层需求。2.2 关键规则抽象与难点德州扑克的牌型从大到小依次为皇家同花顺 同花顺 四条 满堂红葫芦 同花 顺子 三条 两对 一对 高牌。要实现程序判定难点不在于记住这个顺序而在于如何让计算机“理解”并“执行”这个比较过程。这涉及到几个核心的抽象牌面编码首先需要将“SA”黑桃A、“H10”红心10这样的字符串解析为可比较的数据结构。通常需要分离花色S, H, D, C和点数A, K, Q, J, 10, 9, …, 2。点数需要转换为数字以便比较例如 A-14, K-13, Q-12, J-11, 10-10, … 2-2。牌型检测这是算法的核心。我们需要编写函数输入5张牌的点数和花色输出其牌型。例如检测“同花”需要判断5张牌花色是否相同检测“顺子”需要判断5张牌的点数是否连续注意A可以作为1使用构成A-2-3-4-5这样的最小顺子。同牌型比较当两手牌牌型相同时需要根据该牌型特定的规则进行比较。这是最容易出错的地方。四条比较四条的点数相同则比较剩余那张“单牌”。满堂红比较三条部分的点数相同则比较一对部分的点数。同花/高牌从大到小依次比较每张牌的点数。顺子/同花顺比较顺子中最大的那张牌的点数。注意A-2-3-4-5这种顺子最大牌是5。三条比较三条的点数然后依次比较剩余两张单牌。两对先比较较大对子的点数再比较较小对子的点数最后比较单牌。一对先比较对子的点数然后依次比较剩余三张单牌。输入输出处理题目可能要求处理多组数据直到输入特定字符如#结束。需要妥善处理输入格式可能包含空格、换行。注意机试真题的输入输出格式是“圣旨”必须严格按照题目描述来解析和输出。一个常见的坑是题目可能要求输出Black wins、White wins或Tie而不是简单的1或0。务必仔细审题。3. 核心算法设计与实现思路面对这样一个多规则判断的问题最忌讳的就是写成一锅粥的if-else。清晰的模块化设计是成功的关键。我的思路是采用“分层判定”和“特征向量”相结合的方法。3.1 整体架构设计数据表示层定义一个Card类或结构体包含point整数点数和suit字符花色两个属性。提供一个解析字符串的构造函数。牌型判定层实现一个get_hand_type函数输入5张Card的列表返回一个代表牌型和大小的“特征值”。这个特征是后续比较的关键。比较决策层实现一个compare_hands函数输入两手牌的特征值或原始牌列表根据牌型优先级和同牌型规则返回比较结果。输入输出驱动层负责读取输入、调用上述函数、输出结果。3.2 牌型判定特征值法详解直接返回一个“皇家同花顺”、“同花顺”这样的枚举值在比较时还需要回溯原始牌数据代码会显得冗长。我推荐使用一个长整型Long Integer作为特征值其不同数位编码了不同的信息。一个实用的编码方案如下假设用一个64位整数高4位bits 60-63编码牌型。例如皇家同花顺10同花顺9四条8满堂红7同花6顺子5三条4两对3一对2高牌1。后续的每4位一组编码用于比较的关键点数。顺序至关重要它直接体现了同牌型下的比较规则。举例说明一手牌是“四条”点数为[8,8,8,8,5]。它的特征值可以构造成0x80000500。0x8牌型“四条”编码为8。0x0000500接下来的4位0x5编码四条的点数8但这里为了演示先存了单牌5不这样不对。正确的逻辑是先存主要比较点再存次要比较点。对于四条首先比较四条的点数然后比较单牌。所以应该先存8再存5。特征值可以是(8 20) | (5 16)再与牌型编码组合。但更通用的方法是我们将5张牌按“重要性”重新排序后将其点数依次存入特征值。更通用的方法是无论什么牌型我们都生成一个“比较序列”。识别牌型。根据牌型规则将5张牌的点数按“比较优先级”排序。例如四条[8,8,8,8,5]- 排序为[8,8,8,8,5]但比较时是先比8再比5。所以我们可以生成序列[8,5]但为了统一我们总是生成一个5个数的序列不足的补0。但这样浪费空间。一个巧妙的办法是我们将牌按“点数频率”降序排序如果频率相同则按点数降序排序。对于[8,8,8,8,5]统计频率{8:4, 5:1}。按频率降序点数降序排序后序列为[8,8,8,8,5]。这个序列的自然顺序从第一个元素开始比较就完全符合四条的比较规则对于满堂红[8,8,8,5,5]频率{8:3, 5:2}排序后为[8,8,8,5,5]。比较时先比第一个元素三条的点数8再比第四个元素一对的点数5。对于高牌[A, K, Q, J, 9]频率都是1按点数降序排序为[14,13,12,11,9]。比较规则正是依次比较。将这个排序后的点数序列每个点数用4位表示范围2-14足够了和牌型编码组合成一个长整数。这样任何两手牌的比较就简化为了两个长整数的直接比较。牌型编码在高位决定了优先级点数序列在低位决定了同牌型下的胜负。这是本题算法最精华的部分。3.3 关键步骤实现要点解析与排序读入字符串解析为Card对象列表。立即按点数降序排序方便后续处理。判断顺子排序后检查点数是否连续。特别注意A,2,3,4,5的情况可以将A视为1重新排序变成[5,4,3,2,1]再判断是否连续。判断同花检查所有卡牌花色是否相同。统计频率并生成比较序列使用字典Map统计每个点数的出现次数。将卡牌列表按照(频率点数)的元组进行降序排序。先按频率降序频率相同按点数降序。这个排序结果就是我们的“比较序列”。注意排序对象应该是Card列表但排序依据是它们点数的频率和点数本身。确定牌型编码根据“是否同花”、“是否顺子”、“频率分布”来判断具体牌型。频率分布可以通过频率字典的values()集合来判断。例如values()是{4,1}-四条{3,2}-满堂红{3,1,1}-三条{2,2,1}-两对{2,1,1,1}-一对。组装特征值将牌型编码左移到高位然后将“比较序列”中的每个点数依次左移后拼接到特征值中。4. 多语言代码实现与核心分析这里我将分别给出C、Java和Python三种语言的核心实现。重点展示特征值生成和比较的逻辑省略部分冗余的输入解析代码。4.1 C 实现要点C实现注重效率和底层控制。可以使用std::pair或自定义结构来辅助排序。#include iostream #include string #include vector #include algorithm #include map using namespace std; int getPoint(char p) { if (p A) return 14; if (p K) return 13; if (p Q) return 12; if (p J) return 11; if (p T) return 10; // 注意题目中10可能用T表示 return p - 0; } long long getHandValue(const vectorpairint, char cards) { // cards 应预先按点数降序排序 vectorint points; for (const auto card : cards) points.push_back(card.first); // 判断同花 bool isFlush all_of(cards.begin()1, cards.end(), [](const pairint,char c){ return c.second cards[0].second; }); // 判断顺子 bool isStraight true; for (size_t i 1; i points.size(); i) { if (points[i-1] - points[i] ! 1) { isStraight false; break; } } // 处理 A,2,3,4,5 的特殊顺子 if (!isStraight points vectorint{14,5,4,3,2}) { isStraight true; points {5,4,3,2,1}; // 重排点数序列将A视为1 } // 统计频率并生成比较序列 mapint, int freq; for (int p : points) freq[p]; // 按频率降序点数降序排序卡片 sort(points.begin(), points.end(), [](int a, int b) { if (freq[a] ! freq[b]) return freq[a] freq[b]; return a b; }); // 确定牌型编码 int handRank 1; // 默认高牌 if (isStraight isFlush) handRank (points[0] 14 points[4] 10) ? 10 : 9; // 皇家同花顺 or 同花顺 else if (freq.size() 2) { // 只有两种点数可能是四条或满堂红 handRank (freq[points[0]] 4) ? 8 : 7; } else if (isFlush) handRank 6; else if (isStraight) handRank 5; else if (freq.size() 3) { // 三种点数可能是三条或两对 handRank (freq[points[0]] 3) ? 4 : 3; } else if (freq.size() 4) handRank 2; // 一对 // 组装特征值 long long value handRank; for (int p : points) { value value * 100 p; // 每个点数用两位十进制数表示足够 } return value; } int main() { string line; while (getline(cin, line) line ! #) { // 解析两手牌每手5张共10张牌 // ... 解析逻辑存入cards1, cards2 vectorpairint, char cards1, cards2; // 假设已解析并排序 long long val1 getHandValue(cards1); long long val2 getHandValue(cards2); if (val1 val2) cout Black wins endl; else if (val1 val2) cout White wins endl; else cout Tie endl; } return 0; }C实现心得使用std::pairint, char存储点数和花色很方便。all_of算法用于判断同花很简洁。特征值用十进制拼接点数是一种取巧但有效的方法避免了位运算的复杂性且比较结果正确。因为牌型编码在最高位决定了大小。排序时使用lambda表达式捕获频率字典freq是关键操作。4.2 Java 实现要点Java实现更注重面向对象和代码的清晰度。可以定义一个Card类和一个Hand类。import java.util.*; class Card implements ComparableCard { int point; char suit; Card(String s) { // 解析如“SA”的字符串 suit s.charAt(0); char p s.charAt(1); point switch (p) { case A - 14; case K - 13; case Q - 12; case J - 11; case T - 10; default - p - 0; }; } Override public int compareTo(Card o) { // 降序排序 return o.point - this.point; } } public class Main { private static long getHandValue(ListCard hand) { hand.sort(Comparator.naturalOrder()); // 按点数降序 // 判断同花 boolean isFlush hand.stream().map(c - c.suit).distinct().count() 1; // 判断顺子 ListInteger points hand.stream().map(c - c.point).collect(Collectors.toList()); boolean isStraight true; for (int i 1; i points.size(); i) { if (points.get(i - 1) - points.get(i) ! 1) { isStraight false; break; } } // 处理 A,2,3,4,5 if (!isStraight points.equals(Arrays.asList(14, 5, 4, 3, 2))) { isStraight true; points Arrays.asList(5, 4, 3, 2, 1); // 需要重新排序hand吗为了后续频率统计我们基于新的points构建频率映射但hand列表本身顺序不影响后续“按频率和点数排序” } // 统计频率 MapInteger, Integer freq new HashMap(); for (int p : points) freq.put(p, freq.getOrDefault(p, 0) 1); // 按频率降序点数降序排序手牌 hand.sort((a, b) - { int freqCmp freq.get(b.point) - freq.get(a.point); if (freqCmp ! 0) return freqCmp; return b.point - a.point; }); // 确定牌型 int handRank 1; if (isStraight isFlush) { handRank (hand.get(0).point 14 hand.get(4).point 10) ? 10 : 9; } else if (freq.size() 2) { handRank (freq.get(hand.get(0).point) 4) ? 8 : 7; } else if (isFlush) handRank 6; else if (isStraight) handRank 5; else if (freq.size() 3) { handRank (freq.get(hand.get(0).point) 3) ? 4 : 3; } else if (freq.size() 4) handRank 2; // 组装特征值 long value handRank; for (Card c : hand) { value value * 100 c.point; } return value; } public static void main(String[] args) { Scanner sc new Scanner(System.in); while (sc.hasNextLine()) { String line sc.nextLine().trim(); if (#.equals(line)) break; String[] tokens line.split(\\s); // 解析牌... ListCard black new ArrayList(); ListCard white new ArrayList(); for (int i 0; i 5; i) black.add(new Card(tokens[i])); for (int i 5; i 10; i) white.add(new Card(tokens[i])); long blackVal getHandValue(black); long whiteVal getHandValue(white); if (blackVal whiteVal) System.out.println(Black wins); else if (blackVal whiteVal) System.out.println(White wins); else System.out.println(Tie); } sc.close(); } }Java实现心得使用Card类使数据更清晰。实现Comparable接口便于初始排序。Java 8的Stream API让判断同花、收集点数等操作非常优雅。频率统计使用HashMap排序时在Comparator中动态查询频率。同样采用十进制拼接法生成特征值简单可靠。4.3 Python 实现要点Python实现最为简洁利用其强大的列表推导式和内置排序可以快速实现核心逻辑。def get_point(p_char): point_map {A: 14, K: 13, Q: 12, J: 11, T: 10} return point_map.get(p_char) or int(p_char) def get_hand_value(cards): # cards: list of tuple (point, suit) # 1. 按点数降序排序 cards.sort(keylambda x: x[0], reverseTrue) points [c[0] for c in cards] suits [c[1] for c in cards] # 2. 判断同花 is_flush len(set(suits)) 1 # 3. 判断顺子 is_straight all(points[i] - points[i1] 1 for i in range(4)) # 处理 A,2,3,4,5 if not is_straight and points [14, 5, 4, 3, 2]: is_straight True points [5, 4, 3, 2, 1] # 注意这里修改了points但cards未变。后续频率统计需基于此新points。 # 我们需要同步更新cards中的点数吗不我们后续排序依赖的是频率和点数我们可以基于新的points构建频率字典。 # 但cards列表的顺序和点数已经和points不一致了。更稳妥的方法是重新生成一个基于新points的“虚拟”手牌列表用于频率统计和排序。 # 简化处理我们直接操作points列表并记住此时A已被视为1。 # 4. 统计频率并生成排序依据 from collections import Counter freq Counter(points) # 关键步骤按频率降序点数降序对points排序。注意这里排序的是points列表但顺序决定了比较序列。 # 我们不能直接对points排序因为会打乱频率关联。我们需要对cards列表或points列表按照自定义规则排序。 # 更清晰的做法对cards列表进行排序排序key为 ( -freq[point], -point ) sorted_cards sorted(cards, keylambda c: (-freq[c[0]], -c[0])) sorted_points [c[0] for c in sorted_cards] # 5. 确定牌型 hand_rank 1 # 高牌 if is_straight and is_flush: hand_rank 10 if (sorted_points[0] 14 and sorted_points[4] 10) else 9 elif len(freq) 2: hand_rank 8 if (freq[sorted_points[0]] 4) else 7 elif is_flush: hand_rank 6 elif is_straight: hand_rank 5 elif len(freq) 3: hand_rank 4 if (freq[sorted_points[0]] 3) else 3 elif len(freq) 4: hand_rank 2 # 6. 组装特征值 value hand_rank for p in sorted_points: value value * 100 p return value def main(): import sys for line in sys.stdin: line line.strip() if line #: break tokens line.split() black_cards [(get_point(t[1]), t[0]) for t in tokens[:5]] # 假设输入格式是“花色点数”如“SA” white_cards [(get_point(t[1]), t[0]) for t in tokens[5:10]] black_val get_hand_value(black_cards) white_val get_hand_value(white_cards) if black_val white_val: print(Black wins) elif black_val white_val: print(White wins) else: print(Tie) if __name__ __main__: main()Python实现心得collections.Counter是频率统计的神器。sorted(cards, keylambda c: (-freq[c[0]], -c[0]))这行代码是核心完美实现了按频率和点数降序排序。处理A-5顺子时直接修改points列表并记住A视为1但后续频率统计和排序必须基于这个新的points列表。这里需要小心处理原始cards列表与points的一致性。上述代码通过基于points生成频率字典并在排序key中使用freq[c[0]]实际上依赖的是更新后的points对应的频率而c[0]来自cards其点数在A-5顺子情况下未变。这会导致错误。正确的做法是在判断为A-5顺子后创建一个新的cards列表其中A的点数替换为1然后基于这个新列表进行后续所有操作。这是Python实现中一个容易忽略的坑。十进制拼接特征值在Python中同样有效因为Python整数无限大。5. 常见陷阱与调试技巧这道题看似规则明确但实际编码时陷阱不少。下面是我在练习和实战中总结的几个关键点和排查技巧。5.1 高频错误点清单A在顺子中的双重身份这是最大的坑。必须单独处理A,2,3,4,5这种特殊情况。判断顺子时如果常规判断失败要检查排序后的点数是否为[14,5,4,3,2]。如果是不仅要标记为顺子还要在后续比较中将A的点数视为1即最大牌是5。牌型判断顺序同花顺既是同花也是顺子必须优先判断。如果先判断了同花或顺子就漏掉了同花顺。逻辑上应该是先检查是否同花且顺子-皇家/同花顺再检查其他牌型。同牌型比较规则特别是两对和一对的比较。两对要先比大对子再比小对子最后比单牌。一对则是先比对子点数再依次比剩下的单牌。实现时通过“按频率和点数降序排序”生成的序列已经天然符合了这个比较顺序这是该解法最巧妙的地方。输入格式解析题目中点数‘10’可能用字符‘T’表示而不是字符串“10”。务必确认题目说明。花色也可能用不同字母。特征值生成溢出如果用位运算要确保有足够的位数如64位。用十进制拼接*100则要确保点数不超过99牌型编码是个位数5张牌拼接后也不会超过64位整数的范围约9e18是安全的。平局处理所有牌都完全一样才叫Tie。我们的特征值包含了所有点数信息如果特征值相等则一定是平局。5.2 调试与测试策略自己测试时要构造覆盖所有牌型和边缘情况的测试用例。# 示例测试集 test_cases [ # (黑手牌 白手牌 预期结果) (SA SK SQ SJ S10, HA HK HQ HJ H10, Tie), # 都是皇家同花顺 (S9 S8 S7 S6 S5, H9 H8 H7 H6 H5, Tie), # 都是同花顺 (SA SA SA SA S2, HA HA HA HA H3, White wins), # 四条比较单牌 (SA SA SA S2 S2, HA HA HA H3 H3, Black wins), # 满堂红比较三条部分 (SA S10 S8 S6 S4, HA H10 H8 H6 H4, Tie), # 同花依次比较 (SA SK SQ SJ S9, HA HK HQ HJ H8, Black wins), # 同花比较第五张 (SA SK SQ SJ S10, HA HK HQ HJ C10, Black wins), # 皇家同花顺 vs 同花顺 (S2 S3 S4 S5 SA, H2 H3 H4 H5 H6, White wins), # A-5顺子 vs 6高顺子 (S2 S3 S4 S5 SA, H2 H3 H4 H5 HA, Tie), # 都是A-5顺子 ]调试技巧打印中间变量在getHandValue函数中打印出排序后的牌、判断出的isFlush/isStraight、频率字典、最终的特征值。对比两手牌的特征值看大小是否符合预期。重点关注边界专门测试A-5顺子、两对中大小对子相同但单牌不同、一对中单牌顺序等容易出错的比较场景。使用在线OJ的样例通常题目会提供几个样例输入输出务必确保全部通过。5.3 性能优化与小技巧一次排序多处使用初始按点数降序排序便于判断顺子和生成频率前的准备。特征值比较法将复杂的多规则比较转化为一次整数比较极大简化了主逻辑。使用查表法解析点数将‘A’,‘K’等映射到数字比一连串的if-else更清晰高效。在Python中使用itertools.combinations如果题目变体涉及7张牌选5张的最优牌型真实德州扑克则需要遍历所有5张牌组合。此时itertools.combinations是利器但本题通常不涉及。这道“德州扑克”题是华为OD机试中一道经典的中等难度题。它不涉及高深的图论或动态规划但对编程者的逻辑严谨性、抽象能力、代码实现细节和测试能力提出了全面要求。掌握这种“规则抽象特征编码”的解题范式不仅能帮你通过这道题更能提升你解决复杂业务逻辑编程的能力。在最后我建议在理解上述思路后抛开参考代码自己从头实现一遍过程中遇到的每一个问题都会让你对这道题和编程本身有更深的理解。