LeetCode 763. 划分字母区间
本节目标
用字符最后出现位置动态扩张分段右边界,得到尽可能多的合法分段。
这是区间排序与调度中把字符出现范围化为区间的母题。
题意与约束
把字符串划为尽可能多的连续片段,要求每个字母至多出现在一个片段中,返回各片段长度。题目字符为小写英文字母;空字符串返回空数组。
直接思路与瓶颈
从当前位置尝试任意切分点,再向后检查前缀字符是否重现,会重复扫描大量后缀。按字符逐个独立分段也不合法:例如 ab...a 中,第一个 a 不能在看到 b 后立即关闭。
贪心模型与算法推导
先记录每个字符最后一次出现的位置。扫描当前位置 index 时,将当前片段右边界更新为 end=max(end,last[s[index]]);这等价于把当前片段中每个字符的出现区间并起来。只有 index==end 时,所有已见字符都不会在后面出现,才能关闭片段并记录长度。
正确性依据
当前片段从 start 开始。扫描到任意位置前,end 是其中所有字符最后出现位置的最大值;所以在 end 之前切分必把至少一个字符分到两段,不合法。到达 end 时,每个已见字符的全部出现位置都在 [start,end] 内,切分合法。由于这是第一个可能合法的切分点,立即切分留下最长后缀,因而不会减少后续可切分次数;对后缀重复同一论证即可得到最多片段。
样例执行过程
字符串 ababcbacadefegdehijhklij 的最后出现位置让边界依次扩张。
- 从
a开始,扫描a,b,a,b,c,b,a,c,a后边界被扩张到8,在索引8关闭,长度为9。 - 下一段从
d开始,字符d,e,f,e,g,d,e的最远最后位置为15,关闭得到长度7。 - 剩余
hijhklij在索引23关闭,长度为8。
答案为 [9,7,8]。
代码实现
- C++
- Python
C++17
#include <algorithm>
#include <string>
#include <vector>
using namespace std;
vector<int> partitionLabels(string s) {
vector<int> last(26, -1);
for (int index = 0; index < static_cast<int>(s.size()); ++index) {
last[s[index] - 'a'] = index;
}
vector<int> parts;
int start = 0;
int end = 0;
for (int index = 0; index < static_cast<int>(s.size()); ++index) {
end = max(end, last[s[index] - 'a']);
if (index == end) {
parts.push_back(end - start + 1);
start = index + 1;
}
}
return parts;
}
Python 3
def partitionLabels(s: str) -> list[int]:
last = {character: index for index, character in enumerate(s)}
parts: list[int] = []
start = 0
end = 0
for index, character in enumerate(s):
end = max(end, last[character])
if index == end:
parts.append(end - start + 1)
start = index + 1
return parts
复杂度分析
两次线性扫描,时间 O(n);C++ 的字母表数组为 O(1) 空间,Python 映射最多保存字符表大小。
边界与易错点
- 记录的是最后出现位置,不能只在第一次出现时决定边界。
- 关闭条件是
index == end,不是扫描到一个新字符时。 - 单字符字符串返回
[1];空字符串返回[]。 - 每次关闭后要把下一段起点设为
index + 1。
模式迁移
当对象在序列中反复出现、且同一对象不能跨段时,可先把每个对象压缩为“首次到末次”的区间,再用动态右边界合并重叠范围。这是区间覆盖思想在字符串上的轻量变体。