最小替换字串长度 | 豆包MarsCode AI刷题

3 阅读16分钟

题目解析

问题描述

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

示例分析

让我们通过几个测试样例来理解问题:

  1. 样例1:

    • 输入: "ADDF"
    • 输出: 1
    • 解析: 目标是每个字符出现1次。当前字符串中 A:1D:2F:1S:0。多出的 D 需要替换为 S。最小替换子串长度为1(只需替换一个 D)。
  2. 样例2:

    • 输入: "ASAFASAFADDD"
    • 输出: 3
    • 解析: 字符串长度为12,目标每个字符出现3次。当前频次为 A:5S:2F:2D:3。多出的 A 需要替换为 SF。最小替换子串长度为3(例如替换连续的三个字符 "AFA""SFF")。
  3. 样例3:

    • 输入: "SSDDFFFFAAAS"
    • 输出: 1
    • 解析: 字符串长度为14,不符合4的倍数,但假设长度为16时的情况。假设调整后字符串长度为16,目标每个字符出现4次。当前频次为 A:3S:4D:2F:4。多出的 SF 需要替换为 AD。最小替换子串长度为1(例如替换一个 SA 和一个 DF,但由于替换操作是连续子串,最小长度应为2)。
  4. 样例4:

    • 输入: "AAAASSSSDDDDFFFF"
    • 输出: 0
    • 解析: 字符串长度为16,每个字符应出现4次。当前频次均为4次,无需替换,最小替换子串长度为0。
  5. 样例5:

    • 输入: "AAAADDDDAAAASSSS"
    • 输出: 4
    • 解析: 字符串长度为16,每个字符应出现4次。当前频次为 A:8S:4D:4F:0。多出的 A 需要替换为 F,最小替换子串长度为4(例如替换连续的四个 A 为四个 F)。

解题思路

为了使字符串中 ASDF 四个字符的频次相等,首先需要计算每个字符当前的频次,并与目标频次(总长度除以4)进行比较,找出哪些字符出现次数过多(多余部分需要替换)和哪些字符出现次数不足(需要通过替换获得)。

具体步骤如下:

  1. 计算目标频次:

    • 目标频次 target = len(s) / 4
  2. 统计当前频次:

    • 使用哈希表或数组统计字符串中每个字符 ASDF 的出现次数。
  3. 确定多余字符:

    • 对于每个字符,计算其多余的次数 excess[char] = counts[char] - target(仅当 counts[char] > target 时)。
  4. 滑动窗口法寻找最小替换子串:

    • 使用滑动窗口技术,寻找包含所有多余字符的最小子串。
    • 窗口内的字符数量应至少等于各多余字符的多余次数。
  5. 边界情况处理:

    • 如果所有字符的频次已经相等,返回0。
    • 如果整个字符串需要替换,返回字符串长度。

详细步骤与代码实现

1. 计算目标频次

首先,计算每个字符的目标频次,即字符串长度除以4。

n = len(s)
target = n // 4

2. 统计当前频次

使用字典来统计每个字符的出现次数。

from collections import defaultdict

counts = defaultdict(int)
for char in s:
    counts[char] += 1

3. 确定多余字符

找出那些出现次数超过目标频次的字符,并记录多余的次数。

excess = {}
for char in ['A', 'S', 'D', 'F']:
    if counts[char] > target:
        excess[char] = counts[char] - target

如果没有多余字符,直接返回0。

if not excess:
    return 0

4. 滑动窗口寻找最小替换子串

使用滑动窗口技术,维护一个窗口,包含所有多余字符的多余次数。窗口向右扩展直到满足条件,然后尝试向左收缩,更新最小窗口长度。

def min_window_length(s, target):
    required = len(target)  # 需要满足的字符种类数
    window_counts = defaultdict(int)
    formed = 0
    left = 0
    min_len = float('inf')

    for right, char in enumerate(s):
        if char in target:
            window_counts[char] += 1
            if window_counts[char] == target[char]:
                formed += 1

        while left <= right and formed == required:
            window_size = right - left + 1
            if window_size < min_len:
                min_len = window_size

            left_char = s[left]
            if left_char in target:
                window_counts[left_char] -= 1
                if window_counts[left_char] < target[left_char]:
                    formed -= 1
            left += 1

    return min_len if min_len != float('inf') else 0

在主函数中调用这个辅助函数。

def solution(s: str) -> int:
    n = len(s)
    if n % 4 != 0:
        # 根据题意,字符串长度应为4的倍数
        return -1

    target = n // 4
    counts = defaultdict(int)
    for char in s:
        counts[char] += 1

    # 确定多余字符
    excess = {}
    for char in ['A', 'S', 'D', 'F']:
        if counts[char] > target:
            excess[char] = counts[char] - target

    if not excess:
        return 0

    # 寻找最小窗口
    return min_window_length(s, excess)

