刷题实践:最小替换子串长度 | 豆包MarsCode

67 阅读8分钟

题目内容

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

题意分析

这个问题实际上是一个滑动窗口问题。

我们需要找到一个子串,使得在这个子串中,'A'、'S'、'D'、'F'的出现次数尽可能接近,且替换次数最少

如果整个字符串已经满足条件,则不需要替换,返回0。

💡思路分析:来源于豆包MarsCode

🤔刚开始拿到这个题目的时候,我就首先想到了滑动窗口这一方法。于是,我便将我的想法告诉MarsCode AI,得到了肯定的答复。

image.png

MarsCode真的很智能,他同时给出了通过滑动窗口解题的相关算法流程、思路,方便我们更好的进行参考。将MarsCode的回复总结如下——

  1. 计算目标频率:由于字符串长度为4的倍数,我们首先计算出每个字符的目标出现频率,即字符串长度除以4。
  2. 统计字符频率:遍历字符串,统计每个字符的实际出现频率。
  3. 检查是否已经满足条件:如果所有字符的出现频率已经等于目标频率,则不需要替换,返回0。
  4. 滑动窗口:使用滑动窗口的方法,从左到右遍历字符串,找到满足条件的最小子串长度。
    • 维护一个滑动窗口,使得窗口内的字符频率尽可能接近目标频率。
    • 当窗口内字符频率超过目标频率时,移动窗口左边界,减少窗口内相应字符的计数。
    • 当窗口内所有字符的频率都不超过目标频率时,尝试缩小窗口,更新最小子串长度。
  5. 返回结果:遍历结束后,返回计算出的最小子串长度。

核心算法

核心算法是滑动窗口的应用,关键在于如何维护窗口内字符的频率,并在满足条件时更新最小子串长度。

💭如果对算法的核心概念还有一点陌生,也可以直接询问豆包MarsCode AI,他会结合题目给出详尽的解释和分析(重点是会结合题目帮你理解,特别nice!)

贴一张图带大家感受一下——

image.png

滑动窗口的基本知识

滑动窗口是一种处理字符串或数组子串/子序列问题的经典算法技巧,它通过维护一个动态的窗口来遍历数据,根据需要调整窗口的大小,以满足特定的条件或优化特定的指标,常用于查找连续子串、子数组或实现流式数据处理。滑动窗口的基本步骤如下——

  1. 初始化窗口:通常从数组或字符串的起始位置开始,初始化窗口的左右边界。
  2. 扩展窗口:通过移动右边界(通常是增加窗口的大小),尝试找到满足条件的窗口。
  3. 收缩窗口:如果当前窗口满足条件,尝试通过移动左边界(通常是减小窗口的大小)来找到更小的窗口,同时保持条件满足。
  4. 更新结果:在每次窗口满足条件时,更新结果(如最小长度、最大长度等)。
  5. 重复步骤2-4:直到右边界到达数组或字符串的末尾。

通过这个流程,我们就可以找到最适合的窗口。

不但如此,豆包还为我们总结了滑动窗口的一些典型应用,比如最小覆盖子串最长无重复子串等等,方便我们进行思维拓展!👍

滑动窗口的伪代码

🚩为了更加方便大家理解,这里贴出了滑动窗口的基本伪代码。

初始化窗口左边界 left = 0
初始化窗口右边界 right = 0
初始化目标值 target
初始化窗口内元素的总和 sum = 0

while right < 数组的长度:
    将 right 指针所指向的元素加到 sum
    while sum 大于或等于 target:
        如果需要找到满足条件的窗口,记录窗口的大小或位置
        将 left 指针所指向的元素从 sum 中减去
        将 left 指针向右移动一位
    将 right 指针向右移动一位

核心代码实现

