Leetcode(763)——划分字母区间
题目
字符串 S 由小写字母组成。我们要把这个字符串划分为尽可能多的片段,同一字母最多出现在一个片段中。返回一个表示每个字符串片段的长度的列表。
示例:
输入:S = “ababcbacadefegdehijhklij”
输出:[9,7,8]
解释:
划分结果为 “ababcbaca”, “defegde”, “hijhklij”。
每个字母最多出现在一个片段中。
像 “ababcbacadefegde”, “hijhklij” 的划分是错误的,因为划分的片段数较少。
提示:
- S的长度在 [ 1 , 500 ] [1, 500] [1,500]之间。
- S只包含小写字母 ‘a’ 到 ‘z’ 。
题解
关键:处理数组前可以统计一遍信息(频率、个数、第一次或最后一次的位置等)可以降低难度
方法一:贪心
思路
由于同一个字母只能出现在同一个片段,显然同一个字母的第一次出现的下标位置和最后一次出现的下标位置必须出现在同一个片段。因此需要遍历字符串,得到每个字母最后一次出现的下标位置。
在得到每个字母最后一次出现的下标位置之后,可以使用贪心的方法将字符串划分为尽可能多的片段,具体做法如下。
- 从左到右遍历字符串,遍历的同时维护当前片段的开始下标 start \textit{start} start 和结束下标 end \textit{end} end,初始时 start = end = 0 \textit{start}=\textit{end}=0 start=end=0。
- 对于每个访问到的字母 c c c,得到当前字母的最后一次出现的下标位置 end c \textit{end}_c endc,则当前片段的结束下标一定不会小于 end c \textit{end}_c endc,因此令 end = max ( end , end c ) \textit{end}=\max(\textit{end},\textit{end}_c) end=max(end,endc)。
- 当访问到下标 end \textit{end} end 时,当前片段访问结束,当前片段的下标范围是 [ start , end ] [\textit{start},\textit{end}] [start,end],长度为 end − start + 1 \textit{end}-\textit{start}+1 end−start+1,将当前片段的长度添加到返回值,然后令 start = end + 1 \textit{start}=\textit{end}+1 start=end+1,继续寻找下一个片段。
- 重复上述过程,直到遍历完字符串。
上述做法使用贪心的思想寻找每个片段可能的最小结束下标,因此可以保证每个片段的长度一定是符合要求的最短长度,如果取更短的片段,则一定会出现同一个字母出现在多个片段中的情况。由于每次取的片段都是符合要求的最短的片段,因此得到的片段数也是最多的。
由于每个片段访问结束的标志是访问到下标 end \textit{end} end,因此对于每个片段,可以保证当前片段中的每个字母都一定在当前片段中,不可能出现在其他片段,可以保证同一个字母只会出现在同一个片段。
或者
- 从头遍历字符串,并统计每一个字符最后出现的位置;
- 用一个变量 MaxPos 保存 目前已扫描字符的最后出现位置中的最大值 。然后再次从头遍历字符串,并更新 MaxPos 的值,如果找到当前下标和 MaxPos 相等了,则找到了分割点。
比如下图:先扫描 “ababc” 那么已扫描字符为 ‘a’ ‘b’ ‘c’,最后出现位置中的最大值,即 MaxPos 为8。当扫描到 “def” 时,已扫描字符为 ‘d’ ‘e’ ‘f’,MaxPos 为15。因为字符 ‘a’ ‘b’ ‘c’ 已经是上一个区间的字符了,被分割开了。
代码实现
Leetcode 官方题解:
class Solution {
public:
vector<int> partitionLabels(string s) {
vector<int> res;
int last[26];
int n = s.size();
for (int i = 0; i < n; i++)
last[s[i]-'a'] = i; //记录每个字母在s中的最后位置
int start = 0, end = 0;
for (int i = 0; i < n; i++){
end = max(end, last[s[i]-'a']);
if (end == i){
res.push_back(end - start + 1);
start = end + 1;
}
}
return res;
}
};
我自己的:
class Solution {
public:
vector<int> partitionLabels(string s) {
// 总问题:找出最多的区间,满足同一字母只会出现在同一个区间
// 将同一字母的起始点和终点看成一个区间的左端点和右端点
// 该问题转换为:将重叠区间看成同一个区间,获取最多的区间个数
unordered_map<char, int> letter;
vector<vector<int>> section;
int size = s.size();
for(int n = 0; n < size; n++){
if(letter.count(s[n]) == 0){
section.push_back({n, n});
letter.emplace(s[n], section.size()-1);
}else section[letter[s[n]]][1] = n;
}
// for(auto& it: letter) cout << it.first << " :" << section[it.second][0] << " " << section[it.second][1] << endl;
// 不需要按左端点进行升序排序,因为之前循环时保存就是以左端点从0到size-1保存的
vector<int> ans;
vector<vector<int>> tmp;
size = section.size();
for(int n = 0; n < size; n++){
// 开始时或新区间与上一个区间不重叠,则创建新区间,并计算前一个区间的长度
if(tmp.empty() || section[n][0] >= (*tmp.rbegin())[1]){
if(!tmp.empty()) ans.push_back((*tmp.rbegin())[1] - (*tmp.rbegin())[0] + 1);
tmp.push_back({section[n][0], section[n][1]});
}else (*tmp.rbegin())[1] = max((*tmp.rbegin())[1], section[n][1]);
if(n == size-1) ans.push_back((*tmp.rbegin())[1] - (*tmp.rbegin())[0] + 1);
}
return ans;
}
};
复杂度分析
时间复杂度:
O
(
n
)
O(n)
O(n),其中 nn 是字符串的长度。需要遍历字符串两次,第一次遍历时记录每个字母最后一次出现的下标位置,第二次遍历时进行字符串的划分。
空间复杂度:
O
(
∣
Σ
∣
)
O(∣\Sigma∣)
O(∣Σ∣),其中
Σ
\Sigma
Σ 是字符串中的字符集。这道题中,字符串只包含小写字母,因此
∣
Σ
∣
=
26
|\Sigma|=26
∣Σ∣=26。