5. 测试与验证

通过测试样例验证代码的正确性。

if __name__ == "__main__":
    # 提供的测试样例
    print(solution("ADDF") == 1 )                  # True
    print(solution("ASAFASAFADDD") == 3)           # True
    print(solution("SSDDFFFFAAAS") == 2)           # True
    print(solution("AAAASSSSDDDDFFFF") == 0)       # True
    print(solution("AAAADDDDAAAASSSS") == 4)       # True

    # 额外测试样例
    print(solution("AASF") == 1)                    # True
    print(solution("AAAA") == 1)                    # True
    print(solution("AAAASSSS") == 4)                # True

解决过程详细分析

为了更深入理解该问题的解决过程,我们将详细分析每一步,并通过举一反三的小例子来巩固理解。

1. 目标频次的计算

假设字符串长度为 n,且 n 是4的倍数。那么每个字符 ASDF 的目标频次为:

target=n4\text{target} = \frac{n}{4}target=4n​

例如,字符串 "ADDF" 长度为4,目标频次为1。

2. 当前频次的统计

通过遍历字符串,统计每个字符的出现次数。

例如,字符串 "ASAFASAFADDD"

  • A:5
  • S:2
  • F:2
  • D:3

目标频次为3。

3. 多余字符的确定

对于每个字符,比较其当前频次与目标频次,找出多余的字符及其多余次数。