def solution(input_str):
    # 计算每个字符的目标出现频率,因为字符串长度是4的倍数,所以是长度除以4
    target_freq = len(input_str) // 4
    # 初始化字符频率字典,'A'、'S'、'D'、'F'初始频率都为0
    char_freq = {"A": 0, "S": 0, "D": 0, "F": 0}

    # 遍历输入字符串,统计每个字符的出现频率
    for char in input_str:
        char_freq[char] += 1

    # 如果所有字符的出现频率已经等于目标频率,则不需要替换,直接返回0
    if all(char_freq[char] == target_freq for char in "ASDF"):
        return 0

    # 使用滑动窗口方法找到最小替换子串长度
    min_length = float('inf')  # 初始化最小长度为无穷大
    start = 0  # 滑动窗口的左边界
    for end in range(len(input_str)):  # 滑动窗口的右边界
        char_freq[input_str[end]] -= 1  # 将当前字符的频率减1

        # 如果窗口内所有字符的频率都不大于目标频率,则尝试缩小窗口
        while all(char_freq[char] <= target_freq for char in "ASDF"):
            min_length = min(min_length, end - start + 1)  # 更新最小长度
            char_freq[input_str[start]] += 1  # 将窗口左边界的字符频率加1
            start += 1  # 移动窗口左边界
            # 如果窗口左边界超过了右边界,跳出循环
            if start > end:
                break

    # 如果没有找到合适的子串长度,则返回0;否则返回找到的最小长度
    return min_length if min_length != float('inf') else 0

这段代码首先计算了每个字符的目标频率,并统计了字符串中每个字符的实际出现次数。然后,通过滑动窗口的方法,找到了使得'A'、'S'、'D'、'F'出现次数尽可能接近的最小子串长度。如果整个字符串已经满足条件,则直接返回0。👋具体步骤如下——

  1. 初始化和目标频率计算

    • 首先,代码计算了每个字符的目标出现频率target_freq,这是通过将字符串的总长度除以4得到的,因为题目要求最终每个字符的出现次数相等,且字符串长度为4的倍数。
    • 接着,初始化一个字典char_freq来记录字符'A'、'S'、'D'、'F'在字符串中的实际出现次数。
  2. 统计字符出现次数:通过一个循环遍历整个字符串input_str,对每个字符的出现次数进行统计,并更新到char_freq字典中。

  3. 检查是否已经满足条件:在进行任何操作之前,代码检查当前字符串是否已经满足了每个字符出现次数相等的条件。如果是,直接返回0,因为不需要任何替换。

  4. 滑动窗口查找最小子串

    • 如果字符串不满足条件,代码使用滑动窗口的方法来找到最小替换子串长度。初始化min_length为无穷大,表示最小长度的初始值。
    • 使用两个指针startend来表示滑动窗口的边界,end从0开始遍历整个字符串。
    • 对于每个end位置,将对应的字符在char_freq中的计数减1,模拟从窗口中移除该字符。
    • 在窗口内,如果所有字符的频率都不大于目标频率target_freq,则尝试通过移动start指针来缩小窗口,同时更新min_length为当前窗口的最小长度。
  5. 更新窗口和返回结果

    • 当窗口左边界start移动到右边界end的右边时,跳出内层循环,继续增加end指针,寻找下一个可能的窗口。
    • 最后,如果min_length仍然是无穷大,表示没有找到合适的子串,返回0;否则,返回计算出的最小长度min_length

时间复杂度:O(n)O(n),其中n是字符串的长度

解释:因为每个字符只被遍历了两次(一次增加频率,一次减少频率)。

总结

这次刷题实现结合豆包MarsCode完成了一道滑动窗口的问题。我们可以利用豆包MarsCode来学习核心的算法知识、流程。

豆包MarsCode的特色在于它提供了一个实践与学习相结合的环境,让我们能够通过编写和运行代码来直观地理解算法的工作原理和流程,同时,可以与AI交互,完成对知识的学习与实操。在这个过程中,不仅掌握了相关算法的技术细节,还学会了如何将理论知识应用到实际问题中,从而加深了对算法核心概念的理解。

这种互动式学习方式极大地提高了我们的学习效率,使我们能够在解决具体问题的同时,构建起扎实的算法知识体系。豆包MarsCode👍