題目
給你一個字符串 s
,請你返回滿足以下條件的最長子字符串的長度:每個元音字母,即 ‘a’,‘e’,‘i’,‘o’,‘u’ ,在子字符串中都恰好出現了偶數次。
示例 1:
輸入:s = "eleetminicoworoep"
輸出:13
解釋:最長子字符串是 "leetminicowor" ,它包含 e,i,o 各 2 個,以及 0 個 a,u 。
示例 2:
輸入:s = "leetcodeisgreat"
輸出:5
解釋:最長子字符串是 "leetc" ,其中包含 2 個 e 。
示例 3:
輸入:s = "bcbcbc"
輸出:6
解釋:這個示例中,字符串 "bcbcbc" 本身就是最長的,因爲所有的元音 a,e,i,o,u 都出現了 0 次。
提示:
1 <= s.length <= 5 x 10^5
s
只包含小寫英文字母。
解題思路
注:參考Leetcode官方解答。
先考慮暴力方法,首先枚舉所有子串,遍歷子串中的所有字符,統計元音字母出現的個數。如果符合條件,我們就更新答案,這是O(n^3)的解法,顯然不能通過。
考慮怎麼優化,對於一個區間,我們可以用兩個前綴和的差值,得到其中某個字母的出現次數。對每個元音字母維護一個前綴和,定義 pre[i][k] 表示在字符串前 i 個字符中,第 k 個元音字母一共出現的次數。假設我們需要求出 [j,i] 這個區間的子串是否滿足條件,那麼我們可以用 pre[i][k]-pre[j-1][k],在 O(1) 的時間得到第 k 個元音字母出現的次數。對於每一個元音字母,我們都判斷一下其是否出現偶數次即可。
雖然利用前綴和優化了統計子串的時間複雜度,但是枚舉所有子串的複雜度仍需要 O(n^2),還需要繼續進行優化,避免枚舉所有子串。我們考慮枚舉字符串的每個位置 i,計算以它結尾的滿足條件的最長字符串長度。其實我們要做的就是快速找到最小的j∈[0,i),滿足 pre[i][k]-pre[j][k](即每一個元音字母出現的次數)均爲偶數,那麼以 i 結尾的最長字符串 s[j+1,i]的長度就是 i-j。
因爲有一個數學性質:奇數減奇數等於偶數,偶數減偶數等於偶數。所以對於滿足條件的子串來說,兩個前綴和 pre[i][k] 和 pre[j][k]的奇偶性一定是相同的。因此我們可以對前綴和稍作修改,從維護元音字母出現的次數改作維護元音字母出現次數的奇偶性。這樣我們只要實時維護每個元音字母出現的奇偶性,那麼 s[j+1,i] 滿足條件當且僅當對於所有的 k,pre[i][k] 和 pre[j][k] 的奇偶性相同,此時我們就可以利用哈希表存儲每一種奇偶性(即考慮所有的元音字母)對應最早出現的位置,邊遍歷邊更新答案。
又因爲出現次數的奇偶性無非就兩個值,0 代表出現了偶數次,1 代表出現了奇數次,我們可以將其壓縮到一個二進制數中,第 k 位的 0 或 1 代表了第 k 個元音字母出現的奇偶性。這樣我們就可以將 5 個元音字母出現次數的奇偶性壓縮到了一個二進制數中,且連續對應了二進制數的 [00000,11111]的範圍,轉成十進制數即 [0,31]。因此可以不用哈希表,直接用一個長度爲 32 的數組來存儲對應狀態出現的最早位置即可。
具體做法如下:如果座標 j 對應的狀態碼與座標 i 對應的狀態碼相同,那麼他們倆中間的元音字母數一定是偶數。所以我們每次求出一個座標的狀態碼的時候就去查詢這個狀態碼前面是否存在,如果存在,那麼就計算一下之間子串的長度,並更新最大子串長度。
(詳細步驟見代碼註解)
複雜度分析:
時間複雜度:O(n),其中 n 爲字符串 s 的長度。我們只需要遍歷一遍字符串即可求得答案,因此時間複雜度爲 O(n)。
空間複雜度:O(S),其中 S 表示元音字母壓縮成一個狀態數的最大值,在本題中 S = 32。我們需要對應 S 大小的空間來存放每個狀態第一次出現的位置,因此需要 O(S) 的空間複雜度。
代碼
Python代碼如下:
class Solution:
def findTheLongestSubstring(self, s: str) -> int:
n = len(s)
res = 0 # 記錄最大長度
status = 0 # 初始狀態爲0,表示所有元音字母出現了0次
pos = [-2]*32 # 記錄狀態出現的位置,初值設置爲-2(比-1小的數都可以)
pos[0] = -1 # 狀態碼爲0設置在第一個元素的左邊,即-1的位置
for i in range(n):
if s[i] == 'a':
status ^= 1<<0 # 00001
elif s[i] == 'e':
status ^= 1<<1 # 00010
elif s[i] == 'i':
status ^= 1<<2 # 00100
elif s[i] == 'o':
status ^= 1<<3 # 01000
elif s[i] == 'u':
status ^= 1<<4 # 10000
if pos[status] != -2: # 如果狀態碼不爲-2,就表示重複出現了這個狀態碼,那麼更新一下最大長度
res = max(res, i-pos[status])
else: # 否則就表示第一次出現
pos[status] = i
return res
Java代碼如下:
class Solution {
public int findTheLongestSubstring(String s) {
int n = s.length();
int res = 0;
int status = 0;
int[] pos = new int[32];
Arrays.fill(pos, -2);
pos[0] = -1;
for(int i=0; i<n; i++){
char c = s.charAt(i);
if(c == 'a'){
status ^= 1<<0;
}else if(c == 'e'){
status ^= 1<<1;
}else if(c == 'i'){
status ^= 1<<2;
}else if(c == 'o'){
status ^= 1<<3;
}else if(c == 'u'){
status ^= 1<<4;
}
if(pos[status] != -2){
res = Math.max(res, i-pos[status]);
}else{
pos[status] = i;
}
}
return res;
}
}