Medium
题目描述
给你一个由字符 ‘a’、‘b’、‘c’ 组成的字符串 s 和一个非负整数 k。每分钟,你可以选择取走 s 最左侧或者最右侧的那个字符。
请返回你需要的最少分钟数,使得你可以取到至少 k 个每种字符,如果无法取到 k 个每种字符,则返回 -1。
示例 1:
输入:s = "aabaaaacaabc", k = 2
输出:8
解释:
从 s 的左侧取三个字符,你现在有两个 'a' 字符,和一个 'b' 字符。
从 s 的右侧取五个字符,你现在有四个 'a' 字符,两个 'b' 字符,和两个 'c' 字符。
总共需要 3 + 5 = 8 分钟。
可以证明需要的最少分钟数是 8。
示例 2:
输入:s = "a", k = 1
输出:-1
解释:无法取到一个 'b' 或者 'c',所以返回 -1。
提示:
- 1 <= s.length <= 10^5
- s 由字母 ‘a’、‘b’、‘c’ 组成
- 0 <= k <= s.length
解题思路
这道题需要从字符串的左右两端取字符,使得每种字符 (‘a’, ‘b’, ‘c’) 都至少取到 k 个。
核心思路: 我们可以枚举从左侧取 i 个字符的所有情况,然后计算为了满足条件还需要从右侧取多少个字符。
解决步骤:
- 首先统计字符串中每种字符的总数,如果某种字符总数不足 k,直接返回 -1
- 使用双指针技术,枚举从左侧取 0 到 n 个字符的所有情况
- 对于每种从左侧取字符的情况,统计已取到的各字符数量
- 然后从右侧开始取字符,直到满足每种字符都至少有 k 个为止
- 记录所有可能方案中的最小值
优化点: 使用双指针避免重复计算。当左指针向右移动时,右指针可能需要向左移动来补充不足的字符,这样可以在 O(n) 时间内完成所有枚举。
时间复杂度为 O(n),空间复杂度为 O(1),是一个高效的滑动窗口解法。
代码实现
class Solution {
public:
int takeCharacters(string s, int k) {
int n = s.length();
vector<int> count(3, 0);
// 统计总字符数
for (char c : s) {
count[c - 'a']++;
}
// 检查是否可能
for (int i = 0; i < 3; i++) {
if (count[i] < k) return -1;
}
int result = n;
vector<int> left(3, 0);
int right = n - 1;
// 枚举从左侧取的字符数
for (int i = 0; i <= n; i++) {
// 添加左侧字符
if (i > 0) {
left[s[i-1] - 'a']++;
}
// 从右侧取字符直到满足条件
while (right >= i) {
bool valid = true;
for (int j = 0; j < 3; j++) {
if (left[j] + (n - 1 - right >= 0 ? count[j] - (count[j] - left[j] - (n - 1 - right)) : 0) < k) {
valid = false;
break;
}
}
if (valid) break;
right--;
}
if (right < i) break;
result = min(result, i + (n - 1 - right + 1));
}
return result;
}
};
class Solution:
def takeCharacters(self, s: str, k: int) -> int:
n = len(s)
count = [0, 0, 0]
# 统计总字符数
for c in s:
count[ord(c) - ord('a')] += 1
# 检查是否可能
for i in range(3):
if count[i] < k:
return -1
result = n
left = [0, 0, 0]
right = n - 1
# 枚举从左侧取的字符数
for i in range(n + 1):
# 添加左侧字符
if i > 0:
left[ord(s[i-1]) - ord('a')] += 1
# 从右侧取字符直到满足条件
while right >= i:
# 计算右侧已取的字符数
right_count = [0, 0, 0]
for j in range(right + 1, n):
right_count[ord(s[j]) - ord('a')] += 1
# 检查是否满足条件
valid = True
for j in range(3):
if left[j] + right_count[j] < k:
valid = False
break
if valid:
break
right -= 1
if right < i:
break
result = min(result, i + (n - right))
return result
public class Solution {
public int TakeCharacters(string s, int k) {
int n = s.Length;
int[] count = new int[3];
// 统计总字符数
foreach (char c in s) {
count[c - 'a']++;
}
// 检查是否可能
for (int i = 0; i < 3; i++) {
if (count[i] < k) return -1;
}
int result = n;
int[] left = new int[3];
int right = n - 1;
// 枚举从左侧取的字符数
for (int i = 0; i <= n; i++) {
// 添加左侧字符
if (i > 0) {
left[s[i-1] - 'a']++;
}
// 从右侧取字符直到满足条件
while (right >= i) {
// 计算右侧已取的字符数
int[] rightCount = new int[3];
for (int j = right + 1; j < n; j++) {
rightCount[s[j] - 'a']++;
}
// 检查是否满足条件
bool valid = true;
for (int j = 0; j < 3; j++) {
if (left[j] + rightCount[j] < k) {
valid = false;
break;
}
}
if (valid) break;
right--;
}
if (right < i) break;
result = Math.Min(result, i + (n - right));
}
return result;
}
}
var takeCharacters = function(s, k) {
const n = s.length;
const count = [0, 0, 0];
// 统计总字符数
for (const c of s) {
count[c.charCodeAt(0) - 'a'.charCodeAt(0)]++;
}
// 检查是否可能
for (let i = 0; i < 3; i++) {
if (count[i] < k) return -1;
}
let result = n;
const left = [0, 0, 0];
let right = n - 1;
// 枚举从左侧取的字符数
for (let i = 0; i <= n; i++) {
// 添加左侧字符
if (i > 0) {
left[s[i-1].charCodeAt(0) - 'a'.charCodeAt(0)]++;
}
// 从右侧取字符直到满足条件
while (right >= i) {
// 计算右侧已取的字符数
const rightCount = [0, 0, 0];
for (let j = right + 1; j < n; j++) {
rightCount[s[j].charCodeAt(0) - 'a'.charCodeAt(0)]++;
}
// 检查是否满足条件
let valid = true;
for (let j = 0; j < 3; j++) {
if (left[j] + rightCount[j] < k) {
valid = false;
break;
}
}
if (valid) break;
right--;
}
if (right < i) break;
result = Math.min(result, i + (n - right));
}
return result;
};
复杂度分析
| 复杂度类型 | 复杂度 |
|---|---|
| 时间复杂度 | O(n²) |
| 空间复杂度 | O(1) |
注:虽然有嵌套循环,但由于双指针的特性,右指针在整个过程中最多移动n次,所以实际时间复杂度接近O(n)。但在最坏情况下仍可能达到O(n²)。
相关题目
. Merge Sorted Array (Easy)
. Reorder List (Medium)
. Defuse the Bomb (Easy)