1. 手牌接龙问题背景与核心逻辑
手牌接龙是华为OD机试中常见的算法题型,主要考察候选人对深度优先搜索(DFS)的应用能力。题目通常给出若干张手牌,每张牌有特定数值,要求玩家按照"后一张牌数值必须大于前一张"的规则依次出牌,计算能够连续出牌的最大次数。
这个问题的核心在于如何高效遍历所有可能的出牌顺序,找到最长递增序列。实际机试中,牌的数量通常在10-20张之间,时间复杂度成为关键考量。我参加过多次华为OD的模拟考试,发现这类题目往往设置两个考察维度:基础逻辑实现和算法优化能力。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 问题建模与数据结构选择
2.1 输入输出规范
典型的输入格式为一行用空格分隔的整数,表示手牌数值。例如:
code复制1 3 2 4 5 2
输出应为最大出牌次数,如上例应输出4(对应序列1->3->4->5或1->2->4->5)。
2.2 核心数据结构
使用vector存储手牌是最直接的选择:
cpp复制vector<int> cards;
考虑到需要频繁查找和比较,可以预先对牌组排序。但要注意原始顺序可能影响结果,所以排序前需要保留原始序列。
3. DFS算法实现详解
3.1 基础DFS框架
cpp复制int maxLength = 0;
void dfs(vector<int>& cards, int index, int lastNum, int currentLength) {
maxLength = max(maxLength, currentLength);
for (int i = index; i < cards.size(); ++i) {
if (cards[i] > lastNum) {
dfs(cards, i + 1, cards[i], currentLength + 1);
}
}
}
这个基础版本存在明显效率问题:当cards=[1,2,3,4,5,6,7,8]时,时间复杂度达到O(2^n)。
3.2 记忆化优化
添加memo数组存储中间结果:
cpp复制vector<vector<int>> memo; // memo[i][j]表示从第i张牌开始,上张牌为j时的最大长度
int dfs(vector<int>& cards, int index, int lastIndex) {
if (index >= cards.size()) return 0;
if (lastIndex != -1 && memo[index][lastIndex] != -1)
return memo[index][lastIndex];
int maxLen = 0;
for (int i = index; i < cards.size(); ++i) {
if (lastIndex == -1 || cards[i] > cards[lastIndex]) {
maxLen = max(maxLen, 1 + dfs(cards, i + 1, i));
}
}
if (lastIndex != -1)
memo[index][lastIndex] = maxLen;
return maxLen;
}
初始化memo为cards.size() × cards.size()的-1矩阵。
4. 动态规划解法对比
4.1 DP状态定义
定义dp[i]为以第i张牌结尾的最长子序列长度:
cpp复制vector<int> dp(n, 1);
for (int i = 1; i < n; ++i) {
for (int j = 0; j < i; ++j) {
if (cards[i] > cards[j]) {
dp[i] = max(dp[i], dp[j] + 1);
}
}
}
return *max_element(dp.begin(), dp.end());
时间复杂度O(n²),空间复杂度O(n)。
4.2 贪心+二分优化
维护一个递增序列tails,其中tails[i]表示长度为i+1的子序列的最小末尾:
cpp复制vector<int> tails;
for (int num : cards) {
auto it = lower_bound(tails.begin(), tails.end(), num);
if (it == tails.end()) {
tails.push_back(num);
} else {
*it = num;
}
}
return tails.size();
将时间复杂度优化到O(nlogn)。
5. 华为OD机试实战技巧
5.1 输入处理规范
华为OD机试通常需要处理标准输入:
cpp复制vector<int> readCards() {
vector<int> cards;
string line;
getline(cin, line);
stringstream ss(line);
int num;
while (ss >> num) {
cards.push_back(num);
}
return cards;
}
5.2 边界条件处理
特别注意以下case:
- 空输入(返回0)
- 所有牌相同(返回1)
- 严格递减序列(返回1)
- 包含重复值的序列(如[1,2,2,3]应返回3)
5.3 性能优化技巧
- 提前终止:当剩余牌数+当前长度 ≤ maxLength时直接返回
- 预处理:对相同数值的牌去重(需确认题目规则)
- 剪枝:优先尝试较大的牌可以减少递归深度
6. 完整参考代码
cpp复制#include <iostream>
#include <vector>
#include <algorithm>
#include <sstream>
using namespace std;
int maxLength = 0;
void dfs(vector<int>& cards, int index, int lastNum, int currentLength) {
if (currentLength + cards.size() - index <= maxLength) return;
maxLength = max(maxLength, currentLength);
for (int i = index; i < cards.size(); ++i) {
if (cards[i] > lastNum) {
dfs(cards, i + 1, cards[i], currentLength + 1);
}
}
}
int main() {
vector<int> cards;
string line;
getline(cin, line);
stringstream ss(line);
int num;
while (ss >> num) {
cards.push_back(num);
}
if (cards.empty()) {
cout << 0 << endl;
return 0;
}
dfs(cards, 0, -1, 0);
cout << maxLength << endl;
return 0;
}
7. 常见错误与调试技巧
7.1 典型错误案例
- 未初始化maxLength导致输出随机值
- 递归终止条件遗漏造成栈溢出
- 输入处理时未考虑行尾换行符
- 使用全局变量时未在多次测试间重置
7.2 调试建议
- 打印关键变量:在dfs入口处输出index和currentLength
- 小规模测试:先用[1,2,3]这样的简单case验证
- 边界测试:空输入、单元素、全相同元素等特殊情况
- 性能分析:对于n=20的输入,检查是否能在1s内完成
8. 算法扩展与变种
8.1 变种题型
- 允许相等数值出牌(将>改为>=)
- 限制出牌间隔(如必须隔k张牌)
- 多维属性(同时满足数值和花色条件)
- 带权值求最大得分(每张牌有得分值)
8.2 实际应用场景
- 游戏AI中的出牌策略
- 序列模式识别
- 任务调度中的依赖关系处理
- 基因序列比对
在华为OD的后续面试环节中,面试官可能会基于这个题目延伸讨论动态规划与DFS的适用场景。我的经验是:当n≤20时DFS更直观,n较大时必须使用DP;如果被问到最优解法,应该能脱口而出贪心+二分的O(nlogn)方案。
