日常刷题-最小替换子串长度 | 豆包MarsCode AI刷题

69 阅读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

思考

模拟解

  1. 首先,确定每个字符在平衡情况下出现的目标次数,即 n/4n/4,其中 nn 是字符串的长度。
  2. 统计字符串中每个字符的实际出现次数。
  3. 逐步模拟替换字符串中的字符,直到达到平衡。

具体步骤:

  1. 计算初始频率差:计算每个字符相对于目标频率的差值,这告诉我们需要增加或减少多少个该字符。

  2. 逐步调整字符

    • 从字符串的左侧开始,逐个检查字符。
    • 如果某个字符超过了目标频率,我们可以考虑替换它。
    • 不断更新剩余需要处理的字符数量,直到所有字符的频率达到目标。
  3. 计算替换的最小子串长度:记录替换所需的最小子串长度。

复杂度分析

虽然这种方法的时间复杂度一般仍为 O(n)O(n),因为我们可能需要遍历整个字符串来找到合适的替换位置,但由于没有使用滑动窗口技术,它更直观地展示了问题的解决过程。这种方法通过模拟手动替换字符的过程,帮助我们更好地理解问题的本质。

贴上代码:

def solution(s):
    from collections import Counter

    n = len(s)
    target_count = n // 4
    count = Counter(s)
    
    # 计算每个字符需要调整的数量
    excess = {char: count[char] - target_count for char in 'ASDF'}
    
    # 如果已经平衡
    if all(value == 0 for value in excess.values()):
        return 0
    
    # 初始化变量
    min_substring_length = n
    start = 0
    
    # 模拟替换过程
    for end in range(n):
        # 减少窗口右边界字符的多余量
        if s[end] in excess:
            excess[s[end]] -= 1
        
        # 检查当前窗口是否能够平衡整个字符串
        while all(excess[c] <= 0 for c in 'ASDF'):
            # 更新最小子串长度
            min_substring_length = min(min_substring_length, end - start + 1)
            
            # 尝试增加窗口左边界
            if s[start] in excess:
                excess[s[start]] += 1
            start += 1
    
    return min_substring_length

另一种解法

我们可以使用滑动窗口(或双指针)技术来解决这个问题。核心思想是找到一个最小的子串,使得替换这个子串后,字符串中剩余部分的字符数量达到平衡。

Steps:

  1. 计算目标频率:对于长度为 nn 的字符串,每个字符的目标频率是 n/4n/4

  2. 统计初始字符频率:计算输入字符串中每个字符的实际出现次数。

  3. 确定多余字符:计算每个字符比目标频率多出的数量,即需要替换的字符数。

  4. 使用滑动窗口找最小子串

    • 使用两个指针来维护一个窗口 [start, end]
    • 通过移动 end 指针扩展窗口,并更新窗口中字符的频率。
    • 一旦窗口中的字符数量满足替换条件,使得窗口外的字符数量可以达到平衡,就尝试通过移动 start 指针缩小窗口,寻找最小子串。
  5. 检查条件:在每次迭代中,检查当前窗口是否可以通过替换满足字符串平衡。如果可以,则更新最小长度。

贴上代码:

def solution(input):
    from collections import Counter

    n = len(input)
    target = n // 4
    count = Counter(input)

    if all(count[char] == target for char in 'ASDF'):
        return 0

    min_length = n
    left = 0

    for right in range(n):
        count[input[right]] -= 1

        while all(count[char] <= target for char in 'ASDF'):
            min_length = min(min_length, right - left + 1)
            count[input[left]] += 1
            left += 1

    return min_length

两个解法的对比:

使用模拟过程的基本解法和滑动窗口解法各有其特点和适用场景,下面我们来比较两者的优缺点:

模拟过程解法的优缺点

优点:

  1. 直观易懂:模拟过程的解法通过逐步替换字符的方式解决问题,直观地展示了如何达到字符平衡,对于理解问题本身很有帮助。
  2. 实现简单:不需要复杂的数据结构或算法,直接从问题需求出发进行实现,逻辑清晰。

缺点:

  1. 效率较低:如果不加以优化,模拟过程可能需要多次遍历字符串以找到合适的替换位置,导致在某些情况下效率较低。
  2. 无法动态调整窗口:模拟方法缺乏滑动窗口的灵活性,难以高效地调整窗口大小以找到最优解。
  3. 处理大数据量时性能不佳:在处理非常长的字符串时,模拟方法可能会显得笨重,尤其是在需要多次调整的情况下。

滑动窗口解法的优缺点

优点:

  1. 高效:滑动窗口解法通过动态调整窗口的大小进行字符替换,通常能够在线性时间复杂度 O(n)O(n) 内找到最小替换子串。
  2. 灵活性强:滑动窗口允许我们灵活地扩大和缩小窗口,从而快速找到符合条件的最小子串。
  3. 适合大数据量:在处理较大的输入时,滑动窗口的效率优势更加明显。

总结

对于这类问题,滑动窗口解法通常是更优的选择,尤其是在需要高效处理的情况下。它通过动态调整窗口边界,迅速找到最小的满足条件的子串。然而,模拟方法可以作为一种基础的解法,帮助我们从直观的角度理解问题的本质。

在实际应用中,如果输入数据量较小或者问题规模有限,模拟过程的解法也可以被接受且易于实现。但在数据量大、需要高效率的场景下,滑动窗口解法无疑是更好的选择。