1. 题目解析与核心思路
这道LeetCode 1405题要求构造一个最长的"快乐字符串",其定义为不包含连续三个相同字符的字符串。给定a、b、c分别表示字符'a'、'b'、'c'的可用数量,我们需要用尽或接近用尽这些字符来构造满足条件的字符串。
1.1 问题本质分析
这个问题本质上是一个贪心算法与字符串构造的结合题。关键在于每次选择字符时,需要避免连续三个相同字符出现,同时尽可能消耗剩余最多的字符。这与现实生活中的资源分配问题有相似之处——如何在限制条件下最大化利用有限资源。
1.2 解题关键点
- 字符选择策略:每次优先选择剩余数量最多的字符
- 连续限制处理:已经连续使用两个相同字符后必须切换其他字符
- 终止条件:当无法继续添加任何字符时停止
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 算法实现详解
2.1 数据结构设计
使用最大堆(优先队列)来动态跟踪当前可用的字符及其数量。每个堆元素可以设计为一个包含字符类型和剩余数量的结构体:
cpp复制struct CharCount {
char ch;
int count;
bool operator<(const CharCount& other) const {
return count < other.count;
}
};
2.2 核心算法流程
- 初始化最大堆,将非零数量的字符加入堆中
- 循环执行以下步骤直到堆为空:
a. 取出当前数量最多的字符
b. 检查是否可以添加该字符(不违反连续三个相同字符的规则)
c. 如果可以,添加并减少计数;否则尝试次多的字符
d. 如果计数仍大于0,将字符重新放回堆中
2.3 边界条件处理
- 当连续使用两个相同字符后,必须强制切换其他字符
- 当堆中只剩一种字符且已连续使用两次时终止循环
- 处理字符计数为零的情况
3. 优化与实现技巧
3.1 性能优化
使用优先队列的插入和提取操作时间复杂度为O(log n),整体算法复杂度为O(n log n),其中n为总字符数。对于本题的约束条件(a,b,c ≤ 100)完全足够。
3.2 代码实现示例(C++)
cpp复制string longestDiverseString(int a, int b, int c) {
priority_queue<CharCount> heap;
if(a > 0) heap.push({'a', a});
if(b > 0) heap.push({'b', b});
if(c > 0) heap.push({'c', c});
string result;
while(!heap.empty()) {
auto curr = heap.top(); heap.pop();
// 检查是否可以添加当前字符
if(result.size() >= 2 &&
result.back() == curr.ch &&
result[result.size()-2] == curr.ch) {
if(heap.empty()) break;
auto next = heap.top(); heap.pop();
result += next.ch;
if(--next.count > 0) heap.push(next);
heap.push(curr); // 把当前字符放回去
} else {
result += curr.ch;
if(--curr.count > 0) heap.push(curr);
}
}
return result;
}
3.3 常见实现错误
- 未正确处理连续两个相同字符后的强制切换
- 在字符计数为零时仍尝试添加到结果中
- 优先队列的比较函数实现错误导致排序不正确
4. 测试用例与验证
4.1 典型测试用例
cpp复制TEST_CASE("Longest Happy String") {
CHECK(longestDiverseString(1,1,1) == "abc" ||
longestDiverseString(1,1,1) == "acb" ||
/* 其他合法排列 */);
CHECK(longestDiverseString(7,1,0) == "aabaa");
CHECK(longestDiverseString(0,1,1) == "bc" ||
longestDiverseString(0,1,1) == "cb");
CHECK(longestDiverseString(100,100,100).length() == 300);
}
4.2 特殊边界测试
- 全零输入
- 单一字符大量存在
- 两个字符数量悬殊
- 刚好满足连续两个相同字符的边界情况
5. 算法扩展与变种
5.1 通用化问题
如果将问题扩展为k个不同字符,且限制连续m个相同字符,算法框架仍然适用,只需调整连续字符的检查条件。
5.2 实际应用场景
这类字符串构造问题在以下场景有实际应用:
- 数据压缩中的游程编码限制
- 排版系统中的单词分行算法
- 资源调度中的公平分配策略
5.3 相关LeetCode题目
-
- Reorganize String
-
- Task Scheduler
-
- Rearrange String k Distance Apart
6. 性能分析与优化
6.1 时间复杂度分析
每次堆操作O(log 3)=O(1)(因为最多3种字符),总操作次数O(a+b+c),因此整体时间复杂度为O(n),其中n为总字符数。
6.2 空间复杂度
堆空间O(1)(固定大小),结果字符串O(n),因此空间复杂度O(n)。
6.3 替代实现方案
可以使用简单的条件判断代替优先队列,因为字符种类固定且很少:
cpp复制string longestDiverseString(int a, int b, int c) {
string res;
int total = a + b + c;
int cntA = 0, cntB = 0, cntC = 0;
for(int i = 0; i < total; ++i) {
if((a >= b && a >= c && cntA < 2) ||
(cntB == 2 && a > 0) ||
(cntC == 2 && a > 0)) {
res += 'a';
a--;
cntA++;
cntB = cntC = 0;
}
else if((b >= a && b >= c && cntB < 2) ||
(cntA == 2 && b > 0) ||
(cntC == 2 && b > 0)) {
res += 'b';
b--;
cntB++;
cntA = cntC = 0;
}
else if((c >= a && c >= b && cntC < 2) ||
(cntA == 2 && c > 0) ||
(cntB == 2 && c > 0)) {
res += 'c';
c--;
cntC++;
cntA = cntB = 0;
}
}
return res;
}
7. 解题心得与技巧
- 贪心选择策略:这类问题通常需要找到每一步的最优选择,而不必考虑全局最优
- 数据结构选择:即使问题规模很小,使用合适的数据结构(如优先队列)可以使逻辑更清晰
- 边界条件优先:先处理特殊情况(如连续两个相同字符)可以避免复杂的条件嵌套
- 测试驱动开发:先设计测试用例再实现,可以更快发现逻辑漏洞
在实际面试中,建议先说明解题思路,再逐步实现,最后讨论优化方案。对于这类字符串构造问题,清晰的逻辑比一味追求代码简洁更重要。
