2026-01-10:变为活跃状态的最小时间。用go语言,给定一个长度为 n 的字符串 s 和一个包含 0 到 n-1 的排列 order。按 order 中指定的次序,从时间 t=0 起每一时刻将 s 中对应位置的字符改为 ''(例如在时刻 t 时会把下标为 order[t] 的字符替换为 '',所以经过时刻 t 已替换了索引 order[0] 到 order[t] 共 t+1 个位置)。
把任意连续的一段字符视为一个子串;如果某个子串中至少含有一个 '*',就把它称为“有效子串”。当字符串中有效子串的数量不小于 k 时,称该字符串处于“活跃”状态。
要求找出使 s 首次达到活跃状态的最早时刻 t(按上述时刻计数)。如果经过所有替换也无法让有效子串数达到 k,则返回 -1。
1 <= n == s.length <= 100000。
order.length == n。
0 <= order[i] <= n - 1。
s 由小写英文字母组成。
order 是从 0 到 n - 1 的整数排列。
1 <= k <= 1000000000。
输入: s = "cat", order = [0,2,1], k = 6。
输出: 2。
| t | order[t] | 修改后的 s | 有效子字符串 | 计数 | 激活状态 (计数 >= k) |
|---|---|---|---|---|---|
| 0 | 0 | "*at" | "*", "*a", "*at" | 3 | 否 |
| 1 | 2 | "a" | ""(第0位), "a", "a", "a", ""(第2位) | 5 | 否 |
| 2 | 1 | "***" | 所有子字符串(均包含 '*') | 6 | 是 |
字符串 s 在 t = 2 时变为激活状态。因此,答案是 2。
题目来自力扣3639。
🔄 过程详细步骤
-
初始检查与总子串数计算 首先,代码计算原始字符串
s所有可能的连续子串总数。对于一个长度为n的字符串,子串总数为n * (n + 1) / 2。如果这个总数小于目标k,那么即使将所有字符都改为'*'(这意味着所有子串都变成有效子串),有效子串的数量也无法达到k,因此直接返回-1。 -
初始化双向链表结构 为了高效模拟字符的删除(即被替换为
'*')以及计算由此带来的有效子串数量变化,代码使用数组模拟了一个双向链表。这个链表代表了字符串中字符的原始顺序。prev数组:prev[i]存储下标为i的字符的前一个字符的下标。对于首字符(i=0),其prev[0]设为-1。next数组:next[i]存储下标为i的字符的后一个字符的下标。对于尾字符(i=n-1),其next[n-1]设为n。- 初始状态下,链表保持字符的原始连续顺序。
-
逆向处理与动态维护 算法的核心在于从后向前逆向处理替换顺序
order。初始时,假设所有字符都已被替换为'*',此时有效子串数等于总子串数cnt。- 我们从最后一个时刻(
t = n-1)开始,向更早的时刻倒推。 - 在每一步逆推中,我们撤销在对应时刻
t发生的替换操作。也就是说,我们将当时被替换为'*'的字符(其下标为i = order[t])恢复为原始字符。在链表的语境下,这相当于将之前“删除”的节点重新插入,但在此逆向过程中,我们实际是进行“删除”操作。
- 我们从最后一个时刻(
-
有效子串数量的更新 当一个字符(下标
i)被恢复(即逆向操作中,它从'*'变回原字符)时,关键是要计算因为这个字符不再是'*'而导致减少的有效子串数量。- 在链表中,当前字符
i被恢复后,它可能会将其左右两段由'*'组成的连续区域分割开。此时,我们需要计算以字符i为中心,且包含i的连续子串有多少个变成了无效子串(因为这些子串现在不再包含任何'*')。 - 具体计算方法是:找到字符
i在链表中的前一个节点l(即左边最近的'*'或边界)和后一个节点r(即右边最近的'*'或边界)。那么,所有左端点位于(l, i]且右端点位于[i, r)的连续子串,在字符i恢复后都将变为无效。这样的子串数量为(i - l) * (r - i)。 - 从当前总有效子串数
cnt中减去这个数量。
- 在链表中,当前字符
-
检查活跃状态与确定结果
- 在每次恢复一个字符并更新
cnt后,检查cnt是否小于k。 - 一旦
cnt变得小于k,说明在正向时间轴上,刚刚恢复的这个字符被替换为'*'的时刻(即当前的t),是使有效子串数首次达到或超过k的时刻。因为在这个时刻之后(正向时间上),有效子串数才足以使字符串进入活跃状态。因此,函数返回当前的t。 - 如果逆向处理完所有时刻(回溯到
t=0),cnt仍然大于等于k,则说明在初始时刻(t=0)字符串就已经是活跃状态,根据逻辑应返回0(代码中需注意边界处理)。
- 在每次恢复一个字符并更新
⚙️ 复杂度分析
- 时间复杂度:
O(n)。算法主要操作包括初始化双向链表(O(n))和一次对order数组的逆向遍历(O(n))。在逆向遍历的每个步骤中,通过预处理的链表结构,计算子串数量变化和更新链表都是O(1)操作。 - 空间复杂度:
O(n)。主要的额外空间消耗在于用于模拟双向链表的两个数组prev和next,它们的长度均为n或n+1。
💎 核心思路总结
这个解法的巧妙之处在于其逆向思维和利用双向链表进行高效区间维护。通过从全 '*' 状态倒推,并计算每次“恢复”一个字符所影响的子串范围,避免了在正向模拟时难以高效计算有效子串数量变化的难题。链表结构使得在“恢复”字符时,能够快速确定其左右边界,从而在常数时间内完成对有效子串数量的更新。
Go完整代码如下:
package main
import (
"fmt"
)
func minTime(s string, order []int, k int) int {
n := len(s)
cnt := n * (n + 1) / 2
if cnt < k { // 全改成星号也无法满足要求
return -1
}
// 数组模拟双向链表
prev := make([]int, n+1)
next := make([]int, n)
for i := range n {
prev[i] = i - 1
next[i] = i + 1
}
for t := n - 1; ; t-- {
i := order[t]
l, r := prev[i], next[i]
cnt -= (i - l) * (r - i)
if cnt < k {
return t
}
// 删除链表中的 i
if l >= 0 {
next[l] = r
}
prev[r] = l
}
}
func main() {
s := "cat"
order := []int{0, 2, 1}
k := 6
result := minTime(s, order, k)
fmt.Println(result)
}
Python完整代码如下:
# -*-coding:utf-8-*-
def min_time(s: str, order: list[int], k: int) -> int:
n = len(s)
cnt = n * (n + 1) // 2 # 初始所有子串数量
if cnt < k: # 全改成星号也无法满足要求
return -1
# 模拟双向链表
prev = list(range(-1, n - 1)) # prev[i] = i-1
nxt = list(range(1, n + 1)) # nxt[i] = i+1
# 从最后时刻开始向前遍历
for t in range(n - 1, -1, -1):
i = order[t]
l = prev[i]
r = nxt[i]
# 删除位置 i 后,减少的子串数量
cnt -= (i - l) * (r - i)
if cnt < k:
return t
# 从链表中删除 i
if l >= 0:
nxt[l] = r
if r < n:
prev[r] = l
return 0 # 如果一开始就满足条件
# 测试示例
if __name__ == "__main__":
s = "cat"
order = [0, 2, 1]
k = 6
result = min_time(s, order, k)
print(result)
C++完整代码如下:
#include <iostream>
#include <vector>
#include <string>
using namespace std;
int minTime(string s, vector<int>& order, int k) {
int n = s.length();
long long cnt = (long long)n * (n + 1) / 2; // 初始所有子串数量
if (cnt < k) { // 全改成星号也无法满足要求
return -1;
}
// 模拟双向链表
vector<int> prev(n + 1);
vector<int> next(n);
for (int i = 0; i < n; i++) {
prev[i] = i - 1;
next[i] = i + 1;
}
// 从最后时刻开始向前遍历
for (int t = n - 1; t >= 0; t--) {
int i = order[t];
int l = prev[i];
int r = next[i];
// 删除位置 i 后,减少的子串数量
cnt -= (long long)(i - l) * (r - i);
if (cnt < k) {
return t;
}
// 从链表中删除 i
if (l >= 0) {
next[l] = r;
}
prev[r] = l;
}
return 0; // 如果一开始就满足条件
}
int main() {
string s = "cat";
vector<int> order = {0, 2, 1};
int k = 6;
int result = minTime(s, order, k);
cout << result << endl;
return 0;
}