刷题题目解析(二) | 豆包MarsCode AI 刷题

105 阅读7分钟

最小替换子串长度 - MarsCode

今天我们来做一题难度中等的题目(中等就是难度增加)

问题描述

输入一个长度为 4 倍数的字符串,只有ASDF四个字母构成,现要求替换其中一个子串,调整为词频一样的字符串。例如ADDF,只需要替换DS,就可以得到四个字母词频一样的字符串ASDF。求满足要求的最小子串长度。

输入格式

第一行输入一个字符串

输出格式

输出 1 个整数,满足要求的最小子串长度

输入样例 1

ADDF

输出样例 1

1

样例说明:替换DS,将ADDF转为ASDF

输入样例 2

ASAFASAFADDD

输出样例 2

输出:3

样例说明:替换AFASFF,将ASAFASAFADDD转成ASAFASSFFDDD

问题背景与挑战

在算法竞赛和面试中,字符串处理问题一直是一个重要的类别。今天讨论的“最小替换子串长度”问题,是一个典型的中等难度问题,它不仅考察了对字符串操作的理解和应用,还涉及到了滑动窗口算法的运用。这个问题的核心在于如何在保持字符串长度不变的情况下,通过替换一个子串使得字符串中每个字符的出现次数相等。

滑动窗口算法的适用性

滑动窗口算法之所以适用于这个问题,是因为它能够高效地处理连续子串的问题。通过动态地调整窗口的大小,我们可以在O(n)的时间复杂度内找到满足条件的最小子串。这种方法比暴力搜索更加高效,因为它避免了对所有可能子串的枚举,而是通过移动窗口边界来逐步逼近最优解。

算法设计与实现

在实现算法时,我们首先需要统计整个字符串中每个字符的出现次数,并与理想情况下的词频进行比较。如果字符串已经满足条件,即每个字符的出现次数都相等,那么我们可以直接返回0,表示不需要任何替换。

接下来,我们使用滑动窗口来遍历字符串。窗口的扩展和缩小是通过两个指针leftright来控制的。在扩展窗口的过程中,我们记录窗口内每个字符的出现次数,并与理想词频进行比较。如果当前窗口满足条件,我们尝试缩小窗口,以找到更小的满足条件的子串。这个过程一直持续到right指针遍历完整个字符串。

在算法竞赛和面试中,字符串处理问题一直是一个重要的类别。今天讨论的“最小替换子串长度”问题,是一个典型的中等难度问题,它不仅考察了对字符串操作的理解和应用,还涉及到了滑动窗口算法的运用。这个问题的核心在于如何在保持字符串长度不变的情况下,通过替换一个子串使得字符串中每个字符的出现次数相等。

滑动窗口算法的适用性

滑动窗口算法之所以适用于这个问题,是因为它能够高效地处理连续子串的问题。通过动态地调整窗口的大小,我们可以在O(n)的时间复杂度内找到满足条件的最小子串。这种方法比暴力搜索更加高效,因为它避免了对所有可能子串的枚举,而是通过移动窗口边界来逐步逼近最优解。

算法设计与实现

在实现算法时,我们首先需要统计整个字符串中每个字符的出现次数,并与理想情况下的词频进行比较。如果字符串已经满足条件,即每个字符的出现次数都相等,那么我们可以直接返回0,表示不需要任何替换。

接下来,我们使用滑动窗口来遍历字符串。窗口的扩展和缩小是通过两个指针leftright来控制的。在扩展窗口的过程中,我们记录窗口内每个字符的出现次数,并与理想词频进行比较。如果当前窗口满足条件,我们尝试缩小窗口,以找到更小的满足条件的子串。这个过程一直持续到right指针遍历完整个字符串。

滑动窗口算法的基本思想

  1. 初始化窗口:使用两个指针(left 和 right)来表示窗口的边界。
  2. 扩展窗口:将 right 指针向右移动,扩展窗口,直到窗口内的元素满足某个条件。
  3. 缩小窗口:如果窗口内的元素满足条件,尝试将 left 指针向右移动,缩小窗口,以找到更小的满足条件的子串。
  4. 记录结果:在每次缩小窗口时,记录当前窗口的大小,并更新最小窗口大小。
