https://leetcode.com/problems/longest-repeating-character-replacement/
给定仅由大写英文字母组成的字符串s,可以将任意位置上的字符替换为其他字符,最多可以替换k次。在执行上述操作后,找到包含重复字母的最长子串的长度。字符串长度和k不会超过10^4。
输入:s = "AABABBA",k=1
输出:4
将中间的一个'A'替换为‘B’,字符串变为"AABBBBA". 子串"BBBB"有最长重复字母,因此答案为4.
这类题是比较经典的滑动窗口问题。给定k,要想使替换后所有字符都一样,并且重复、连续的部分更长,我们应该替换出现次数最多字符以外的字符。由于我们找的是最长子串,那么如果窗口内能够在最多替换k个字符后所有的字符都相同,右边界就继续右移。如果不满足这个条件就固定右边界,开始移动左边界。
我们考虑两个问题:
(1)左指针开始移动的条件是什么?
右指针的每次移动需要使得窗口内的子数组能够满足最多替换k个字符后,所有的字符都相同。当右指针移动到窗口内最多出现的字符次数(记为max_occurence)+k (2)左右指针右移时要更新的数据有哪些? 左右指针右移时要更新新加入窗口的字符的出现次数。 下面举例s = "AABABBA",k=1来说明算法步骤: 由于窗口内元素只有A,不需要替换字符,因此更新最长重复子串长度为1,right可以继续右移。 窗口内元素为[A,A],不需要替换字符,因此更新最长重复子串长度为2,right继续右移。 窗口内元素为[A,A,B],由于窗口内出现次数最多的字符为A,出现次数为2次。2+k == right-left,将B替换为A即可得到重复子串"AAA"。因此更新最长重复子串长度为3,right继续右移。 窗口内元素为[A,A,B,A],窗口内出现次数最多的字符为A,出现次数为3次。3+k == right-left,将B替换为A即可得到重复子串"AAAA"。因此更新最长重复子串长度为4,right继续右移。 窗口内元素为[A,A,B,A, B],窗口内出现次数最多的字符为A,出现次数为3次。3+k < right-left,因此替换一个字符后窗口内的字符也不会都相同。right停止右移。如果right继续右移,且新加入窗口的字符为‘A’,将‘B’替换为A也需要2次替换机会,而k仅为1,因此子串内的元素不会都相同。如果新加入窗口的字符为其他字符,且因此改变了出现次数最多的字符(之前出现次数较少的字符超过了原来出现次数最多的字符A),那么需要将A全部替换为新的出现次数最多的字符,而A有3个,k仅为1,因此子串内的元素也不会都相同。 因此如果长度为L的子串不符合题目要求, 那么左边界固定,长度更长的子串也不符合题目要求,这里的题目要求为“在至少替换k个字符后,子串内的元素都相同。” 接下来固定右指针,开始移动左指针。 窗口内元素为[A,B.A,B],出现次数最多的字符为A,出现次数为2次,因为有2+k 窗口内元素为[B.A,B],出现次数最多的字符为B,出现次数为2次,因为有2+k==right-left,因此替换字符A后窗口内的字符为[B,B,B]。又找到了一个满足条件的子串,因此更新替换字符后的最长重复子串长度,由于3<4,因此当前最长重复子串长度仍为4。左边界固定,右边界右移。 窗口内元素为[B.A,B,B],出现次数最多的字符为B,出现次数为3次,因为有3+k==right-left,因此替换字符A后窗口内的字符为[B,B,B,B]。更新当前最长重复子串长度为4,right边界右移。 窗口内元素为[B.A,B,B,A],出现次数最多的字符为B,出现次数为3次,因为有3+k 窗口内元素为[A,B,B,A],出现次数最多的字符为B,出现次数为2次,因为有2+k 窗口内元素为[B,B,A],出现次数最多的字符为B,出现次数为2次,因为有2+k==right-left,因此替换字符A后得到[A,A,A],可以更新当前最长重复子串长度。左指针停止移动,此时右指针也到达数组终点,停止移动。返回当前最长重复子串长度即可。 right-left为窗口内字符个数,等价于窗口中所有字符出现次数之和。这样我们在判断条件时不用遍历窗口,只需要遍历维护窗口内字符出现次数的字典即可。 由于check只是扫描最大大小为26的字典,并不随样本数量的增大而增大,时间复杂度为O(N)。 上面的代码实现容易理解,虽然时间复杂度不高,但是运行时间较长。这道题可以进一步优化。 由于我们只对最长重复子串感兴趣,只需要找出满足(length-max_occurence)<=k的最长子串。假设我们已经找到了一个满足条件的子串,如果右边界右移后得到的条件仍满足子串,那么窗口就正常扩张即可。 左边界向右移时,窗口内出现次数最多的字符次数max_occurence要么不变,要么值减去1。由于我们只关注“最长子串”,而满足条件的最长子串长度是由max_occurence+k决定的。k不变,如果max_occurence变大,我们才能得到一个更长的满足条件的子串。因此我们只需要在右边界右移时更新max_occurence,因为右边界右移时max_occurence才可能增加,因为向窗口中引入了新字符。因此左边界右移时不需要更新max_occurence,当max_occurence不会变大时,不会产生更大的最终结果。 如果右边界右移后得到的子串不再满足条件了,我们没必要收缩窗口,只需要将窗口向右平移一位即可(在具体实现中表现为左指针右移一位,在下一次循环中右指针右移一位,这样窗口就向右平移了一位),因为我们不需要保证窗口内的子数组满足条件,只需要通过滑动窗口找更大的max_occurence。 唯一能够使滑动窗口长度增长的条件是出现一个让当前max_occurence变大的字符,因此不满足(length-max_occurence)<=k条件时,我们直接让滑动窗口以当前长度一直滑动下去。 举s = "AABCABBB",k=2的例子,右边界一直右移,直到不满足(length-max_occurence)<=k,此时max_occurence为3,当前满足条件的最长子串长度为5。 接下来我们先将左边界右移一位,虽然此时窗口[left,right)仍是不符合条件的子串[A,B,C,A,B],但是子串的长度一定等于当前维护的替换k次后字符都相等的最长子串长度(因为上一次右边界右移使得条件不符合,那么在右边界右移之前窗口内的子串一定是符合条件的当前最长子串,因此右边界右移且左边界右移后窗口内的子串长度是不变的),当前满足条件的最长子串长度仍为5。 接着右边界右移,相当于滑动窗口平移了一位。新加入窗口的字符B不会使max_occurence变大。 此时仍不满足(length-max_occurence)<=k条件,因此左边界右移一位,当前满足条件的最长子串长度仍为5。 右边界右移一位,相当于滑动窗口平移了一位。新加入窗口的字符B使得max_occurence增大为4。并且此时满足条件(length-max_occurence)<=k,因此更新满足条件的最长子串长度为right-left = 6。 右边界继续右移,窗口扩张。新加入窗口的字符B使得max_occurence增大为5。并且此时满足条件(length-max_occurence)<=k,因此更新满足条件的最长子串长度为right-left = 7。 此时right到达字符串末尾,算法执行结束。 由于滑动窗口要么平移,要么扩张,长度不会缩小。因此当right到达字符串末尾时,right-left的长度一定是满足条件的最长子串长度。因此我们最后可以直接返回right-left。 https://leetcode-cn.com/problems/longest-repeating-character-replacement/solution/ti-huan-hou-de-zui-chang-zhong-fu-zi-fu-eaacp/Python实现
class Solution:
def characterReplacement(self, s: str, k: int) -> int:
def check(window,k):
length = 0
maxcount = 0
for i in window:
maxcount = max(maxcount,window[i])
length += window[i]
return maxcount + k >= length #窗口内的元素能够被转化为完全相同的元素
left,right = 0,0
from collections import defaultdict
window = defaultdict(int)
res = 0
while(right
进一步优化
Python实现
class Solution:
def characterReplacement(self, s: str, k: int) -> int:
left,right = 0,0
from collections import defaultdict
window = defaultdict(int)
max_occur = 0
while(right < len(s)):
c = s[right]
right += 1
window[c] += 1
max_occur = max(max_occur,window[c]) #更新出现频次最多的字符次数
if(k + max_occur < right - left): #需要平移滑动窗口,因此先将左边界右移。由于max_occur没有在移动左边界时更新,因此if语句只会被执行一次,即左边界只移一位
d = s[left]
left += 1
window[d]-=1
return right-left
参考