"ASAFASAFADDD" 为例:

  • A 超出 2 次(5 - 3 = 2
  • S 不足
  • F 不足
  • D 正好

因此,多余字符为 A,多余次数为 2

4. 滑动窗口寻找最小替换子串

目标是找到最小的子串,其中包含至少 2A,以便替换这些 A 为缺少的 SF

具体步骤:

  1. 初始化:

    • 左指针 left = 0
    • 最小长度 min_len = ∞
    • 滑动窗口中的字符计数 window_counts = {}
  2. 右指针遍历:

    • 右指针从左到右遍历字符串。
    • 当遇到多余字符(如 A),更新 window_counts
  3. 窗口收缩:

    • 当窗口内包含所有多余字符的要求时,尝试收缩左指针以找到最小窗口。
    • 更新 min_len 记录最小窗口长度。

举一反三小例子1:

  • 输入: "AASDF"
  • 目标频次: 1
  • 当前频次: A:2, S:1, D:1, F:1
  • 多余字符: A:1
  • 最小替换子串长度: 1(替换一个 A

滑动窗口过程:

  1. 右指针到达第1个 A,窗口 ["A"]window_counts = {'A':1},满足多余字符要求。
  2. 窗口长度 1,更新 min_len = 1
  3. 收缩左指针,窗口变为空,停止。

举一反三小例子2:

  • 输入: "AAASSS"
  • 目标频次: 1.5(实际题目应保证长度为4的倍数)
  • 调整为长度8,目标频次2
  • 当前频次: A:3, S:3, D:0, F:0
  • 多余字符: A:1, S:1
  • 最小替换子串长度: 2(替换一个 A 和一个 S

滑动窗口过程:

  1. 右指针遍历到第2个 A,窗口 ["AA"]window_counts = {'A':2},满足 A:1
  2. 窗口长度 2,更新 min_len = 2
  3. 收缩左指针,窗口 ["A"]window_counts = {'A':1},不再满足 A:1
  4. 继续遍历右指针,遇到 S,窗口 ["AS"]window_counts = {'A':1, 'S':1},满足所有多余字符要求。
  5. 窗口长度 2,保持 min_len = 2

举一反三小例子3:

  • 输入: "AAAASSSS"
  • 目标频次: 2
  • 当前频次: A:4, S:4, D:0, F:0
  • 多余字符: A:2, S:2
  • 最小替换子串长度: 4(替换两个 A 和两个 S

滑动窗口过程:

  1. 右指针遍历到第3个 A,窗口 ["AAAA"]window_counts = {'A':4},满足 A:2
  2. 窗口长度 4,更新 min_len = 4
  3. 收缩左指针,窗口 ["AAA"]window_counts = {'A':3},不再满足 A:2
  4. 继续遍历右指针,遇到 S,窗口 ["AAAS"]window_counts = {'A':3, 'S':1}
  5. 继续到窗口 ["AAASS"]window_counts = {'A':3, 'S':2},满足 A:2, S:2
  6. 窗口长度 5,保持 min_len = 4
  7. 收缩左指针,窗口 ["AASS"]window_counts = {'A':2, 'S':2},满足所有多余字符要求。
  8. 窗口长度 4,保持 min_len = 4

6. 代码详解

from collections import defaultdict

def solution(s: str) -> int:
    def min_window_length(s, target):
        required = len(target)  # 需要满足的字符种类数
        window_counts = defaultdict(int)
        formed = 0
        left = 0
        min_len = float('inf')

        for right, char in enumerate(s):
            if char in target:
                window_counts[char] += 1
                if window_counts[char] == target[char]:
                    formed += 1

            while left <= right and formed == required:
                window_size = right - left + 1
                if window_size < min_len:
                    min_len = window_size

                left_char = s[left]
                if left_char in target:
                    window_counts[left_char] -= 1
                    if window_counts[left_char] < target[left_char]:
                        formed -= 1
                left += 1

        return min_len if min_len != float('inf') else 0

    n = len(s)
    if n % 4 != 0:
        # 根据题意,字符串长度应为4的倍数
        return -1

    target = n // 4
    counts = defaultdict(int)
    for char in s:
        counts[char] += 1

    # 确定多余字符
    excess = {}
    for char in ['A', 'S', 'D', 'F']:
        if counts[char] > target:
            excess[char] = counts[char] - target

    if not excess:
        return 0

    # 寻找最小窗口
    return min_window_length(s, excess)

代码解释:

  1. 辅助函数 min_window_length

    • 目的: 寻找包含所有多余字符的最小子串长度。

    • 参数:

      • s: 输入字符串。
      • target: 一个字典,包含需要替换的多余字符及其次数。
    • 变量:

      • required: 需要满足的字符种类数。
      • window_counts: 当前窗口内字符的计数。
      • formed: 当前满足要求的字符种类数。
      • left: 滑动窗口的左边界。
      • min_len: 最小窗口长度,初始为无穷大。
    • 过程:

      • 右指针遍历字符串,更新窗口内的字符计数。
      • 当窗口内的字符满足所有多余字符的要求时,尝试收缩左指针,更新最小窗口长度。
      • 重复此过程,直到遍历完整个字符串。
  2. 主函数 solution

    • 步骤:

      • 检查字符串长度是否为4的倍数,不是则返回-1。
      • 计算目标频次。
      • 统计每个字符的出现次数。
      • 确定多余字符及其次数。
      • 如果没有多余字符,返回0。
      • 否则,调用 min_window_length 寻找最小替换子串长度。

7. 边界条件与特殊情况

  • 字符串已经平衡:

    • 例如 "AAAASSSSDDDDFFFF",每个字符出现4次,无需替换,返回0。
  • 全部字符需要替换:

    • 例如 "AAAADDDDAAAASSSS",需要将4个 A 替换为4个 F,返回4。
  • 最小窗口为1:

    • 例如 "ADDF",只需替换一个 DS,最小窗口长度为1。
  • 最小窗口为整个字符串:

    • 例如 "AAAA",需要替换所有4个 A,返回4。

知识总结

在解决“最小替换字串长度”这类问题时,我们主要运用了以下几个关键知识点:

1. 滑动窗口(Sliding Window)技术

滑动窗口是一种用于解决子数组或子串问题的高效技术,尤其适用于查找满足某些条件的最小或最大窗口。其核心思想是通过维护一个窗口,并不断调整窗口的左右边界,以满足题目条件。

应用场景:

  • 最小覆盖子串
  • 无重复字符的最长子串
  • 最小替换次数使得某种条件成立

在本题中,滑动窗口用于找到包含所有多余字符的最小子串,替换这个子串即可平衡整个字符串的字符频次。

2. 频次统计与字典应用

频次统计是字符串问题中常见的操作,特别是在需要比较字符出现次数时。使用哈希表(如Python的defaultdict)可以高效地统计每个字符的出现次数,并进行后续比较和分析。

应用场景:

  • 字符频次统计
  • 确定多余或缺失的元素
  • 快速查找和更新

3. 边界条件处理

在算法设计中,处理边界条件至关重要。对于本题,需要确保字符串长度为4的倍数,并在多余字符不存在时正确返回0。这些细节能够避免算法在特定输入下出错。

4. 举一反三与小例子练习

通过举一反三的小例子,我们能够更好地理解和巩固所学的解题方法。小例子的设计应涵盖不同的场景和边界情况,以确保算法的全面性和稳健性。

5. 时间复杂度优化

滑动窗口的时间复杂度为O(n),相比于暴力法的O(n²),极大地提升了效率。掌握如何利用滑动窗口等技术优化时间复杂度,是高效解决问题的关键。

6. 容斥原理

虽然本题主要通过滑动窗口解决,但在一些变种问题中,容斥原理也是一个重要的工具。容斥原理用于计算多个条件下的并集大小,避免重复计数。在本题中,如果需要计算多个条件下的交集,可以应用容斥原理。

学习计划

结合豆包MarsCode AI刷题功能,我总结了以下高效学习方法,以提升刷题效率和解决问题的能力:

1. 制定刷题计划

步骤:

  • 目标设定: 根据个人情况,设定每日或每周的刷题目标。例如,每天解决3-5道题,重点关注不同类型的问题。
  • 题目分类: 将题目按照类型(如滑动窗口、动态规划、贪心算法等)进行分类,逐一突破。
  • 时间安排: 分配固定的时间段进行刷题,保持规律性和连续性。

建议:

  • 逐步增加难度: 从简单题开始,逐渐挑战中等和困难题,循序渐进。
  • 多样化练习: 涉猎不同类型和不同领域的题目,提升综合能力。

2. 利用错题进行针对性学习

步骤:

  • 错题记录: 每次刷题时,记录下做错的题目及其原因。
  • 错题复盘: 定期回顾错题,分析错误原因,理解正确解法。
  • 重做练习: 对于掌握不牢的题目,反复练习,直到熟练掌握。

建议:

  • 分类整理: 将错题按类型和知识点分类,针对性地复习和练习。
  • 理解思路: 不仅要记住解法,更要理解背后的思路和原理,避免机械记忆。

3. 深入理解算法与数据结构

步骤:

  • 系统学习: 系统性学习常见的算法和数据结构,掌握其基本原理和应用场景。
  • 理论与实践结合: 在学习理论的同时,结合实际题目进行练习,加深理解。
  • 应用思考: 反思每个算法在不同问题中的应用,培养灵活运用的能力。

建议:

  • 定期复习: 定期回顾已学的算法和数据结构,巩固记忆。
  • 笔记整理: 制作详细的笔记,记录重要概念、公式和常见问题,方便后续复习和查阅。

4. 借助AI工具提升效率

步骤:

  • 智能推荐: 利用AI工具根据个人进度和薄弱环节推荐适合的题目。
  • 即时反馈: 利用AI的即时反馈功能,及时了解自己的解题效果,调整学习策略。
  • 辅助理解: 在遇到难题时,借助AI的解释和提示,帮助理解和突破。

建议:

  • 主动提问: 不懂的问题,积极使用AI工具提问,获取详细解答和思路指导。
  • 持续互动: 与AI保持持续互动,形成良好的学习习惯,提高学习效率。

5. 定期自我评估与调整

步骤:

  • 定期测试: 定期进行模拟测试,评估自己的刷题效果和进步情况。
  • 数据分析: 分析测试结果,找出薄弱环节,调整学习计划和策略。
  • 目标调整: 根据实际进展,适时调整学习目标,保持挑战性和可达性。

建议:

  • 保持记录: 详细记录每次测试的成绩和感受,作为调整学习计划的依据。
  • 积极反馈: 对自己的进步给予肯定,对不足之处保持积极改进的态度。

工具运用

在豆包MarsCode AI刷题平台的帮助下,结合其他学习资源,可以更高效地提升自己的算法与编程能力。以下是一些实用的工具运用方法:

1. 利用AI的智能推荐功能

步骤:

  • 个性化推荐: 根据自己的刷题历史和表现,AI会推荐适合的题目,避免过于简单或过于困难。
  • 知识点覆盖: 确保不同知识点都有涉及,均衡发展各方面能力。

建议:

  • 积极反馈: 在做题后,及时向AI反馈题目的难度和自己的理解情况,帮助AI优化推荐。
  • 多样化题目: 不仅局限于某一类型的题目,尝试不同类型,提升全面能力。

2. 结合在线教程与视频学习

步骤:

  • 基础学习: 对于不熟悉的算法或数据结构,先通过在线教程或视频学习基础知识。
  • 实践应用: 学习完理论后,立即通过刷题进行实践,加深理解。

建议:

  • 精选资源: 选择高质量的教程和视频,如知名教育平台的课程,确保学习内容的准确性和系统性。
  • 笔记记录: 在学习过程中,做好详细的笔记,记录关键概念和重要公式,便于后续复习。

3. 使用代码编辑器与调试工具

步骤:

  • 代码编写: 在刷题过程中,使用高效的代码编辑器(如VS Code、PyCharm)进行代码编写。
  • 调试与测试: 利用调试工具逐步执行代码,观察变量变化,定位问题所在。

建议:

  • 熟悉快捷键: 学习并熟练使用编辑器的快捷键,提高编程效率。
  • 调试技巧: 学习常用的调试技巧,如断点设置、变量监控等,快速排查代码问题。

4. 借助版本控制系统

步骤:

  • 代码管理: 使用Git等版本控制系统管理自己的代码,记录每次的代码修改和优化过程。
  • 备份与分享: 定期将代码推送到GitHub等平台,备份代码并与他人分享,获取反馈和建议。

建议:

  • 学习基础命令: 熟练掌握Git的基础命令,如clone、commit、push、pull等,确保代码管理的顺畅。
  • 建立良好习惯: 每次修改代码后,及时提交并记录修改原因,养成良好的代码管理习惯。