2026-01-10:变为活跃状态的最小时间。用go语言,给定一个长度为 n 的字符串 s 和一个包含 0 到 n-1 的排列 order。按 order 中指

16 阅读7分钟

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。

torder[t]修改后的 s有效子字符串计数激活状态 (计数 >= k)
00"*at""*", "*a", "*at"3
12"a"""(第0位), "a", "a", "a", ""(第2位)5
21"***"所有子字符串(均包含 '*')6

字符串 s 在 t = 2 时变为激活状态。因此,答案是 2。

题目来自力扣3639。

🔄 过程详细步骤

  1. 初始检查与总子串数计算 首先,代码计算原始字符串 s 所有可能的连续子串总数。对于一个长度为 n 的字符串,子串总数为 n * (n + 1) / 2。如果这个总数小于目标 k,那么即使将所有字符都改为 '*'(这意味着所有子串都变成有效子串),有效子串的数量也无法达到 k,因此直接返回 -1

  2. 初始化双向链表结构 为了高效模拟字符的删除(即被替换为 '*')以及计算由此带来的有效子串数量变化,代码使用数组模拟了一个双向链表。这个链表代表了字符串中字符的原始顺序。

    • prev 数组:prev[i] 存储下标为 i 的字符的前一个字符的下标。对于首字符(i=0),其 prev[0] 设为 -1
    • next 数组:next[i] 存储下标为 i 的字符的后一个字符的下标。对于尾字符(i=n-1),其 next[n-1] 设为 n
    • 初始状态下,链表保持字符的原始连续顺序。
  3. 逆向处理与动态维护 算法的核心在于从后向前逆向处理替换顺序 order。初始时,假设所有字符都已被替换为 '*',此时有效子串数等于总子串数 cnt

    • 我们从最后一个时刻(t = n-1)开始,向更早的时刻倒推。
    • 在每一步逆推中,我们撤销在对应时刻 t 发生的替换操作。也就是说,我们将当时被替换为 '*' 的字符(其下标为 i = order[t]恢复为原始字符。在链表的语境下,这相当于将之前“删除”的节点重新插入,但在此逆向过程中,我们实际是进行“删除”操作。
  4. 有效子串数量的更新 当一个字符(下标 i)被恢复(即逆向操作中,它从 '*' 变回原字符)时,关键是要计算因为这个字符不再是 '*' 而导致减少的有效子串数量

    • 在链表中,当前字符 i 被恢复后,它可能会将其左右两段由 '*' 组成的连续区域分割开。此时,我们需要计算以字符 i 为中心,且包含 i 的连续子串有多少个变成了无效子串(因为这些子串现在不再包含任何 '*')。
    • 具体计算方法是:找到字符 i 在链表中的前一个节点 l(即左边最近的 '*' 或边界)和后一个节点 r(即右边最近的 '*' 或边界)。那么,所有左端点位于 (l, i] 且右端点位于 [i, r) 的连续子串,在字符 i 恢复后都将变为无效。这样的子串数量为 (i - l) * (r - i)
    • 从当前总有效子串数 cnt 中减去这个数量。
  5. 检查活跃状态与确定结果

    • 在每次恢复一个字符并更新 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)。主要的额外空间消耗在于用于模拟双向链表的两个数组 prevnext,它们的长度均为 nn+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;
}

在这里插入图片描述