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 104.
Example 1:
Input: s = "ABAB", k = 2 Output: 4 Explanation: Replace the two 'A's with two 'B's or vice versa.
Example 2:
Input: s = "AABABBA", k = 1 Output: 4 Explanation: Replace the one 'A' in the middle with 'B' and form "AABBBBA". The substring "BBBB" has the longest repeating letters, which is 4.
这个题看起来也不算特别难,和前面很多题都很类似,很容易想到动态规划,或者滑动窗口。这一题就是用滑动窗口来做。
思路:
滑动窗口的初始大小是k+1(因为小于k的根本不用判断,肯定可以),从左往右滑动。滑动的过程中,使用int[26]来记录下窗口中不同字符的个数,要满足题意的最好情况,就是选择个数最多的那个字符,然后把剩下所有的字符都换掉。于是,我们可以计算:
窗口大小-最多字符数量<k。如果是小于的, 说明k个字符已经足够满足将这个滑动窗口大小的窗口满足题意了,因此,也不需要往后滑动了,直接窗口大小++,开始新循环。
如果这里是大于,说明k不能满足当前窗口,继续滑动(滑动之后,需要再次计算当前窗口内所含的字符的数量。但是,这时候千万不要遍历计算,会超时的。直接将原来的首个字符减去,加上新的字符就可以了。例如:ABBAB,当前窗口是ABBA,a=2,b=2,向右滑动一格,a--,b++,即可),直到窗口检测完整个字符串,如果都不符合,也就不需要再加大窗口了,直接返回窗口大小-1,即可。
下面是代码,复杂度大致是O(N^2),个人猜测在N(N+1)/2左右:
public class Solution {
boolean isReadDigit;
public int characterReplacement(String s, int k) {
if (s.length() <= k) {
return s.length();
}
char[] cArr = s.toCharArray();
int nMinWin = (int) ((double) k / 25 * 26);
int nWindowSize = nMinWin > k + 1 ? nMinWin : k + 1;
int[] nArrDigit = new int[26];
// init find min window at index 0
for (int i = 0; i < nWindowSize-1; i++) {
nArrDigit[cArr[i] - 'A']++;
}
while (nWindowSize <= s.length()) {
int nMax = -1;
nArrDigit[cArr[nWindowSize-1]-'A']++;
for (int i : nArrDigit) {
nMax = i > nMax ? i : nMax;
}
if (nMax + k >= nWindowSize) {
nWindowSize++;
} else {
break;
}
}
while (nWindowSize <= s.length()) {
boolean isFindLongest = false;
isReadDigit = false;
nArrDigit = new int[26];
for (int i = 0; i <= cArr.length - nWindowSize; i++) {
if (nWindowSize - findMaxDigit(i, nWindowSize, cArr, nArrDigit) <= k) {
isFindLongest = true;
break;
}
}
if (isFindLongest) {
nWindowSize++;
} else {
break;
}
}
return nWindowSize - 1;
}
private int findMaxDigit(int nIndex, int nSize, char[] cArr, int[] nArrDigit) {
if (!isReadDigit) {
for (int i = nIndex; i < nIndex + nSize; i++) {
nArrDigit[cArr[i] - 'A']++;
isReadDigit = true;
}
} else {
nArrDigit[cArr[nIndex - 1] - 'A']--;
nArrDigit[cArr[nIndex + nSize - 1] - 'A']++;
}
int nMax = -1;
for (int i : nArrDigit) {
nMax = i > nMax ? i : nMax;
}
return nMax;
}
}
以上代码中,有几处优化说明一下:
- 14-28行,实际上做了一个预处理操作。给定窗口大小,我只判断从[0,0+窗口大小]这个范围内的字符串,如果符合,直接窗口大小++,循环,直到不符合。这之间,扩大窗口大小的时候,字符数组是用的新的字符++来得到的,而不是重新计算。(之所以要做这个初始化,因为存在很高的概率,从头开始的前面几项都是符合的)
- 对于窗口的初始大小,我不是用的k+1,而是用的下面的式子。这是因为,我们想算的是字母最多的数量是几,字母一共只有26了,不可能有k个完全不同的字符。在k大于52的时候,最多字符数量肯定是大于1的。下面的式子其实算的是一种极端最坏情况:
(double) k / 25 * 26