python - leetcode - 424. 替换后的最长重复字符【经典题解 - 贪心滑动窗口算法】

一. 题目:424. 替换后的最长重复字符

描述:
给你一个字符串 s 和一个整数 k 。你可以选择字符串中的任一字符,并将其更改为任何其他大写英文字符。该操作最多可执行 k 次。

在执行上述操作后,返回包含相同字母的最长子字符串的长度。

示例 1:

输入:s = "ABAB", k = 2
输出:4
解释:用两个'A'替换为两个'B',反之亦然。

示例 2:

输入:s = "AABABBA", k = 1
输出:4
解释:
将中间的一个'A'替换为'B',字符串变为 "AABBBBA"。
子串 "BBBB" 有最长重复字母, 答案为 4。
可能存在其他的方法来得到同样的结果。

提示:

  • 1 <= s.length <= 105
  • s 仅由大写英文字母组成
  • 0 <= k <= s.length

二. 解题思路

首先需要区分两个概念: 子串(子数组) 和 子序列。这两个名词经常在题目中出现,非常有必要加以区分。子串sub-string(子数组 sub-array)是连续的,而子序列 subsequence 可以不连续。

另外一个表达方式:求字符串中一个最长的区间,该区间内的出现次数较少的字符的个数不超过 k。

上面的表达方式跟题目是等价的,抽象成这种表达方式的好处是让我们知道这是一个区间题,求子区间经常使用的方法就是双指针。

「虫取法」:双指针移动的过程和虫子爬动的过程非常像:前脚不动,把后脚移动过来;后脚不动,把前脚向前移动。

解释一下为什么叫它 “贪心的滑窗”,因为除了会 “滑动”, 它还会 “扩充”,这也是紧扣本题解法的核心要素。

  • 滑动:窗口的左端和右端同时向右移动一位
  • 扩充:窗口的左端固定,右端向右移动一位

1. 动态窗口如何初始化?

答:类似动态规划的思路,我们先初始化一个长度为 0 的窗口,从字符串的最左侧开始遍历

2. 什么时候扩充?

答:当子串符合要求时,向右扩充一位(贪心,子串已经满足要求了还要继续膨胀)
追问:子串符合要求(只包含重复字符)的情形
追答: win_len - max_freq <= k
解释:可替换次数 k 足以将当前窗内所有字符都替换成最高频字符

3. 什么时候滑动?

答:当子串不符合要求时,整体向右滑动一位(搜索)
即 win_len - max_freq > k
注意窗口长度是不会减少的,即便后面搜索不到更优子串,也不会影响滑动之前的最优解

双指针的模板,能解决大多数的双指针问题:

def findSubstring(s):
    N = len(s) # 数组/字符串长度
    left, right = 0, 0 # 双指针,表示当前遍历的区间[left, right],闭区间
    counter = collections.Counter() # 用于统计 子数组/子区间 是否有效
    res = 0 # 保存最大的满足题目要求的 子数组/子串 长度
    while right < N: # 当右边的指针没有搜索到 数组/字符串 的结尾
        counter[s[right]] += 1 # 增加当前右边指针的数字/字符的计数
        while 区间[left, right]不符合题意:# 此时需要一直移动左指针,直至找到一个符合题意的区间
            counter[s[left]] -= 1 # 移动左指针前需要从counter中减少left位置字符的计数
            left += 1 # 真正的移动左指针,注意不能跟上面一行代码写反
        # 到 while 结束时,我们找到了一个符合题意要求的 子数组/子串
        res = max(res, right - left + 1) # 需要更新结果
        right += 1 # 移动右指针,去探索新的区间
    return res

三. 代码示例

class Solution(object):
    def characterReplacement(self, s, k):
        """
        :type s: str
        :type k: int
        :rtype: int
        """
        # 解法一
        left = 0
        right = 0
        dic = collections.defaultdict(int)
        maxcount = 0
        if len(s) == 0:
            return 0
        for right in range(len(s)):
            dic[s[right]] += 1
            maxcount = max(maxcount,dic[s[right]])
            if right - left + 1 - maxcount > k:
                dic[s[left]] -= 1
                left += 1
        return right - left + 1

		# 解法二
        N = len(s)
        left, right = 0, 0 # [left, right] 都包含
        counter = collections.Counter()
        res = 0
        while right < N:
            counter[s[right]] += 1
            while right - left + 1 - counter.most_common(1)[0][1] > k:
                counter[s[left]] -= 1
                left += 1
            res = max(res, right - left + 1)
            right += 1
        return res

		# 解法三
        count = [0 for _ in range(26)]  # 记录当前窗口的字母出现次数
        left = 0  # 滑动窗口左边界
        right = 0  # 滑动窗口右边界
        retval = 0  # 最长窗口长度

        while right < len(s):
            count[ord(s[right]) - ord('A')] += 1
            benchmark = max(count)  # 选择出现次数最多的字母为基准
            others = sum(count) - benchmark  # 则其他字母需要通过替换操作来变为基准
            if others <= k:  # 通过与K进行比较来判断窗口是进行扩张?
                right += 1
                retval = max(retval, right - left)  # 记录当前有效窗口长度
            else:  # 通过与K进行比较来判断窗口还是进行位移?
                count[ord(s[left]) - ord('A')] -= 1
                left += 1
                right += 1  # 这里注意:位移操作需要整个向右移,不仅仅只是left向右

        return retval

猜你喜欢

转载自blog.csdn.net/qq_43030934/article/details/131677911