问题描述
Problem 424: Given a string that consists of only uppercase English letters, you can replace any letter in the string with another letter at most k times. Find the length of a longest substring containing all repeating letters you can get after performing the above operations.
Note: Both the string’s length and k will not exceed
样例一:
Input:
s = “ABAB”, k = 2Output:
4Explanation:
Replace the two ‘A’s with two ‘B’s or vice versa.
样例二:
Input:
s = “AABABBA”, k = 1Output:
4Explanation:
Replace the one ‘A’ in the middle with ‘B’ and form “AABBBBA”.
The substring “BBBB” has the longest repeating letters, which is 4.
解题分析
首先尝试直接易想到的方法。
题目要求的无非是一个连续子串的长度,只要能确定子串首尾两端的下标即可。可用两重循环试着暴力搜索。
因为允许替换k个字符,很明显输出一定比k大,只要将任意连续k个字符替换为同一字符即可得到长度为k的字符重复的子串。
如果以源字符串 s 的长度为n,则循环外层为子串首端的下标 i ,范围为0 ~ n-k;内层为子串尾端的下标 j ,范围为i+k ~ n。我们还需要一个整型变量 result 记录循环找到的长子串中最长的长度,初始值为0或k均可。
在循环内我们需要做的事情是,判断j是否可以再增长,如果可以,自然继续增长去找更长的子串;如果不可以,跳出内层循环。在外层循环中,更新 result 记录。
那么,怎么知道 j 是否还可以增长呢?当然是k个可替换字符的权限还没用完的时候。
分析源字符串s下标在[i , j-1]这个范围内的子串,假设里面字符重复最多的是字符
问题到这里还没有结束,我们还需要方法来求出m的值,即子串中数量最多的重复字符到底有多少个?
我们知道这些字符一共就是(A~Z)26种,那么我们建立一个一维整型数组count[26],遍历子串,统计字符个数,再求一维数组的最大元素即可。注意当 j 增长时,不需要第二次遍历统计字符个数,因为新的长子串只是尾部多了一个新字符而已。而且我们有了判断 j 何时增长的手段,不如让内层循环k从i+1开始增长,这样初始子串只有1个字符,省去了单独遍历k个字符的麻烦。
代码如下:
int characterReplacement(string s, int k) {
int result = 0;
int n = s.size();
for (int i = 0; i < n-k; ++i) {
int count[26] = {};
int j = i+1;
while (j <= n) {
++count[s[j-1]-'A'];
int m = *max_element(count, count+26);
if (j-i-m > k) {
break;
}
++j;
}
result = max(result, j-i-1);
}
return result;
}
其中max_element函数参见这里 。
算法的时间复杂度大致是O(
问题到这就算解决了,那么在确保了正确性的基础上,我们试着看看能否优化上述代码,提升效率。
优化
在dicuss区看了前辈的代码才知道这种做法。一种叫做滑窗(slide window)的技巧。
还是基于确定子串首尾端下标的思想。上面我的做法是先固定首端的下标,再移动尾端的下标。事实上,可以同时移动两端。
再度分析上面代码的执行过程,当尾端下标达到最远值 j 时,表示以下标 i 为首的子串不能再增长了,于是从下标 i+1 开始再找子串。这时我们舍弃了前面做的所有工作,让 j 从 i+1 重新开始。然而,舍弃掉的那部分是有利用价值的。如果前面找到的子串的范围是[a, b],那么再次找的子串至少包含[a+1, b]这一部分,很明显这一部分没有用尽我们的k次替换字符的权限,无论字符是s[a]是否是子串[a, b]中重复最多的字符。这说明了第二次找长串我们可以从 j =b+1开始,而且这样也不用再重新统计一遍[a+1, b]范围内的字符个数,利用前一次循环的成果对count的更新是十分简单的,只需要使字符s[a]个数减一即可。
代码如下:
int characterReplacement(string s, int k) {
int i = 0;
int j = 0;
int count[26] = {};
while (j < s.size()) {
++count[s[j]-'A'];
if (j-i-*max_element(count, count+26)+1 > k) {
--count[s[i]-'A'];
++i;
}
++j;
}
return j-i;
}
只用了一层循环,时间复杂度为O(n)。
高能预警
下面分享在讨论区看到的大神的代码,看见标题就点进去了。这篇文章所讲的方法和思路虽经过润色,但是可以看出来基本都是受到这位大神stefanpochmann的启发。
他在讨论区的帖子的标题是7 lines c++, 也就是“7行C++”。
int characterReplacement(string s, int k) {
int i = 0, j = 0, ctr[91] = {};
while (j < s.size()) {
ctr[s[j++]]++;
if (j-i - *max_element(ctr+65, ctr+91) > k)
ctr[s[i++]]--;
}
return j - i;
}
后记
让我们再次审视优化后的方法,可以注意到,[i, j]这个下标范围是只增不减的,j增加的不会比i少。而准确地判断何时应该增长j,让我们不会错过最长的那个子串(最长的符合要求的子串可能不止一个)。
关键在于理解题意,将k个可替换字符的能力转化为数学意义上的不等式条件。
当我们路过那个最长子串之后,滑窗的范围仍然没有减少,这是为什么呢?去掉一个重复最多的字符,新增一个非重复最多的字符,不就需要k+1个替换字符才能维持最长子串的长度了吗?这是滑窗方法另一个巧妙的地方。
碰到上述的情况,仔细想一想,这不就是告诉我们从这个首位置不可能得到更长的甚至是同样长的符合要求的子串了吗?因此,参照我们之前的做法,应该从下一个首端下标开始找子串,而且是找更长的子串。因此 j 每次都增一,而 i 每次不变或增一。
同样,由于滑窗范围不减的特性,它自动保存了最长的符合要求的子串的长度,因此结尾只需返回j-i。