如果你刷过华为OD机试的题单,大概率会碰到这么一道:斗地主跑得快·最长顺子判定。这道题在机试里出现的频率不低,但翻车率也很高——不是算法有多难,而是很多人都把“牌”当成普通数组处理,忽略了顺子的几个硬性规则。规则说起来谁都知道,可真要写成C++代码,牌的编号怎么映射、2能不能进顺子、重复牌要不要管,全是坑。这篇文章就把这题彻底拆开讲一遍,从出题思路、算法选型到可AC的完整代码,带着你从头到尾走通。
适合三类人看:马上要参加华为OD机试、正在刷C++算法题的求职者,以及想用“斗地主”场景练手的人。就算你没打过牌,只要理解“连续递增数字”这个概念,也能把这题做出来。
1. 先搞清楚这道题到底在考什么
1.1 华为OD机试为什么爱出这种游戏场景题
华为OD机试的算法题有个很明显的倾向——不喜欢出干巴巴的模板题,而是喜欢套一层生活场景:打牌、排队、分糖果、走迷宫。场景一包装,很多人的第一反应不是“这考的是哪个知识点”,而是先慌着翻译题目,结果把背后真正的基础算法给忘了。斗地主顺子就是这个套路,它考的是“在游戏规则约束下,你能不能把最长连续子序列这种基础操作用好”。
另一个原因是,机试需要覆盖大量考生,游戏题能把难度控制得“看起来不难”,但真正动手写代码时,细节又不少。就拿这题来说,C++要写对,至少得涉及数组映射、去重、排序或哈希、一趟遍历维护连续长度这几个基本功。把这些串在一起,刚好能筛掉一部分代码能力不过关的人。
1.2 顺子的规则别凭感觉,先定清楚
这里先说一个基准:本文按斗地主最通用的规则来。顺子要求至少5张连续递增的牌,2和大小王不能进顺子,A可以接在K后面,形成10、J、Q、K、A。有些地方的跑得快玩法允许2进顺子,有的地方连对也算一种牌型,但机试一般默认的是大众规则,如果在考试里遇到具体题面,以题面描述为准。
真正容易搞错的是A的位置。输入里“1”代表A,它既可以接在K后面当最大的牌,也可以在某些玩法里当最小的1,但由于顺子中不允许出现2,所以A后面不能再往上接。写代码前把规则写进注释里,这是个好习惯,能避免后面调试时自己都忘了当初怎么设计的。
下表是常见规则的对比:
| 牌型规则 | 斗地主 | 跑得快(常见版本) |
|---|---|---|
| 顺子最小长度 | 5张 | 5张 |
| 2是否可入顺子 | 否 | 大部分地区否 |
| 大小王是否可入顺子 | 否 | 否 |
| A的位置 | 可接在K后 | 可接在K后 |
1.3 把牌的外衣剥掉,核心问题是什么
把牌面抽象掉之后,这个题就变成了:给定一个整数集合(可能有重复),找出其中最长的一段连续递增整数,并且这段长度至少为5才有意义。
这里有个非常关键的认知转变:顺子的本质是“这个数字有没有”,跟你手牌里的顺序无关,也跟你每种牌有几张无关。你手里有3、3、4、5、6、7,照样能出34567,多的那张3不影响结果。
所以整个题目的预处理思路就是:去重 → 排序 → 找最长连续段。这一步想通了,题目难度直接降半档。很多人翻车就是因为死盯着原始手牌顺序不放,总想着在里面找一个连续的子数组,方向错了代码怎么写都别扭。
2. 从暴力到标准解,三种思路一次讲透
2.1 最直白的暴力枚举
最简单粗暴的做法是把去重后的数字排序,然后以每个牌面为起点,向后逐一检查连续的牌是否存在。因为牌面范围很小,满打满算也就3到14共12种,所以暴力算起来也不会超时,适合在比赛时间紧的时候先拿分。
这种做法最大的优点是不容易写错。但缺点也很明显:一旦题目要求输出具体顺子牌型,或者要求你考虑剩余牌的出牌次数,纯暴力的代码会越来越臃肿,临时容器一堆,逻辑容易乱,改起来也痛苦。
2.2 排序后一趟扫描:机试里的标准答案
大多数参考解法用的都是这个思路:
- 把A映射成14,过滤掉2和王。
- 用一个布尔数组标记哪些牌面出现过,天然去重。
- 按牌面从小到大收集存在的数字。
- 一次遍历,如果当前数字等于前一个数字加1,就把当前连续长度加1;否则重置连续长度为1。
- 每次更新最大长度,最终判断是否大于等于5。
这种方案的时间复杂度是O(N logN)的排序或者O(N)(牌面范围固定时其实可以做成线性),空间复杂度O(1),因为数组长度固定。代码量小、逻辑清晰、不容易出bug,是我在机试现场最推荐的写法。
2.3 类似最长连续序列的哈希法
如果你刷过LeetCode第128题“最长连续序列”,会想到另一种思路:把所有存在的牌面放进一个哈希集合,然后遍历集合里的每个数字,只有当它是某段连续序列的起点时,才试着向后扩展,从而避免重复计数。
这个思路的复杂度也是线性的,每个数字最多被访问两次。不过说句实在话,本题的牌面范围太小,用布尔数组和哈希集合在实际执行上几乎没有差别。但这个方法值得掌握,因为在处理更广义的连续序列题时,它能帮你建立“跳过非起点”的优化意识。
2.4 三种方案怎么选,直接看这张表
| 方案 | 时间复杂度 | 空间复杂度 | 代码量 | 适用场景 |
|---|---|---|---|---|
| 暴力枚举 | O(K^2),K为牌面种类 | O(K) | 小 | 时间紧,只求AC |
| 排序+一趟扫描 | O(N logN)或O(N) | O(1) | 最小 | 机试首选 |
| 哈希集合+起点跳过 | O(N) | O(N) | 中 | 通用连续序列题 |
我的建议是:平时练习把第三种也写明白,真正上机考试时用第二种,因为它在任何输入规模下都足够快,而且出错概率最低。
3. C++实现详解,代码拆到每一行
3.1 输入映射:A为什么要变成14
题目输入用1表示A,2表示2,3到10表示3到10,11、12、13分别表示J、Q、K。为了让整个顺子处理变成一个纯粹的连续整数序列,我在读入时就做映射:读到1就把它变成14,读到2直接跳过,这样3到A就等价于3到14的连续序列,10、J、Q、K、A也就变成了10、11、12、13、14。
这一步不是为了花哨,而是为了把后面的连续判断统一。如果你保留1在数组最前面,就得额外处理A是接在K后面还是当小牌用,逻辑复杂不说,还容易漏掉边界情况。
3.2 用存在性数组去重,而不是set
牌面范围很小,所以我直接申请一个长度为15的整型数组 exist,下标对应牌面值,读到一张牌就把它标记为1。这个数组同时完成了去重和过滤两件事。
为什么不推荐用 unordered_set?因为本题牌面种类最多12种,哈希集合的常数开销反而成了一种浪费,而且你还得额外保证过滤规则不搞错。数组下标本身就是牌面值,代码读起来一目了然,也不会有哈希冲突问题。
3.3 完整可运行的C++代码
下面是完整的C++17代码,直接复制就能编译运行:
#include <bits/stdc++.h> using namespace std; int main() { ios::sync_with_stdio(false); cin.tie(nullptr); int n; cin >> n; vector<int> exist(15, 0); for (int i = 0; i < n; ++i) { int v; cin >> v; if (v == 1) { v = 14; // A映射到14,保证3~14连续 } if (v <= 2) continue; // 2和大小王不进顺子 if (v >= 3 && v <= 14) { exist[v] = 1; // 去重:只要出现过就标记 } } vector<int> nums; for (int i = 3; i <= 14; ++i) { if (exist[i]) { nums.push_back(i); } } int maxLen = 0; int curLen = 0; for (int i = 0; i < (int)nums.size(); ++i) { if (i > 0 && nums[i] == nums[i - 1] + 1) { ++curLen; } else { curLen = 1; } if (curLen > maxLen) { maxLen = curLen; } } cout << (maxLen >= 5 ? maxLen : 0) << '\n'; return 0; }这里有一个小技巧需要注意:如果牌面集合为空,也就是所有手牌都是2、王或者其他被过滤掉的牌,nums是空的,循环一次都不会执行,maxLen保持0,最终输出0,这个边界是天然正确的。
3.4 为什么建议你不要用vector<bool>
代码里我用的是 vector ,而不是 vector 。C++里的 vector 是一个特殊模板,它为了节省空间按位存储布尔值,导致元素访问返回的是一个代理对象,而不是真正的引用。很多人第一次用时直接对 vector 的元素取地址,编译就报错了,这种问题在机试环境下特别浪费时间。
刷题时求稳,就用 vector 或 bool exist[15] 这样的普通数组。你牺牲一点点空间,换来的是没有任何怪癖的行为。
3.5 现场可以快速手写的简化版
如果你已经把思路烂熟于心,考试时也可以直接对原始数组排序,去重后用双指针扫描,不用显式构造exist数组。核心代码如下:
sort(v.begin(), v.end()); v.erase(unique(v.begin(), v.end()), v.end()); int maxLen = 0, cur = 0; for (int i = 0; i < (int)v.size(); ++i) { if (i == 0 || v[i] == v[i - 1] + 1) cur++; else cur = 1; maxLen = max(maxLen, cur); }不过用这个简化版之前,你必须保证已经处理好了两件事:A映射成14,2和王已被排除。如果输入里的1没转换,排序后1会出现在3前面,连续段会被错误切分。
4. 实操中的坑,这些细节决定你能不能AC
4.1 重复牌到底怎么处理
最常见的翻车点就是重复牌。出顺子的时候,每种牌只需要出一张,你手里如果有3、3、4、5、6、7,依然能组出34567。所以重复的牌不会让顺子变长,去重是对的。
但有一个前提你要想清楚:如果题目问的不是“最长顺子长度”,而是“最多能打出几手顺子”或者“剩牌尽可能少”,那重复牌就必须按数量来考虑了。看到题目先抓住关键词,是最长长度,是输出一手具体顺子,还是问出牌次数,不同问法对应完全不同的算法。
4.2 2和王千万别混进连续序列
很多人在这一步意外翻车。输入里可能有2,也可能用特殊值表示大小王。顺子规则里2和王都不能参与,如果你提前不把它们过滤掉,那么2、3、4、5、6这组数据会算出一个长度为5的假顺子,样例一通过,实际测试直接挂掉。
我习惯的做法是在读入阶段就直接丢弃,而不是等到排序后判断。因为排序后你还要额外判断“当前牌面是不是2”,读入时过滤,逻辑更集中。
4.3 找不到顺子要输出0,别输出最大长度
输出部分看起来没什么,实际上也有隐藏条件。题目要求的是:如果最长连续段长度小于5,就输出0。很多人在本地测试时只输出了maxLen,结果某个边界用例没有顺子,输出一个2或3,被判错。
用三元表达式 quick写一行就行,这就是上面代码里cout << (maxLen >= 5 ? maxLen : 0)的作用。
4.4 常见错误速查表
| 错误类型 | 典型表现 | 正确做法 |
|---|---|---|
| A未映射 | 10JQKA被拆成10、11、12、13、1 | 读入时把1转成14 |
| 2未过滤 | 2、3、4、5、6被当成顺子 | 读入时跳过v<=2的牌 |
| 重复牌没去重 | 33345被当成345,长度3,其实可以出顺子 | 用存在性数组或unique去重 |
| 长度边界错 | 长度为4也输出 | 最终长度必须>=5 |
| 用排序后的原始顺序 | 连续段被重复牌中断 | 先去重再找连续段 |
4.5 机试现场的调试技巧
真到了机试环境下,遇到这题我建议你30秒内先在纸上写输入输出样例,然后通过样例走一遍算法。尤其是连续段中断的那个分支,比如牌面是3、4、5、7、8、9、10、11,遍历到7时发现不连续,curLen要重置为1,很多人在这里会误写成重置为0,导致下一段长度从0计起,少算一段。
这种小问题,平时多练几组手工样例比空想有效得多。拿副扑克牌洗一洗,随机抽一把,在纸上数一遍,比你刷十道同样模板的题都管用。
5. 真题变种与扩展,一次准备到位
5.1 变种:要求输出最长顺子的具体牌型
有些版本不要求输出长度,而是要求输出类似“3-4-5-6-7”这样的字符串。这种情况下,只记录maxLen是不够的,你还得记录最长连续段的起始下标和长度。
思路很简单:在遍历时不仅更新maxLen,同时更新bestStart和bestLen,最后从nums数组中按下标截取这一段,转换成牌面字符串。注意A的位置,如果截取到14,你要输出的是“A”,而不是数字14。
5.2 变种:判断最多能拆出多少手顺子
这个难度就上一个台阶了。比如给你一堆牌,问你最多能打出几手合法顺子,那就不能简单去重了,得为每种牌记数量。一种常见做法是贪心从最小牌面开始,尽量组成最小的顺子,以给后续留空间;另一种是回溯搜索,在牌的数量不多时可以保证全局最优。
这类题在机试里偶尔会作为进阶问法出现,但大多数情况下,考到“最长顺子长度”这层就停了。了解扩展方向,对你理解贪心策略有好处。
5.3 延伸:连对和飞机的顺子逻辑
斗地主里还有连对(连续对子)和飞机(连续三张)的概念。它们本质上和顺子一样,都是“连续牌面”,区别在于每种牌需要的数量不同。连对需要每个牌面数量至少2张,飞机需要至少3张。如果你把“存在性数组”换成“计数数组”,再在判断连续段时加上数量条件,这类延伸题就顺手很多。
这也是为什么我不推荐总结死模板,而是建议你把“牌面映射+计数+连续段判断”这个框架掌握住。换一个玩法,改的只是数量条件,骨架完全不变。
5.4 扩展后的核心框架总结
对这个题及其变种,我的心得是:先把规则翻译成约束条件,再把手牌抽象成一张计数表,最后用连续段扫描去求解。只要这三步的代码结构清晰,无论是求长度、输出牌型,还是换连对飞机,都能很快改出来。
例:连对比顺子多一个条件,只需要把存在性判断改成“数量>=2”,其他流程一点不用动。
写在最后的个人体会
这道题我前前后后给不下十个准备机试的朋友讲过,大家最容易卡住的地方惊人地一致:没有把牌面重新映射就急着排序。只要把A转成14、踢掉2这一步做对,整道题基本就通了一半。如果你临近考试,别只背代码,建议你手动跑几个例子,把“映射→去重→数连续”这个流程写在纸上画一遍,比盲目刷题管用得多。这种题看似简单,但它考察的恰恰是你在约束条件下把基础算法用对的能力,而这正是机试最看重的。