問題描述
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 104 .
樣例一:
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個,那麼爲得到(j-i)這麼長的重複字符子串,我們需要用(j-i-m)個字符 α 去替換掉那些不是字符 α 的字符。於是我們比較(j-i-m)和k就可以知道 j 是不是還可以增長。
問題到這裏還沒有結束,我們還需要方法來求出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( n2 )的。
問題到這就算解決了,那麼在確保了正確性的基礎上,我們試着看看能否優化上述代碼,提升效率。
優化
在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。