LeetCode 2405. 子字符串的最优划分
1. 题目链接与难度
2405. 子字符串的最优划分 | 难度:中等
2. 白话题意重述
场景类比
想象你是一个图书馆管理员,现在有一长串字母序列需要整理到书架上。
规则是:
- 每个书架(子字符串)只能放不同的字母,不能有重复
- 你想让书架数量尽可能少
最直接的做法:
- 从左到右依次放字母
- 如果当前字母已经在当前书架上出现过,就新开一个书架,把这个字母放进去
- 重复这个过程
3. 解法全景与决策
3.1 为什么直觉解法就是最优解?
这道题比较特殊------贪心策略就是最优解,不需要"优化"。
让我们用 s = "abacaba" 来手动模拟:
字母流: a b a c a b a
↓
书架1: a b (遇到第二个 'a',冲突!)
书架2: c a (遇到第二个 'b',冲突!)
书架3: b
书架4: a
为什么贪心是最优的?
这里有一个关键观察:一旦当前字母在当前子字符串中已经存在,继续延长当前子字符串就完全不可能了,因为题目硬性要求"字符唯一"。
所以我们的选择只有:
- 开启新子字符串(必须)
- 不可能继续(只有分割一条路)
既然"必须分割"是唯一选择,那么:
- 让当前子字符串尽可能长(直到遇到冲突为止)
- 分割后立即开始新子字符串
这就是最优策略,没有更好的做法了。
3.2 核心数据结构
我们需要一个数据结构,能快速判断"某个字符是否已在当前子字符串中出现过"。
选择:哈希集合(unordered_set)
count(ch)→ O(1) 时间内判断字符是否已存在insert(ch)→ O(1) 时间添加新字符clear()→ O(1) 时间清空,开始新的子字符串
3.3 算法流程图
开始遍历
│
▼
读取字符 ch
│
├─── ch 不在 set 中 ──→ 加入 set,继续下一个字符
│
└─── ch 已在 set 中 ──→ 计数器+1,清空 set,加入 ch,继续
│
▼
遍历结束
│
▼
返回计数器
4. 代码实现
理解了上述思路后,代码就很好写了:
cpp
class Solution {
public:
int partitionString(string s) {
unordered_set<char> seen; // 记录当前子字符串中出现过的字符
int count = 1; // 至少有1个子字符串
for (char ch : s) {
if (seen.count(ch)) { // 如果字符已存在,说明必须分割
count++; // 新增一个子字符串
seen.clear(); // 清空,重新开始计数
}
seen.insert(ch); // 将当前字符加入集合
}
return count;
}
};
复杂度分析
| 维度 | 复杂度 | 通俗解释 |
|---|---|---|
| 时间复杂度 | O(n) | 每个字符只遍历一次 |
| 空间复杂度 | O(1) | 最多存储26个不同字母(英文字母有限集) |
代码细节解释
- 初始化
count = 1:即使字符串为空也有1个划分?不,空串返回0。但对于非空串,至少有1个划分 - 冲突时
count++再clear():先增加计数表示新开一个子字符串,然后清空集合,再把当前字符放进去 seen.count(ch)判断顺序:先判断再插入,确保冲突后立即开始新的计数周期
5. 易错点与防坑指南
易错点一:初始化陷阱
cpp
// ❌ 错误:空字符串会返回1
int count = 1;
// ✅ 正确:根据是否为空字符串动态处理
int count = s.empty() ? 0 : 1;
分析 :如果 s = ""(空字符串),按照题意"划分成零个子字符串",正确答案是 0。
易错点二:清空顺序陷阱
cpp
// ❌ 错误:先插入再清空,当前字符会丢失
seen.insert(ch);
if (seen.count(ch)) {
count++;
seen.clear();
}
// ✅ 正确:先清空,再插入当前字符
if (seen.count(ch)) {
count++;
seen.clear(); // 清空后 ch 不在集合中
}
seen.insert(ch); // 无论是否分割,都要把 ch 放进去
分析:冲突时必须把当前字符放到新的子字符串中,不能丢失。
6. 思考题与同类扩展
思考题
如果题目改成"每个子字符串中字符出现次数不超过 K 次"(K > 1),我们的代码需要怎么改?
提示 :把 seen.count(ch) > 0 改成 seen.count(ch) >= K,并且用一个哈希表计数而不只是集合。
推荐扩展题目
| 题号 | 题目名称 | 推荐理由 |
|---|---|---|
| 128. 最长连续序列 | 哈希集合的经典应用,本题是"找最长无重复段",本题是"找最少分割数" | |
| 3. 无重复字符的最长子串 | 滑动窗口处理无重复字符,本质上是同一类问题的不同问法 |
总结
这道题的核心思想就一句话:一旦遇到重复字符,就必须分割,且让当前段尽可能长。
这就是"贪心"------每一步都做当前最优选择(当前字符能加就加,不能加就开新段),最终得到全局最优解。