public class Main {
    public static int solution(String input) {
        int n = input.length();
        int idealFreq = n / 4;
        // 统计整个字符串中每个字母的出现次数
        int[] count = new int[4]; // 0: A, 1: S, 2: D, 3: F
        for (char c : input.toCharArray()) {
            count[charToIndex(c)]++;
        }
        // 如果已经满足条件,直接返回0
        if (isIdeal(count, new int[4], idealFreq)) {
            return 0;
        }
        // 滑动窗口
        int left = 0, right = 0;
        int minLen = n; // 初始化为最大可能值
        int[] windowCount = new int[4];
        while (right < n) {
            // 扩展窗口
            windowCount[charToIndex(input.charAt(right))]++;
            right++;
            // 尝试缩小窗口
            while (isIdeal(count, windowCount, idealFreq)) {
                minLen = Math.min(minLen, right - left);
                windowCount[charToIndex(input.charAt(left))]--;
                left++;
            }
        }
        return minLen;
    }
    // 将字符转换为索引
    private static int charToIndex(char c) {
        switch (c) {
            case 'A': return 0;
            case 'S': return 1;
            case 'D': return 2;
            case 'F': return 3;
            default: throw new IllegalArgumentException("Invalid character");
        }
    }
    // 检查当前窗口是否满足理想词频
    private static boolean isIdeal(int[] totalCount, int[] windowCount, int idealFreq) {
        for (int i = 0; i < 4; i++) {
            if (totalCount[i] - windowCount[i] > idealFreq) {
                return false;
            }
        }
        return true;
    }
    public static void main(String[] args) {
        System.out.println(solution("ADDF") == 1);
        System.out.println(solution("ASAFASAFADDD") == 3);
    }
}

关键步骤

主要分为3个部分,先统计字符串,然后创建对应的容器,然后去匹配

  1. 统计整个字符串的词频:使用一个数组count来记录每个字母的出现次数。
  int[] count = new int[4]; // 0: A, 1: S, 2: D, 3: F
        for (char c : input.toCharArray()) {
            count[charToIndex(c)]++;
        }
        // 如果已经满足条件,直接返回0
        if (isIdeal(count, new int[4], idealFreq)) {
            return 0;
        }
  1. 滑动窗口:使用两个指针leftright来表示当前窗口的边界,逐步扩展和缩小窗口,直到找到满足条件的最小子串。
while (right < n) {
            // 扩展窗口
            windowCount[charToIndex(input.charAt(right))]++;
            right++;
            // 尝试缩小窗口
            while (isIdeal(count, windowCount, idealFreq)) {
                minLen = Math.min(minLen, right - left);
                windowCount[charToIndex(input.charAt(left))]--;
                left++;
            }
        }
  1. 检查窗口是否满足条件:在每次扩展和缩小窗口时,检查当前窗口内的字母词频是否满足理想词频。
    private static boolean isIdeal(int[] totalCount, int[] windowCount, int idealFreq) {
        for (int i = 0; i < 4; i++) {
            if (totalCount[i] - windowCount[i] > idealFreq) {
                return false;
            }
        }
        return true;
    }

算法优化与思考

在实现算法的过程中,我们需要注意的是,如何高效地判断当前窗口是否满足条件。在这个问题中,我们通过比较窗口内字符的出现次数与理想词频的差异来实现。如果差异超过了理想词频,那么当前窗口就不满足条件,我们需要继续寻找。

此外,我们还可以考虑如何减少算法的空间复杂度。在这个问题中,我们使用了额外的数组来存储窗口内字符的出现次数,这是必要的,因为我们需要跟踪窗口的变化。但是,我们可以通过一些技巧来减少空间的使用,比如使用位运算或者哈希表来存储字符的出现次数。

总结

通过这个问题,我们不仅学习了滑动窗口算法的应用,还深入理解了如何通过算法来解决实际问题。这个问题的解决过程涉及到了字符串处理、条件判断和算法优化等多个方面,是一个很好的练习机会。在实际应用中,我们可以根据问题的具体要求,灵活地调整算法的设计和实现,以达到最优的解决方案。