2025 A卷 100分 题型
本文涵盖详细的问题分析、解题思路、代码实现、代码详解、测试用例以及综合分析;
并提供Java、python、JavaScript、C++、C语言、GO六种语言的最佳实现方式!
本文收录于专栏:《2025华为OD真题目录+全流程解析/备考攻略/经验分享》
华为OD机试真题《最长的顺子》:
目录
- 题目名称:最长的顺子
- 题目描述
- Java
- 问题分析
- 解题思路
- 代码实现
- 代码详细解析
- 示例测试
- 综合分析
- python
- 问题分析
- 解题思路
- 代码实现
- 代码详细解析
- 示例测试
- 综合分析
- JavaScript
- 问题分析
- 解题思路
- 代码实现
- 代码详细解析
- 示例测试
- 综合分析
- C++
- 问题分析
- 解题思路
- 代码实现
- 代码详细解析
- 示例测试
- 示例1输入:
- 示例2输入(无顺子):
- 示例3输入(完整顺子):
- 综合分析
- C语言
- 问题分析
- 解题思路
- 代码实现
- 代码详细解析
- 示例测试
- 示例1输入:
- 示例2输入(无顺子):
- 示例3输入(最长顺子):
- 综合分析
- GO
- 问题分析
- 解题思路
- 代码实现
- 代码详细解析
- 示例测试
- 示例1输入:
- 示例2输入(无顺子):
- 示例3输入(最长顺子):
- 综合分析
- 更多内容:
题目名称:最长的顺子
知识点:字符串、动态规划/滑动窗口、逻辑处理
时间限制:1秒
空间限制:256MB
语言限制:不限
题目描述
输入当前手牌和已出牌,计算对手可能组成的最长顺子(连续5-12张牌,不含2和大小王)。若存在多个最长顺子,输出牌面最大的那个,否则返回"NO-CHAIN"。
规则说明
- 顺子定义:连续递增的牌序列,范围从3到A(3<4<5<6<7<8<9<10<J<Q<K<A),不含2、B(小王)、C(大王)。
- 输入格式:
- 第一行为当前手牌(如
3-3-4-5-A
) - 第二行为已出牌(如
A-4-5-6-7
)
- 第一行为当前手牌(如
- 输出格式:最长顺子,若长度相同则输出牌面最大的(如
9-10-J-Q-K-A
优于5-6-7-8-9
)。 - 数据约束:每张牌初始有4张(除大小王),总牌数为54张。
输入示例
3-3-4-4-5-A-5-6-2-8-3-9-10-Q-7-K-J-10-B
A-4-5-8-8-10-C-6-7-8
输出示例
9-10-J-Q-K-A
Java
问题分析
我们需要找到对手可能组成的最长顺子。顺子由3到A的连续牌组成,不含2和大小王。输出最长顺子,若存在多个相同长度的,选择牌面最大的那个。若无法组成,返回"NO-CHAIN"。
解题思路
- 统计剩余牌数:根据手牌和已出牌,计算每个有效牌的剩余数量。
- 滑动窗口遍历:从每个可能的起始牌开始,尽可能向右扩展窗口,直到遇到剩余不足的牌。
- 选择最优结果:记录最长窗口,若长度相同则选择末尾最大的窗口。
代码实现
import java.util.*;
public class Main {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
// 读取输入的两行并分割成牌列表
String handInput = scanner.nextLine().trim();
String playedInput = scanner.nextLine().trim();
List<String> allCards = new ArrayList<>();
if (!handInput.isEmpty()) {
allCards.addAll(Arrays.asList(handInput.split("-")));
}
if (!playedInput.isEmpty()) {
allCards.addAll(Arrays.asList(playedInput.split("-")));
}
// 有效牌的顺序和集合
String[] order = {"3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K", "A"};
Set<String> validCards = new HashSet<>(Arrays.asList(order));
// 统计每个牌的出现次数
Map<String, Integer> countMap = new HashMap<>();
for (String card : allCards) {
if (validCards.contains(card)) {
countMap.put(card, countMap.getOrDefault(card, 0) + 1);
}
}
// 计算每个牌的剩余次数
int[] remain = new int[order.length];
for (int i = 0; i < order.length; i++) {
String card = order[i];
int used = countMap.getOrDefault(card, 0);
remain[i] = 4 - used; // 每张牌最多4张
}
// 滑动窗口遍历寻找最长顺子
int maxLen = 0;
int bestStart = 0;
int bestEnd = 0;
for (int i = 0; i < order.length; i++) {
if (remain[i] <= 0) continue; // 起始牌必须可用
int currentEnd = i;
// 向右扩展窗口,直到遇到不可用的牌
for (int j = i; j < order.length; j++) {
if (remain[j] <= 0) break;
currentEnd = j;
}
int currentLen = currentEnd - i + 1;
// 更新最长且牌面最大的顺子
if (currentLen >= 5) {
if (currentLen > maxLen || (currentLen == maxLen && currentEnd > bestEnd)) {
maxLen = currentLen;
bestStart = i;
bestEnd = currentEnd;
}
}
}
// 输出结果
if (maxLen >= 5) {
List<String> result = new ArrayList<>();
for (int i = bestStart; i <= bestEnd; i++) {
result.add(order[i]);
}
System.out.println(String.join("-", result));
} else {
System.out.println("NO-CHAIN");
}
}
}
代码详细解析
- 输入处理:读取手牌和已出牌,分割为列表。
- 有效牌集合:定义合法的顺子牌和顺序。
- 统计次数:遍历所有牌,统计每个有效牌的出现次数。
- 剩余计算:每种牌最多4张,剩余次数为
4 - 已用次数
。 - 滑动窗口:
- 外层遍历每个起始点
i
,若该牌剩余可用则继续。 - 内层扩展窗口
j
,直到遇到剩余为0的牌,记录窗口结束位置。 - 更新最长且最大的顺子。
- 外层遍历每个起始点
- 结果输出:根据窗口生成顺子字符串或输出“NO-CHAIN”。
示例测试
示例1输入:
3-3-4-4-5-A-5-6-2-8-3-9-10-Q-7-K-J-10-B
A-4-5-8-8-10-C-6-7-8
输出:9-10-J-Q-K-A
解析:剩余牌中9到A的连续序列最长且牌面最大。
示例2输入(无法形成顺子):
3-3-3-3
4-4-4-4
输出:NO-CHAIN
示例3输入(最长顺子):
3-4-5-6-7-8-9-10-J-Q-K-A
(空已出牌)
输出:3-4-5-6-7-8-9-10-J-Q-K-A
综合分析
- 时间复杂度:O(n²),n为有效牌数量(12),双重循环遍历。
- 空间复杂度:O(n),存储剩余次数和牌顺序。
- 优势:滑动窗口确保找到最优解,处理高效。
- 适用场景:适用于题目约束的小规模输入,快速找到最长顺子。
python
问题分析
我们需要找到对手可能组成的最长顺子。顺子由3到A的连续牌组成,不含2和大小王。输出最长顺子,若存在多个相同长度的,选择牌面最大的那个。若无法组成,返回"NO-CHAIN"。
解题思路
- 输入处理:将手牌和已出牌合并,统计每张牌的出现次数。
- 剩余牌计算:每张牌最多4张,减去已出现的次数得到剩余数量。
- 滑动窗口遍历:从每个可能的起点出发,寻找最长的连续可用牌序列。
- 最优顺子选择:记录最长且最大的顺子。
代码实现
def main():
# 读取输入的两行数据
hand = input().strip().split('-') if input().strip() != '' else []
played = input().strip().split('-') if input().strip() != '' else []
all_cards = hand + played
# 定义有效牌的顺序(从3到A)
order = ["3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K", "A"]
valid_cards = set(order) # 用于快速判断是否为有效牌
# 统计每张牌的使用次数
count = {}
for card in all_cards:
if card in valid_cards:
count[card] = count.get(card, 0) + 1
# 计算每张牌的剩余数量(总牌数为4)
remain = {card: 4 - count.get(card, 0) for card in order}
max_length = 0 # 最长顺子的长度
best_start = 0 # 最长顺子的起始索引
best_end = -1 # 最长顺子的结束索引
# 遍历所有可能的起始点
for i in range(len(order)):
# 如果当前牌不可用,跳过
if remain[order[i]] <= 0:
continue
current_end = i # 当前窗口的结束位置
# 从i出发,尽可能向右扩展窗口
for j in range(i, len(order)):
if remain[order[j]] <= 0:
break # 遇到不可用的牌,停止扩展
current_end = j
# 计算当前窗口的长度
current_length = current_end - i + 1
# 更新最长顺子(长度优先,其次比较结束索引)
if current_length >= 5:
if (current_length > max_length) or \
(current_length == max_length and current_end > best_end):
max_length = current_length
best_start = i
best_end = current_end
# 输出结果
if max_length >= 5:
chain = order[best_start : best_end+1]
print("-".join(chain))
else:
print("NO-CHAIN")
if __name__ == "__main__":
main()
代码详细解析
-
输入处理:
- 读取手牌和已出牌,分割为列表(
split('-')
)。 - 合并所有牌到
all_cards
列表。
- 读取手牌和已出牌,分割为列表(
-
有效牌定义:
order = ["3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K", "A"]
- 明确顺子的合法牌面和顺序。
-
统计剩余牌数:
remain = {card: 4 - count.get(card, 0) for card in order}
- 计算每张牌剩余可用次数(初始为4,减去已出现的次数)。
-
滑动窗口遍历:
- 外层循环遍历每个可能的起始点
i
。 - 内层循环从
i
出发向右扩展窗口,直到遇到不可用的牌。 - 记录当前窗口的结束位置
current_end
。
- 外层循环遍历每个可能的起始点
-
最优顺子选择:
- 如果当前窗口长度大于历史最大值,更新结果。
- 如果长度相同,但结束索引更大(即牌面更大),也更新结果。
-
结果输出:
- 根据记录的起始和结束索引生成顺子字符串。
- 若没有合法顺子,输出
NO-CHAIN
。
示例测试
示例1输入:
3-3-4-4-5-A-5-6-2-8-3-9-10-Q-7-K-J-10-B
A-4-5-8-8-10-C-6-7-8
输出:
9-10-J-Q-K-A
解析:
- 剩余牌中
9,10,J,Q,K,A
连续且全部可用,长度为6,是唯一最长顺子。
示例2输入(无顺子):
3-3-3-3
4-4-4-4
输出:
NO-CHAIN
解析:
- 所有牌的剩余次数为0,无法组成顺子。
示例3输入(最长顺子):
3-4-5-6-7-8-9-10-J-Q-K-A
(空已出牌)
输出:
3-4-5-6-7-8-9-10-J-Q-K-A
解析:
- 所有牌均未被使用,可以组成完整的12张顺子。
综合分析
-
时间复杂度:
- 滑动窗口遍历:外层循环12次,内层最多遍历12次,复杂度为 (O(n^2)),其中 (n=12)。
- 总复杂度为 (O(144)),完全满足题目约束。
-
空间复杂度:
- 存储牌的顺序和剩余次数,空间复杂度为 (O(n)),其中 (n=12)。
-
优势:
- 严格保证最优解:通过滑动窗口直接遍历所有可能性。
- 高效剪枝:遇到不可用牌立即停止扩展,减少计算量。
-
适用场景:
- 题目明确要求牌数 (n < 31),滑动窗口法在小规模数据中表现最佳。
JavaScript
问题分析
我们需要找到对手可能组成的最长顺子。顺子由3到A的连续牌组成,不含2和大小王。输出最长顺子,若存在多个相同长度的,选择牌面最大的那个。若无法组成,返回"NO-CHAIN"。
解题思路
- 输入处理:读取手牌和已出牌,合并并分割为数组。
- 统计次数:计算每个有效牌的出现次数。
- 剩余计算:每张牌最多4张,剩余次数为
4 - 已用次数
。 - 滑动窗口遍历:从每个可能的起点向右扩展,找到最长连续可用序列。
- 结果选择:记录最长且最大的顺子。
代码实现
const readline = require('readline');
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
terminal: false
});
let lines = [];
rl.on('line', (line) => {
lines.push(line.trim());
}).on('close', () => {
// 解析输入的两行数据
const hand = lines[0] ? lines[0].split('-') : [];
const played = lines[1] ? lines[1].split('-') : [];
const allCards = [...hand, ...played];
// 定义有效牌的顺序和集合
const order = ["3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K", "A"];
const validCards = new Set(order);
// 统计每张牌的出现次数
const count = {};
allCards.forEach(card => {
if (validCards.has(card)) {
count[card] = (count[card] || 0) + 1;
}
});
// 计算剩余次数(4 - 已用次数)
const remain = {};
order.forEach(card => {
remain[card] = 4 - (count[card] || 0);
});
let maxLength = 0; // 最长顺子的长度
let bestStart = 0; // 最长顺子的起始索引
let bestEnd = -1; // 最长顺子的结束索引
// 滑动窗口遍历所有可能的起始点
for (let i = 0; i < order.length; i++) {
const startCard = order[i];
if (remain[startCard] <= 0) continue; // 起始牌不可用则跳过
let currentEnd = i;
// 向右扩展窗口直到遇到不可用的牌
for (let j = i; j < order.length; j++) {
const currentCard = order[j];
if (remain[currentCard] <= 0) break;
currentEnd = j;
}
const currentLength = currentEnd - i + 1;
// 更新最优解(长度优先,其次比较末尾牌大小)
if (currentLength >= 5) {
if (
currentLength > maxLength ||
(currentLength === maxLength && currentEnd > bestEnd)
) {
maxLength = currentLength;
bestStart = i;
bestEnd = currentEnd;
}
}
}
// 输出结果
if (maxLength >= 5) {
const bestChain = order.slice(bestStart, bestEnd + 1).join('-');
console.log(bestChain);
} else {
console.log("NO-CHAIN");
}
});
代码详细解析
-
输入处理:
- 使用
readline
读取输入,存储到lines
数组。 - 将手牌和已出牌分割为数组,合并到
allCards
。
- 使用
-
有效牌定义:
const order = ["3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K", "A"]; const validCards = new Set(order);
- 明确合法的顺子牌及其顺序。
-
统计次数:
allCards.forEach(card => { if (validCards.has(card)) { count[card] = (count[card] || 0) + 1; } });
- 遍历所有牌,统计有效牌的出现次数。
-
剩余计算:
order.forEach(card => { remain[card] = 4 - (count[card] || 0); });
- 计算每个牌的剩余次数(总次数为4)。
-
滑动窗口遍历:
- 外层循环遍历每个可能的起始点
i
。 - 内层循环从
i
出发向右扩展窗口,直到遇到剩余为0的牌。 - 记录窗口的结束位置
currentEnd
。
- 外层循环遍历每个可能的起始点
-
结果选择:
- 如果当前窗口长度大于历史最大值,更新最优解。
- 如果长度相同但末尾牌更大(即
currentEnd
更大),也更新最优解。
-
输出结果:
- 生成顺子字符串(如
9-10-J-Q-K-A
)或输出NO-CHAIN
。
- 生成顺子字符串(如
示例测试
示例1输入:
3-3-4-4-5-A-5-6-2-8-3-9-10-Q-7-K-J-10-B
A-4-5-8-8-10-C-6-7-8
输出:
9-10-J-Q-K-A
解析:
- 剩余牌中
9,10,J,Q,K,A
连续可用,形成最长顺子。
示例2输入(无顺子):
3-3-3-3
4-4-4-4
输出:
NO-CHAIN
解析:
- 所有牌剩余次数为0,无法组成顺子。
示例3输入(最长顺子):
3-4-5-6-7-8-9-10-J-Q-K-A
(空已出牌)
输出:
3-4-5-6-7-8-9-10-J-Q-K-A
综合分析
-
时间复杂度:
- 滑动窗口遍历:外层循环12次,内层最多12次,复杂度为 (O(n^2)),其中 (n=12)。
- 总复杂度为 (O(144)),完全满足题目约束。
-
空间复杂度:
- 存储牌的顺序和剩余次数,空间复杂度为 (O(n)),其中 (n=12)。
-
优势:
- 严格保证最优解:滑动窗口直接遍历所有可能性。
- 高效剪枝:遇到不可用牌立即停止扩展,减少计算量。
-
适用场景:
- 题目明确要求牌数 (n < 31),滑动窗口法在小规模数据中表现最佳。
C++
问题分析
我们需要找出对手可能组成的最长顺子。顺子由3到A的连续牌组成,不含2和大小王。输出最长顺子,若存在多个相同长度的,选择牌面最大的那个。若无法组成,返回"NO-CHAIN"。
解题思路
- 输入处理:读取手牌和已出牌,分割成数组。
- 统计次数:过滤无效牌,统计每张牌的出现次数。
- 剩余计算:每张牌最多4张,剩余次数为
4 - 已用次数
。 - 滑动窗口遍历:从每个可能的起点扩展窗口,找到最长连续可用序列。
- 结果生成:记录最长且最大的顺子并输出。
代码实现
#include <iostream>
#include <vector>
#include <string>
#include <unordered_map>
#include <sstream>
#include <unordered_set>
using namespace std;
// 分割字符串函数
vector<string> split(const string &s, char delimiter) {
vector<string> tokens;
string token;
istringstream tokenStream(s);
while (getline(tokenStream, token, delimiter)) {
if (!token.empty()) {
tokens.push_back(token);
}
}
return tokens;
}
// 连接字符串函数
string join(const vector<string> &v, const string &delimiter) {
if (v.empty()) return "";
string result = v[0];
for (size_t i = 1; i < v.size(); ++i) {
result += delimiter + v[i];
}
return result;
}
int main() {
// 定义有效牌的顺序
const vector<string> order = {"3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K", "A"};
unordered_set<string> validCards(order.begin(), order.end());
// 读取输入
string handInput, playedInput;
getline(cin, handInput);
getline(cin, playedInput);
// 分割输入为牌列表
vector<string> hand = split(handInput, '-');
vector<string> played = split(playedInput, '-');
vector<string> allCards;
allCards.insert(allCards.end(), hand.begin(), hand.end());
allCards.insert(allCards.end(), played.begin(), played.end());
// 统计每张牌的出现次数(仅有效牌)
unordered_map<string, int> countMap;
for (const auto &card : allCards) {
if (validCards.find(card) != validCards.end()) {
countMap[card]++;
}
}
// 计算剩余次数
vector<int> remain(order.size(), 4);
for (int i = 0; i < order.size(); ++i) {
const string &card = order[i];
remain[i] = 4 - countMap[card];
}
// 滑动窗口遍历寻找最长顺子
int maxLen = 0;
int bestStart = 0;
int bestEnd = -1;
for (int i = 0; i < order.size(); ++i) {
if (remain[i] <= 0) continue; // 起始牌不可用则跳过
int currentEnd = i;
for (int j = i; j < order.size(); ++j) {
if (remain[j] <= 0) break; // 遇到不可用的牌,停止扩展
currentEnd = j; // 更新窗口结束位置
}
int currentLen = currentEnd - i + 1;
// 更新最优解(优先长度,其次末尾牌大小)
if (currentLen >= 5) {
if (currentLen > maxLen || (currentLen == maxLen && currentEnd > bestEnd)) {
maxLen = currentLen;
bestStart = i;
bestEnd = currentEnd;
}
}
}
// 输出结果
if (maxLen >= 5) {
vector<string> bestChain;
for (int i = bestStart; i <= bestEnd; ++i) {
bestChain.push_back(order[i]);
}
cout << join(bestChain, "-") << endl;
} else {
cout << "NO-CHAIN" << endl;
}
return 0;
}
代码详细解析
-
输入处理:
getline
读取输入的两行数据。split
函数按-
分割字符串,得到手牌和已出牌列表。
-
统计次数:
- 使用
unordered_map
统计有效牌的出现次数,过滤2和大小王。
- 使用
-
剩余计算:
- 数组
remain
存储每个有效牌的剩余次数,初始为4,减去已用次数。
- 数组
-
滑动窗口遍历:
- 外层循环遍历每个起始点
i
,若该牌剩余可用则继续。 - 内层循环向右扩展窗口,直到遇到剩余为0的牌,记录窗口结束位置。
- 比较当前窗口长度和记录的最大值,更新最优解。
- 外层循环遍历每个起始点
-
结果生成:
- 根据记录的起始和结束位置生成顺子字符串,用
join
函数连接。
- 根据记录的起始和结束位置生成顺子字符串,用
示例测试
示例1输入:
3-3-4-4-5-A-5-6-2-8-3-9-10-Q-7-K-J-10-B
A-4-5-8-8-10-C-6-7-8
输出:
9-10-J-Q-K-A
解析:剩余牌中 9-10-J-Q-K-A
是连续且最长的。
示例2输入(无顺子):
3-3-3-3
4-4-4-4
输出:
NO-CHAIN
示例3输入(完整顺子):
3-4-5-6-7-8-9-10-J-Q-K-A
(空已出牌)
输出:
3-4-5-6-7-8-9-10-J-Q-K-A
综合分析
-
时间复杂度:
- 滑动窗口遍历:外层循环12次,内层最多12次,复杂度为 (O(n^2)),其中 (n=12)。
- 总时间 (O(144)),完全满足题目时间限制。
-
空间复杂度:
- 存储牌的顺序和剩余次数,空间复杂度为 (O(n)),其中 (n=12)。
-
优势:
- 严格最优解:滑动窗口遍历所有可能性,保证找到最长且最大的顺子。
- 高效剪枝:遇到不可用牌立即停止扩展,减少无效计算。
-
适用场景:
- 题目约束的输入规模(牌数 < 31),高效处理中小规模数据。
C语言
问题分析
我们需要找到对手可能组成的最长顺子,顺子由3到A的连续牌组成,不含2和大小王。输出最长顺子,若存在多个相同长度的,选择牌面最大的那个。若无法组成,返回"NO-CHAIN"。
解题思路
- 输入处理:读取手牌和已出牌,分割为数组。
- 统计剩余牌数:计算每个有效牌的剩余数量(总4张减去已出现次数)。
- 滑动窗口遍历:从每个可能的起点向右扩展,寻找最长连续可用序列。
- 结果选择:记录最长且末尾最大的顺子。
代码实现
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#define MAX_CARDS 1000 // 最大输入牌数
#define ORDER_SIZE 12 // 有效牌种类数
// 有效牌顺序:3,4,5,6,7,8,9,10,J,Q,K,A
const char *order[] = {"3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K", "A"};
// 分割字符串为数组,返回分割后的元素数量
int split_string(char *str, char result[][4], const char *delim) {
int count = 0;
char *token = strtok(str, delim);
while (token != NULL) {
strcpy(result[count], token);
count++;
token = strtok(NULL, delim);
}
return count;
}
// 根据牌名查找其索引,返回-1表示无效牌
int find_index(const char *card) {
for (int i = 0; i < ORDER_SIZE; i++) {
if (strcmp(card, order[i]) == 0) return i;
}
return -1;
}
int main() {
char hand[1000], played[1000];
char cards_hand[MAX_CARDS][4], cards_played[MAX_CARDS][4];
int hand_count = 0, played_count = 0;
// 读取输入并分割
fgets(hand, sizeof(hand), stdin);
hand[strcspn(hand, "\n")] = '\0'; // 去除换行符
fgets(played, sizeof(played), stdin);
played[strcspn(played, "\n")] = '\0';
hand_count = split_string(hand, cards_hand, "-");
played_count = split_string(played, cards_played, "-");
// 统计所有牌的出现次数
int count[ORDER_SIZE] = {0};
for (int i = 0; i < hand_count; i++) {
int idx = find_index(cards_hand[i]);
if (idx != -1) count[idx]++;
}
for (int i = 0; i < played_count; i++) {
int idx = find_index(cards_played[i]);
if (idx != -1) count[idx]++;
}
// 计算剩余次数
int remain[ORDER_SIZE];
for (int i = 0; i < ORDER_SIZE; i++) {
remain[i] = 4 - count[i];
}
// 滑动窗口遍历寻找最长顺子
int max_len = 0, best_start = 0, best_end = -1;
for (int i = 0; i < ORDER_SIZE; i++) {
if (remain[i] <= 0) continue; // 起始牌不可用
int current_end = i;
for (int j = i; j < ORDER_SIZE; j++) {
if (remain[j] <= 0) break;
current_end = j;
}
int current_len = current_end - i + 1;
if (current_len >= 5) {
if (current_len > max_len || (current_len == max_len && current_end > best_end)) {
max_len = current_len;
best_start = i;
best_end = current_end;
}
}
}
// 输出结果
if (max_len >= 5) {
for (int i = best_start; i <= best_end; i++) {
printf("%s", order[i]);
if (i != best_end) printf("-");
}
printf("\n");
} else {
printf("NO-CHAIN\n");
}
return 0;
}
代码详细解析
-
定义常量:
MAX_CARDS
:输入牌的最大数量。ORDER_SIZE
:有效牌的种类数(3到A共12种)。
-
有效牌顺序:
const char *order[] = {"3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K", "A"};
- 明确有效牌的顺序,方便后续处理。
-
分割字符串:
int split_string(char *str, char result[][4], const char *delim) { // 使用strtok分割字符串为数组 }
- 将输入行按“-”分割为单独的牌。
-
查找牌索引:
int find_index(const char *card) { for (int i = 0; i < ORDER_SIZE; i++) { if (strcmp(card, order[i]) == 0) return i; } return -1; }
- 根据牌名查找其在顺序数组中的索引,用于统计次数。
-
统计剩余次数:
int remain[ORDER_SIZE]; for (int i = 0; i < ORDER_SIZE; i++) { remain[i] = 4 - count[i]; }
- 计算每张牌的剩余可用次数。
-
滑动窗口遍历:
- 外层循环遍历所有可能的起始点。
- 内层循环向右扩展窗口,直到遇到不可用牌。
- 记录最长且最大的窗口。
-
输出结果:
- 生成顺子字符串或输出"NO-CHAIN"。
示例测试
示例1输入:
3-3-4-4-5-A-5-6-2-8-3-9-10-Q-7-K-J-10-B
A-4-5-8-8-10-C-6-7-8
输出:
9-10-J-Q-K-A
解析:
- 剩余牌中9到A连续且可用,形成最长顺子。
示例2输入(无顺子):
3-3-3-3
4-4-4-4
输出:
NO-CHAIN
解析:
- 所有牌剩余次数为0,无法组成顺子。
示例3输入(最长顺子):
3-4-5-6-7-8-9-10-J-Q-K-A
(空已出牌)
输出:
3-4-5-6-7-8-9-10-J-Q-K-A
综合分析
-
时间复杂度:
- 输入处理:O(n),n为输入牌数量。
- 滑动窗口遍历:O(12²) = O(144),高效处理小规模数据。
-
空间复杂度:
- 输入存储:O(n),存储分割后的牌。
- 统计数组:O(12),固定空间消耗。
-
优势:
- 严格顺序遍历:保证找到最长且最大的顺子。
- 高效剪枝:遇到不可用牌立即停止扩展。
-
适用场景:
- 输入规模小(题目约束),适合滑动窗口法。
GO
问题分析
我们需要找到对手可能组成的最长顺子。顺子由3到A的连续牌组成,不含2和大小王。输出最长顺子,若存在多个相同长度的,选择牌面最大的那个。若无法组成,返回"NO-CHAIN"。
解题思路
- 输入处理:读取手牌和已出牌,分割为数组并过滤无效牌。
- 统计剩余牌数:每张牌最多4张,剩余次数为
4 - 已用次数
。 - 滑动窗口遍历:从每个可能的起点向右扩展,寻找最长连续可用序列。
- 结果选择:记录最长且末尾最大的顺子。
代码实现
package main
import (
"bufio"
"fmt"
"os"
"strings"
)
func main() {
scanner := bufio.NewScanner(os.Stdin)
// 读取手牌和已出牌
var hand, played string
if scanner.Scan() {
hand = scanner.Text()
}
if scanner.Scan() {
played = scanner.Text()
}
// 合并并分割所有牌
allCards := append(
strings.Split(hand, "-"),
strings.Split(played, "-")...,
)
// 有效牌顺序
order := []string{"3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K", "A"}
validCards := make(map[string]bool)
for _, card := range order {
validCards[card] = true
}
// 统计每张牌的出现次数
count := make(map[string]int)
for _, card := range allCards {
if validCards[card] {
count[card]++
}
}
// 计算剩余次数
remain := make(map[string]int)
for _, card := range order {
remain[card] = 4 - count[card]
}
maxLength := 0 // 最长顺子的长度
bestStart := 0 // 起始索引
bestEnd := -1 // 结束索引
// 滑动窗口遍历所有可能的起始点
for i := 0; i < len(order); i++ {
if remain[order[i]] <= 0 {
continue // 起始牌不可用则跳过
}
currentEnd := i // 当前窗口的结束位置
// 向右扩展窗口直到遇到不可用的牌
for j := i; j < len(order); j++ {
if remain[order[j]] <= 0 {
break
}
currentEnd = j
}
currentLength := currentEnd - i + 1
// 更新最优解(长度优先,其次比较结束位置)
if currentLength >= 5 {
if currentLength > maxLength ||
(currentLength == maxLength && currentEnd > bestEnd) {
maxLength = currentLength
bestStart = i
bestEnd = currentEnd
}
}
}
// 输出结果
if maxLength >= 5 {
chain := strings.Join(order[bestStart:bestEnd+1], "-")
fmt.Println(chain)
} else {
fmt.Println("NO-CHAIN")
}
}
代码详细解析
-
输入处理:
- 使用
bufio.Scanner
读取输入的两行数据。 - 合并手牌和已出牌,分割为数组
allCards
。
- 使用
-
有效牌定义:
order := []string{"3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K", "A"}
- 明确有效牌的顺序,用
map
快速判断合法性。
- 明确有效牌的顺序,用
-
统计剩余次数:
remain := make(map[string]int) for _, card := range order { remain[card] = 4 - count[card] }
- 计算每张牌的剩余次数(4 - 已用次数)。
-
滑动窗口遍历:
- 外层循环遍历每个可能的起始点
i
。 - 内层循环从
i
向右扩展,直到遇到剩余为0的牌。 - 记录窗口的结束位置
currentEnd
。
- 外层循环遍历每个可能的起始点
-
结果选择:
- 如果当前窗口长度大于历史最大值,或长度相同但结束位置更大,则更新最优解。
-
输出结果:
- 生成顺子字符串或输出
NO-CHAIN
。
- 生成顺子字符串或输出
示例测试
示例1输入:
3-3-4-4-5-A-5-6-2-8-3-9-10-Q-7-K-J-10-B
A-4-5-8-8-10-C-6-7-8
输出:
9-10-J-Q-K-A
解析:剩余牌中 9,10,J,Q,K,A
连续可用,形成最长顺子。
示例2输入(无顺子):
3-3-3-3
4-4-4-4
输出:
NO-CHAIN
解析:所有牌剩余次数为0,无法组成顺子。
示例3输入(最长顺子):
3-4-5-6-7-8-9-10-J-Q-K-A
(空已出牌)
输出:
3-4-5-6-7-8-9-10-J-Q-K-A
综合分析
-
时间复杂度:
- 输入处理:O(n),n为输入牌数量。
- 滑动窗口遍历:O(12²) = O(144),高效处理小规模数据。
-
空间复杂度:
- 统计存储:O(12),存储剩余次数和牌顺序。
-
优势:
- 严格顺序遍历:保证找到最长且最大的顺子。
- 高效剪枝:遇到不可用牌立即停止扩展。
-
适用场景:
- 输入规模小(题目约束),适合滑动窗口法。
更多内容:
https://www.kdocs.cn/l/cvk0eoGYucWA
本文发表于【纪元A梦】,关注我,获取更多实用教程/资源!