M2405.子字符串的最优划分
greedy, bit manipulation, https://leetcode.cn/problems/optimal-partition-of-string/
给你一个字符串 s ,请你将该字符串划分成一个或多个 子字符串 ,并满足每个子字符串中的字符都是 唯一 的。也就是说,在单个子字符串中,字母的出现次数都不超过 一次 。
满足题目要求的情况下,返回 最少 需要划分多少个子字符串。
注意,划分后,原字符串中的每个字符都应该恰好属于一个子字符串。
示例 1:
输入:s = "abacaba"
输出:4
解释:
两种可行的划分方法分别是 ("a","ba","cab","a") 和 ("ab","a","ca","ba") 。
可以证明最少需要划分 4 个子字符串。示例 2:
输入:s = "ssssss"
输出:6
解释:
只存在一种可行的划分方法 ("s","s","s","s","s","s") 。提示:
1 <= s.length <= 10^5s仅由小写英文字母组成
这是一个典型的贪心算法问题。
解题思路
为了使划分出的子字符串数量最少,我们应该让每一个子字符串尽可能地长。
- 贪心策略:从左到右遍历字符串,将字符逐个加入当前的子字符串中。
- 冲突检测:如果当前字符已经存在于当前的子字符串中,说明当前子字符串必须在此处截止。
- 更新状态:一旦发生冲突,我们将划分计数加 1,然后开始一个新的子字符串,这个新字符串的第一个字符就是当前产生冲突的字符。
- 优化:由于字符集仅由小写英文字母组成(共 26 个),我们可以使用一个哈希集合(Set)来记录当前子字符串中出现的字符。或者为了更高的效率,使用位掩码(Bitmask)来替代集合。
代码实现(使用位掩码优化)
python
class Solution:
def partitionString(self, s: str) -> int:
# ans 初始化为 1,因为至少会有一个子字符串
ans = 1
# seen 用于记录当前子字符串中出现的字符,使用位运算模拟集合
seen = 0
for char in s:
# 计算当前字符对应的位偏移量 (0-25)
val = ord(char) - ord('a')
# 如果该位已经是 1,说明字符重复了
if seen & (1 << val):
# 开启一个新的子字符串
ans += 1
# 重置 seen,仅包含当前这一个新字符
seen = (1 << val)
else:
# 否则,将当前字符加入 seen 中
seen |= (1 << val)
return ans复杂度分析
- 时间复杂度:
,其中 是字符串 s的长度。我们只需要遍历一遍字符串。 - 空间复杂度:
。虽然我们使用了 seen变量,但它本质上是一个整数(位掩码),且字符集大小固定为 26。如果使用哈希集合,空间复杂度也是,因为集合大小最大不会超过 26。
示例执行过程 (s = "abacaba")
- 遇到
'a':seen=00...01(a),ans= 1 - 遇到
'b':seen=00...11(ab),ans= 1 - 遇到
'a': 冲突!ans= 2,seen=00...01(a) - 遇到
'c':seen=00...101(ac),ans= 2 - 遇到
'a': 冲突!ans= 3,seen=00...01(a) - 遇到
'b':seen=00...11(ab),ans= 3 - 遇到
'a': 冲突!ans= 4,seen=00...01(a) - 结束,返回 4。