news 2026/10/10 12:43:11

华为OD机试斗地主最长顺子判定:C++映射去重与连续序列扫描详解

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
华为OD机试斗地主最长顺子判定:C++映射去重与连续序列扫描详解

如果你刷过华为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 排序后一趟扫描:机试里的标准答案

大多数参考解法用的都是这个思路:

  1. 把A映射成14,过滤掉2和王。
  2. 用一个布尔数组标记哪些牌面出现过,天然去重。
  3. 按牌面从小到大收集存在的数字。
  4. 一次遍历,如果当前数字等于前一个数字加1,就把当前连续长度加1;否则重置连续长度为1。
  5. 每次更新最大长度,最终判断是否大于等于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这一步做对,整道题基本就通了一半。如果你临近考试,别只背代码,建议你手动跑几个例子,把“映射→去重→数连续”这个流程写在纸上画一遍,比盲目刷题管用得多。这种题看似简单,但它考察的恰恰是你在约束条件下把基础算法用对的能力,而这正是机试最看重的。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/10 12:40:32

PyCharm编辑器背景颜色修改全攻略:新旧版本入口与三套护眼配色

打开 PyCharm 默认的白色背景&#xff0c;连续盯上几个小时&#xff0c;眼睛是真的受不了。我最早是直接去 Settings 里翻“Background”这个关键词&#xff0c;结果翻到的是主题切换&#xff0c;并不是编辑器代码区的背景色&#xff0c;折腾半天也没改成想要的效果。后来才发现…

作者头像 李华
网站建设 2026/10/10 12:40:05

Java大厂面试实战:Spring Boot微服务与MySQL数据库调优通关攻略

考虑到面试官手边大概率正摊着你的简历&#xff0c;你需要在半小时内用技术深度支撑起“熟练Spring Boot”“理解微服务”“熟悉MySQL”这三句自我评价。这篇内容的定位不是面经合集&#xff0c;而是一条按真实面试节奏组织的主线&#xff1a;从项目本身出发&#xff0c;依次穿…

作者头像 李华
网站建设 2026/10/10 12:39:22

Git 历史重写:用 filter-repo 清理误提交的大文件与敏感信息

如果你只是平时用git add、git commit、git push这套流程&#xff0c;大概率不会注意到.git目录其实是个只进不出的垃圾场。前阵子我接手一个历史项目&#xff0c;代码总量 600M&#xff0c;.git目录却膨胀到 1.8G&#xff0c;排查了半天&#xff0c;发现是两年前有人把整个构建…

作者头像 李华
网站建设 2026/10/10 12:38:53

计算机网络高频计算题手算全攻略:从CRC到RSA

期末复习计网的日子&#xff0c;对计科的同学来说总有点魔幻&#xff1a;明明是一门讲协议的课&#xff0c;背起来却像文科&#xff1b;可一到考试&#xff0c;满卷子都是计算题。我当年就是吃了这个亏——概念背得滚瓜烂熟&#xff0c;翻开“计科-计网8-计算题”这个整理文件夹…

作者头像 李华
网站建设 2026/10/10 12:38:13

Linux进程控制基石:fork、wait、exec实战详解与坑点

fork、wait、exec&#xff0c;这三个系统调用是Linux进程控制的基石。无论你是做嵌入式开发、写后台服务、维护运维脚本&#xff0c;还是准备Linux岗位的面试&#xff0c;都绕不开它们。这篇文章我会从最基础的概念讲起&#xff0c;用手写C代码的方式&#xff0c;把进程创建、回…

作者头像 李华