青训营X豆包MarsCode技术训练营第二课|豆包MarsCode刷题

70 阅读5分钟

今天刷的也是一道难度中等的题目:最小替换子串长度

问题描述

小F得到了一个特殊的字符串,这个字符串只包含字符ASDF,其长度总是4的倍数。他的任务是通过尽可能少的替换,使得ASDF这四个字符在字符串中出现的频次相等。求出实现这一条件的最小子串长度。


测试样例

样例1:

输入:input = "ADDF"
输出:1

样例2:

输入:input = "ASAFASAFADDD"
输出:3

样例3:

输入:input = "SSDDFFFFAAAS"
输出:1

样例4:

输入:input = "AAAASSSSDDDDFFFF"
输出:0

样例5:

输入:input = "AAAADDDDAAAASSSS"
输出:4

为了将给定的字符串通过最少的替换使得字符 ASDF 的出现频次相等,我们需要解决以下问题:

思路与分析

  1. 问题理解

    • 题目要求你将字符串中的字符 ASDF 的出现频率调整到相等。假设字符串的长度是 n,因为每次替换的目标是让这四个字符的出现频次相等,我们可以推算出:

      • 目标是每个字符出现的次数应该为 n / 4(这里 n 一定是 4 的倍数)。
  2. 步骤概括

    • 统计字符串中每个字符 ASDF 的频次。
    • 计算每个字符的超额和缺失。
    • 通过滑动窗口(双指针)找到最小的子串,使得可以通过替换操作使字符频次平衡。
  3. 关键步骤的分析

    • 统计频次:首先统计每个字符在字符串中的出现次数。这一步是简单的计数操作,时间复杂度是 O(n)
    • 计算目标频次:每个字符的目标频次是 n / 4。然后通过比较当前字符的频次与目标频次的差距,得到超额或缺失。超额是指某个字符的出现次数超过了目标频次,缺失是指某个字符的出现次数低于目标频次。
    • 滑动窗口法:接下来,利用滑动窗口法来查找最小的子串,使得我们可以通过替换字符来平衡频次。滑动窗口法的思想是通过两个指针来维护一个区间,该区间包含足够的多余字符,可以通过替换使频次达到目标。
  4. 核心操作

    • 滑动窗口的扩展:每次向右移动右指针,加入新的字符。更新当前窗口中每个字符的计数。
    • 滑动窗口的收缩:当当前窗口满足平衡条件(即超额字符总数已经可以通过替换来平衡),尝试缩小窗口,从左边开始移动左指针,看看是否可以找到更小的满足条件的窗口。
  5. 返回最小窗口长度:最终,窗口大小就是最少替换次数的结果。


详细步骤与例子

假设字符串是 "ASDFASDFAS", 即目标是平衡字符频次到 3(因为总长度 12,所以每个字符的目标频次为 12 / 4 = 3

  1. 统计字符频次

    • A → 3
    • S → 3
    • D → 3
    • F → 3

    这里每个字符的频次已经是目标频次,所以无需进行任何替换。最小替换次数是 0

  2. 再举个例子,比如字符串是 "AADDSFFAA"

    • A → 4
    • S → 1
    • D → 1
    • F → 2
    • 每个字符的目标频次是 2(因为长度是 8)。

    这时我们需要将 A4 降到 2,并且 SDF 都需要增加到 2

  3. 通过滑动窗口调整

    • 从左指针和右指针同时开始,逐步扩展右指针,直到窗口内字符的超额足以进行替换。
    • 窗口缩小时,左指针从左边缩减,尽可能减小窗口的大小,找到最小的窗口大小,即最小替换次数。

滑动窗口实现

滑动窗口的实现关键在于:

  • 扩展窗口:右指针向右移动,加入新的字符并更新频次。
  • 收缩窗口:当当前窗口内的字符已经足够替换以平衡频次时,尝试通过移动左指针缩小窗口,看看是否能找到更小的替换窗口。

时间复杂度分析

  • 统计频次:统计字符的频次是 O(n),其中 n 是字符串的长度。
  • 滑动窗口:滑动窗口中的左指针和右指针各自最多走一遍字符串,所以滑动窗口的操作是 O(n)

因此,整体时间复杂度是 O(n),其中 n 是字符串的长度。


结论

这道题的关键在于通过滑动窗口优化计算最少替换次数,确保字符频次相等。通过滑动窗口,我们可以在 O(n) 的时间复杂度内解决这个问题,比起暴力枚举每个子串要高效得多。

实现代码如下:

from collections import Counter

def solution(input):

    n = len(input)
    target_count = n // 4  # 每个字符的目标频次
    freq = Counter(input)

    # 计算每个字符的超额
    excess = {ch: freq.get(ch, 0) - target_count for ch in "ASDF"}
    # 只有超额字符才能在替换过程中起作用
    excess = {ch: max(0, excess[ch]) for ch in excess}

    # 如果没有超额字符,说明本来就已经平衡
    if sum(excess.values()) == 0:
        return 0

    # 滑动窗口的变量
    left = 0
    min_len = n  # 最小窗口长度,初始化为字符串长度

    # 维护窗口内字符的超额情况
    current_excess = Counter()

    for right in range(n):
        # 将当前字符添加到窗口
        current_excess[input[right]] += 1

        # 如果窗口满足条件,即超额字符的总数达到平衡要求
        while all(current_excess[ch] >= excess[ch] for ch in "ASDF"):
            # 计算当前窗口的大小
            min_len = min(min_len, right - left + 1)
            # 尝试缩小窗口
            current_excess[input[left]] -= 1
            left += 1

    return min_len
    return -2

if __name__ == "__main__":
#  You can add more test cases here
print(solution("ADDF") == 1 )
print(solution("ASAFASAFADDD") == 3)

时间复杂度

  • 字符串的长度是 n,遍历字符串的每个字符,滑动窗口的操作是线性的,所以时间复杂度为 O(n)

今天成功ac了这道题目,希望明天可以继续加油!