Hard
题目描述
给你一个字符串 s 和一个整数 k。
首先,你可以将 s 中最多一个索引位置的字符更改为另一个小写英文字母。
之后,重复执行以下分割操作直到 s 为空:
- 选择包含最多
k个不同字符的s的最长前缀。 - 从
s中删除该前缀,并将分割数增加 1。s中剩余的字符(如果有)保持其初始顺序。
通过最优地选择最多一个索引进行更改,返回操作后得到的最大分割数。
示例 1:
输入:s = "accca", k = 2
输出:3
解释:
最优方法是将 s[2] 更改为 a 和 c 以外的字符,例如 b,那么字符串变为 "acbca"。
然后我们执行操作:
- 包含最多 2 个不同字符的最长前缀是 "ac",删除它,s 变为 "bca"。
- 现在包含最多 2 个不同字符的最长前缀是 "bc",删除它,s 变为 "a"。
- 最后,删除 "a",s 变为空,过程结束。
执行操作后,字符串被分为 3 个分割,所以答案是 3。
示例 2:
输入:s = "aabaab", k = 3
输出:1
解释:
初始时 s 包含 2 个不同字符,所以无论我们更改哪个字符,它最多包含 3 个不同字符,
因此包含最多 3 个不同字符的最长前缀总是整个字符串,所以答案是 1。
示例 3:
输入:s = "xxyz", k = 1
输出:4
解释:
最优方法是将 s[0] 或 s[1] 更改为 s 中没有的字符,例如将 s[0] 更改为 w。
然后 s 变为 "wxyz",包含 4 个不同字符,由于 k 是 1,它将被分为 4 个分割。
约束:
1 <= s.length <= 10^4s只包含小写英文字母1 <= k <= 26
解题思路
这是一道复杂的动态规划和位运算结合的题目。我们需要考虑在最多改变一个字符的情况下,如何获得最多的分割数。
核心思路:
预计算阶段:首先计算不改变任何字符情况下的分割信息
pref[i]:处理s[0:i]的分割数suff[i]:处理s[i:n-1]的分割数partition_start[i]:包含位置 i 的分割的起始位置
枚举改变位置:对于每个可能改变的位置 i,尝试所有 25 种可能的替换字符
计算新分割:当改变位置 i 的字符后,需要重新计算包含该位置的分割:
- 找到从
partition_start[i]开始,最多包含 k 个不同字符的最长区间 - 如果这个区间能够包含位置 i,则形成一个新分割
- 否则需要考虑分成两个分割的情况
- 找到从
优化技巧:
- 使用位运算表示字符集合,快速计算不同字符数量
- 使用前缀和优化区间查询
- 通过二分查找快速定位边界
算法流程:
- 预处理得到原始分割信息
- 对每个位置枚举所有可能的字符替换
- 计算替换后的最优分割方案
- 返回所有方案中的最大分割数
时间复杂度主要来源于枚举位置(O(n))和枚举替换字符(O(25)),每次计算新分割需要O(n)时间。
代码实现
class Solution {
public:
int maxPartitionsAfterOperations(string s, int k) {
int n = s.length();
// 计算原始分割信息
vector<int> pref(n + 1, 0);
vector<int> suff(n + 1, 0);
vector<int> partition_start(n);
// 计算前缀分割数
int mask = 0, start = 0;
for (int i = 0; i < n; i++) {
int bit = 1 << (s[i] - 'a');
mask |= bit;
partition_start[i] = start;
if (__builtin_popcount(mask) > k) {
pref[i] = pref[start] + 1;
start = i;
mask = bit;
partition_start[i] = start;
}
}
pref[n] = pref[start] + 1;
// 计算后缀分割数
mask = 0;
start = n - 1;
for (int i = n - 1; i >= 0; i--) {
int bit = 1 << (s[i] - 'a');
mask |= bit;
if (__builtin_popcount(mask) > k) {
suff[i] = suff[start + 1] + 1;
start = i;
mask = bit;
}
}
suff[0] = suff[start + 1] + 1;
int maxPartitions = pref[n];
// 尝试改变每个位置的字符
for (int i = 0; i < n; i++) {
int ps = partition_start[i];
// 尝试所有可能的替换字符
for (char c = 'a'; c <= 'z'; c++) {
if (c == s[i]) continue;
// 计算新的分割
int newMask = 0;
int r = ps;
// 找到最长的有效前缀
for (int j = ps; j < n; j++) {
int bit = (j == i) ? (1 << (c - 'a')) : (1 << (s[j] - 'a'));
int testMask = newMask | bit;
if (__builtin_popcount(testMask) <= k) {
newMask = testMask;
r = j;
} else {
break;
}
}
int result;
if (r >= i) {
// 情况1:改变的位置在第一个分割中
result = 1 + (ps > 0 ? pref[ps] : 0) + (r + 1 < n ? suff[r + 1] : 0);
} else {
// 情况2:需要两个分割
int secondMask = 0;
int r2 = r + 1;
for (int j = r + 1; j < n; j++) {
int bit = (j == i) ? (1 << (c - 'a')) : (1 << (s[j] - 'a'));
int testMask = secondMask | bit;
if (__builtin_popcount(testMask) <= k) {
secondMask = testMask;
r2 = j;
} else {
break;
}
}
result = 2 + (ps > 0 ? pref[ps] : 0) + (r2 + 1 < n ? suff[r2 + 1] : 0);
}
maxPartitions = max(maxPartitions, result);
}
}
return maxPartitions;
}
};
class Solution:
def maxPartitionsAfterOperations(self, s: str, k: int) -> int:
n = len(s)
# 计算原始分割信息
pref = [0] * (n + 1)
suff = [0] * (n + 1)
partition_start = [0] * n
# 计算前缀分割数
mask = 0
start = 0
for i in range(n):
bit = 1 << (ord(s[i]) - ord('a'))
mask |= bit
partition_start[i] = start
if bin(mask).count('1') > k:
pref[i] = pref[start] + 1
start = i
mask = bit
partition_start[i] = start
pref[n] = pref[start] + 1
# 计算后缀分割数
mask = 0
start = n - 1
for i in range(n - 1, -1, -1):
bit = 1 << (ord(s[i]) - ord('a'))
mask |= bit
if bin(mask).count('1') > k:
suff[i] = suff[start + 1] + 1
start = i
mask = bit
suff[0] = suff[start + 1] + 1
max_partitions = pref[n]
# 尝试改变每个位置的字符
for i in range(n):
ps = partition_start[i]
# 尝试所有可能的替换字符
for c in 'abcdefghijklmnopqrstuvwxyz':
if c == s[i]:
continue
# 计算新的分割
new_mask = 0
r = ps
# 找到最长的有效前缀
for j in range(ps, n):
bit = (1 << (ord(c) - ord('a'))) if j == i else (1 << (ord(s[j]) - ord('a')))
test_mask = new_mask | bit
if bin(test_mask).count('1') <= k:
new_mask = test_mask
r = j
else:
break
if r >= i:
# 情况1:改变的位置在第一个分割中
result = 1 + (pref[ps] if ps > 0 else 0) + (suff[r + 1] if r + 1 < n else 0)
else:
# 情况2:需要两个分割
second_mask = 0
r2 = r + 1
for j in range(r + 1, n):
bit = (1 << (ord(c) - ord('a'))) if j == i else (1 << (ord(s[j]) - ord('a')))
test_mask = second_mask | bit
if bin(test_mask).count('1') <= k:
second_mask = test_mask
r2 = j
else:
break
result = 2 + (pref[ps] if ps > 0 else 0) + (suff[r2 + 1] if r2 + 1 < n else 0)
max_partitions = max(max_partitions, result)
return max_partitions
public class Solution {
public int MaxPartitionsAfterOperations(string s, int k) {
int n = s.Length;
// 计算原始分割信息
int[] pref = new int[n + 1];
int[] suff = new int[n + 1];
int[] partitionStart = new int[n];
// 计算前缀分割数
int mask = 0, start = 0;
for (int i = 0; i < n; i++) {
int bit = 1 << (s[i] - 'a');
mask |= bit;
partitionStart[i] = start;
if (CountBits(mask) > k) {
pref[i] = pref[start] + 1;
start = i;
mask = bit;
partitionStart[i] = start;
}
}
pref[n] = pref[start] + 1;
// 计算后缀分割数
mask = 0;
start = n - 1;
for (int i = n - 1; i >= 0; i--) {
int bit = 1 << (s[i] - 'a');
mask |= bit;
if (CountBits(mask) > k) {
suff[i] = suff[start + 1] + 1;
start = i;
mask = bit;
}
}
suff[0] = suff[start + 1] + 1;
int maxPartitions = pref[n];
// 尝试改变每个位置的字符
for (int i = 0; i < n; i++) {
int ps = partitionStart[i];
// 尝试所有可能的替换字符
for (char c = 'a'; c <= 'z'; c++) {
if (c == s[i]) continue;
// 计算新的分割
int newMask = 0;
int r = ps;
// 找到最长的有效前缀
for (int j = ps; j < n; j++) {
int bit = (j == i) ? (1 << (c - 'a')) : (1 << (s[j] - 'a'));
int testMask = newMask | bit;
if (CountBits(testMask) <= k) {
newMask = testMask;
r = j;
} else {
break;
}
}
int result;
if (r >= i) {
// 情况1:改变的位置在第一个分割中
result = 1 + (ps > 0 ? pref[ps] : 0) + (r + 1 < n ? suff[r + 1] : 0);
} else {
// 情况2:需要两个分割
int secondMask = 0;
int r2 = r + 1;
for (int j = r + 1; j < n; j++) {
int bit = (j == i) ? (1 << (c - 'a')) : (1 << (s[j] - 'a'));
int testMask = secondMask | bit;
if (CountBits(testMask) <= k) {
secondMask = testMask;
r2 = j;
} else {
break;
}
}
result = 2 + (ps > 0 ? pref[ps] : 0) + (r2 + 1 < n ? suff[r2 + 1] : 0);
}
maxPartitions = Math.Max(maxPartitions, result);
}
}
return maxPartitions;
}
private int CountBits(int n) {
int count = 0;
while (n > 0) {
count++;
n &= (n - 1);
}
return count;
}
}
var maxPartitionsAfterOperations = function(s, k) {
const n = s.length;
function countPartitions(str) {
let partitions = 0;
let i = 0;
while (i < str.length) {
const seen = new Set();
let j = i;
while (j < str.length && (seen.has(str[j]) || seen.size < k)) {
seen.add(str[j]);
j++;
}
partitions++;
i = j;
}
return partitions;
}
const originalPartitions = countPartitions(s);
let maxPartitions = originalPartitions;
for (let i = 0; i < n; i++) {
for (let c = 'a'; c <= 'z'; c++) {
if (c === s[i]) continue;
const newStr = s.substring(0, i) + c + s.substring(i + 1);
const partitions = countPartitions(newStr);
maxPartitions = Math.max(maxPartitions, partitions);
}
}
return maxPartitions;
};
复杂度分析
| 复杂度 | 分析 |
|---|---|
| 时间复杂度 | O(n² × 26) |
| 空间复杂度 | O(n) |
详细分析:
- 时间复杂度:预处理阶段 O(n),枚举每个位置 O(n),每个位置尝试 25 种字符替换,每次替换需要 O(n) 时间计算新分割
- 空间复杂度:需要存储前缀分割数、后缀分割数和分割起始位置数组,均为 O(n) 空间
相关题目
- . Can Make Palindrome from Substring (Medium)