Hard
题目描述
给你一个由小写英文字母组成的字符串 s。
你可以执行以下操作任意次(可能是 0 次):
- 选择当前字符串
s中出现至少两次的任意字母,并删除其中任意一个出现位置。
返回通过这种方式能够形成的字典序最小的结果字符串。
示例 1:
输入:s = "aaccb"
输出:"aacb"
解释:
我们可以形成字符串 "acb"、"aacb"、"accb" 和 "aaccb"。"aacb" 是字典序最小的。
例如,我们可以通过选择 'c' 并删除其第一个出现位置来获得 "aacb"。
示例 2:
输入:s = "z"
输出:"z"
解释:
我们无法执行任何操作。我们能形成的唯一字符串是 "z"。
约束:
1 <= s.length <= 10^5s仅包含小写英文字母。
提示:
- 贪心求解。
- 每个不同的字母在最终字符串中必须至少出现一次。
- 对于每个字母,维护其位置的双端队列。
- 在每一步中,尝试从 ‘a’ 到 ‘z’ 的字母,并选择最早位置在安全窗口内的最小字母。
- 如果选择某个出现位置会使其他字母无法保留,则不要选择它。
- 标记位置为已使用并重复,始终最小化下一个选择的字符。
解题思路
这是一个贪心算法问题,核心思想是在保证每个字符至少出现一次的前提下,尽可能选择字典序较小的字符。
解题思路:
预处理阶段:统计每个字符的出现次数和位置,为每个字符创建位置队列。
贪心选择:从左到右构建结果字符串,每次都尝试选择当前能选择的最小字符:
- 按字母顺序 ‘a’ 到 ‘z’ 遍历
- 对于每个字符,检查是否可以安全选择其最早的未使用位置
- “安全"的定义是:选择这个位置后,剩余的字符串中仍能保证所有必需字符至少出现一次
安全性检查:对于位置
pos,需要确保在pos之后的字符串中,所有剩余的必需字符都至少有一个出现位置。状态更新:选择一个字符后,更新该字符的剩余次数,如果次数变为1,则该字符变为必需字符(不能再删除)。
这种方法确保我们总是优先选择字典序最小的安全字符,从而得到全局最优解。
时间复杂度主要来自于对每个位置的安全性检查,空间复杂度用于存储字符位置信息。
代码实现
class Solution {
public:
string lexSmallestAfterDeletion(string s) {
int n = s.length();
vector<vector<int>> pos(26);
vector<int> count(26, 0);
// 记录每个字符的位置和出现次数
for (int i = 0; i < n; i++) {
int c = s[i] - 'a';
pos[c].push_back(i);
count[c]++;
}
string result;
vector<bool> used(n, false);
while (result.length() < n) {
bool found = false;
// 尝试从 'a' 到 'z'
for (int c = 0; c < 26; c++) {
if (pos[c].empty()) continue;
// 找到第一个未使用的位置
int earliestPos = -1;
for (int p : pos[c]) {
if (!used[p]) {
earliestPos = p;
break;
}
}
if (earliestPos == -1) continue;
// 检查选择这个位置是否安全
bool safe = true;
for (int other = 0; other < 26; other++) {
if (other == c || pos[other].empty()) continue;
// 计算这个字符在 earliestPos 之后还有多少个未使用的位置
int remaining = 0;
for (int p : pos[other]) {
if (p > earliestPos && !used[p]) {
remaining++;
}
}
// 如果这个字符只剩一个位置且都在 earliestPos 之前,则不安全
if (count[other] == 1 && remaining == 0) {
safe = false;
break;
}
}
if (safe) {
result += (char)('a' + c);
used[earliestPos] = true;
count[c]--;
found = true;
break;
}
}
if (!found) break;
}
return result;
}
};
class Solution:
def lexSmallestAfterDeletion(self, s: str) -> str:
n = len(s)
pos = [[] for _ in range(26)]
count = [0] * 26
# 记录每个字符的位置和出现次数
for i, char in enumerate(s):
c = ord(char) - ord('a')
pos[c].append(i)
count[c] += 1
result = []
used = [False] * n
while len(result) < n:
found = False
# 尝试从 'a' 到 'z'
for c in range(26):
if not pos[c]:
continue
# 找到第一个未使用的位置
earliest_pos = -1
for p in pos[c]:
if not used[p]:
earliest_pos = p
break
if earliest_pos == -1:
continue
# 检查选择这个位置是否安全
safe = True
for other in range(26):
if other == c or not pos[other]:
continue
# 计算这个字符在 earliest_pos 之后还有多少个未使用的位置
remaining = sum(1 for p in pos[other] if p > earliest_pos and not used[p])
# 如果这个字符只剩一个位置且都在 earliest_pos 之前,则不安全
if count[other] == 1 and remaining == 0:
safe = False
break
if safe:
result.append(chr(ord('a') + c))
used[earliest_pos] = True
count[c] -= 1
found = True
break
if not found:
break
return ''.join(result)
public class Solution {
public string LexSmallestAfterDeletion(string s) {
int n = s.Length;
List<int>[] pos = new List<int>[26];
int[] count = new int[26];
// 初始化位置列表
for (int i = 0; i < 26; i++) {
pos[i] = new List<int>();
}
// 记录每个字符的位置和出现次数
for (int i = 0; i < n; i++) {
int c = s[i] - 'a';
pos[c].Add(i);
count[c]++;
}
StringBuilder result = new StringBuilder();
bool[] used = new bool[n];
while (result.Length < n) {
bool found = false;
// 尝试从 'a' 到 'z'
for (int c = 0; c < 26; c++) {
if (pos[c].Count == 0) continue;
// 找到第一个未使用的位置
int earliestPos = -1;
foreach (int p in pos[c]) {
if (!used[p]) {
earliestPos = p;
break;
}
}
if (earliestPos == -1) continue;
// 检查选择这个位置是否安全
bool safe = true;
for (int other = 0; other < 26; other++) {
if (other == c || pos[other].Count == 0) continue;
// 计算这个字符在 earliestPos 之后还有多少个未使用的位置
int remaining = 0;
foreach (int p in pos[other]) {
if (p > earliestPos && !used[p]) {
remaining++;
}
}
// 如果这个字符只剩一个位置且都在 earliestPos 之前,则不安全
if (count[other] == 1 && remaining == 0) {
safe = false;
break;
}
}
if (safe) {
result.Append((char)('a' + c));
used[earliestPos] = true;
count[c]--;
found = true;
break;
}
}
if (!found) break;
}
return result.ToString();
}
}
var lexSmallestAfterDeletion = function(s) {
const count = new Array(26).fill(0);
for (let char of s) {
count[char.charCodeAt(0) - 97]++;
}
const result = [];
const used = new Array(26).fill(0);
for (let char of s) {
const idx = char.charCodeAt(0) - 97;
count[idx]--;
if (used[idx] > 0) continue;
while (result.length > 0 &&
result[result.length - 1] > char &&
count[result[result.length - 1].charCodeAt(0) - 97] > 0) {
const removed = result.pop();
used[removed.charCodeAt(0) - 97]--;
}
result.push(char);
used[idx]++;
}
return result.join('');
};
复杂度分析
| 复杂度类型 | 分析 |
|---|---|
| 时间复杂度 | O(n² × 26) = O(n²),其中 n 是字符串长度。对于每个位置,我们需要检查所有26个字符的安全性 |
| 空间复杂度 | O(n),用于存储字符位置信息、使用标记数组和结果字符串 |
相关题目
- . Remove Duplicate Letters (Medium)