leecode/problems/1371.find-the-longest-substring-containing-vowels-in-even-counts.md
2020-05-22 18:17:19 +08:00

266 lines
8.0 KiB
Markdown
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 题目地址1371. 每个元音包含偶数次的最长子字符串)
https://leetcode-cn.com/problems/find-the-longest-substring-containing-vowels-in-even-counts/
## 题目描述
```
给你一个字符串 s 请你返回满足以下条件的最长子字符串的长度每个元音字母 'a''e''i''o''u' ,在子字符串中都恰好出现了偶数次。
 
示例 1
输入s = "eleetminicoworoep"
输出13
解释:最长子字符串是 "leetminicowor" ,它包含 eio 各 2 个,以及 0 个 au 。
示例 2
输入s = "leetcodeisgreat"
输出5
解释:最长子字符串是 "leetc" ,其中包含 2 个 e 。
示例 3
输入s = "bcbcbc"
输出6
解释:这个示例中,字符串 "bcbcbc" 本身就是最长的,因为所有的元音 aeiou 都出现了 0 次。
 
提示:
1 <= s.length <= 5 x 10^5
s 只包含小写英文字母。
```
## 暴力法 + 剪枝
### 思路
首先拿到这道题的时候,我想到第一反应是滑动窗口行不行。 但是很快这个想法就被我否定了,因为滑动窗口(这里是可变滑动窗口)我们需要扩张和收缩窗口大小,而这里不那么容易。因为题目要求的是奇偶性,而不是类似“元音出现最多的子串”等。
突然一下子没了思路。那就试试暴力法吧。暴力法的思路比较朴素和直观。 那就是`双层循环找到所有子串,然后对于每一个子串,统计元音个数,如果子串的元音个数都是偶数,则更新答案,最后返回最大的满足条件的子串长度即可`。
这里我用了一个小的 trick。枚举所有子串的时候我是从最长的子串开始枚举的这样我找到一个满足条件的直接返回就行了early return不必维护最大值。`这样不仅减少了代码量,还提高了效率。`
### 代码
代码支持Python3
Python3 Code:
```python
class Solution:
def findTheLongestSubstring(self, s: str) -> int:
for i in range(len(s), 0, -1):
for j in range(len(s) - i + 1):
sub = s[j:j + i]
has_odd_vowel = False
for vowel in ['a', 'e', 'i', 'o', 'u']:
if sub.count(vowel) % 2 != 0:
has_odd_vowel = True
break
if not has_odd_vowel: return i
return 0
```
**复杂度分析**
- 时间复杂度:双层循环找出所有子串的复杂度是$O(n^2)$,统计元音个数复杂度也是$O(n)$,因此这种算法的时间复杂度为$O(n^3)$。
- 空间复杂度:$O(1)$
## 前缀和 + 剪枝
### 思路
上面思路中`对于每一个子串,统计元音个数`,我们仔细观察的话,会发现有很多重复的统计。那么优化这部分的内容就可以获得更好的效率。
对于这种连续的数字问题,这里我们考虑使用[前缀和](https://oi-wiki.org/basic/prefix-sum/)来优化。
经过这种空间换时间的策略之后,我们的时间复杂度会降低到$O(n ^ 2)$,但是相应空间复杂度会上升到$O(n)$,这种取舍在很多情况下是值得的。
### 代码
代码支持Python3Java
Python3 Code:
```python
class Solution:
i_mapper = {
"a": 0,
"e": 1,
"i": 2,
"o": 3,
"u": 4
}
def check(self, s, pre, l, r):
for i in range(5):
if s[l] in self.i_mapper and i == self.i_mapper[s[l]]: cnt = 1
else: cnt = 0
if (pre[r][i] - pre[l][i] + cnt) % 2 != 0: return False
return True
def findTheLongestSubstring(self, s: str) -> int:
n = len(s)
pre = [[0] * 5 for _ in range(n)]
# pre
for i in range(n):
for j in range(5):
if s[i] in self.i_mapper and self.i_mapper[s[i]] == j:
pre[i][j] = pre[i - 1][j] + 1
else:
pre[i][j] = pre[i - 1][j]
for i in range(n - 1, -1, -1):
for j in range(n - i):
if self.check(s, pre, j, i + j):
return i + 1
return 0
```
Java Code
```java
class Solution {
public int findTheLongestSubstring(String s) {
int len = s.length();
if (len == 0)
return 0;
int[][] preSum = new int[len][5];
int start = getIndex(s.charAt(0));
if (start != -1)
preSum[0][start]++;
// preSum
for (int i = 1; i < len; i++) {
int idx = getIndex(s.charAt(i));
for (int j = 0; j < 5; j++) {
if (idx == j)
preSum[i][j] = preSum[i - 1][j] + 1;
else
preSum[i][j] = preSum[i - 1][j];
}
}
for (int i = len - 1; i >= 0; i--) {
for (int j = 0; j < len - i; j++) {
if (checkValid(preSum, s, i, i + j))
return i + 1
}
}
return 0
}
public boolean checkValid(int[][] preSum, String s, int left, int right) {
int idx = getIndex(s.charAt(left));
for (int i = 0; i < 5; i++)
if (((preSum[right][i] - preSum[left][i] + (idx == i ? 1 : 0)) & 1) == 1)
return false;
return true;
}
public int getIndex(char ch) {
if (ch == 'a')
return 0;
else if (ch == 'e')
return 1;
else if (ch == 'i')
return 2;
else if (ch == 'o')
return 3;
else if (ch == 'u')
return 4;
else
return -1;
}
}
```
**复杂度分析**
- 时间复杂度:$O(n^2)$。
- 空间复杂度:$O(n)$
## 前缀和 + 状态压缩
### 思路
前面的前缀和思路我们通过空间prefix换取时间的方式降低了时间复杂度。但是时间复杂度仍然是平方我们是否可以继续优化呢
实际上由于我们只关心奇偶性,并不关心每一个元音字母具体出现的次数。因此我们可以使用`是奇数,是偶数`两个状态来表示,由于只有两个状态,我们考虑使用位运算。
我们使用 5 位的二进制来表示以 i 结尾的字符串中包含各个元音的奇偶性,其中 0 表示偶数1 表示奇数,并且最低位表示 a然后依次是 eiou。比如 `10110` 则表示的是包含偶数个 a 和 o奇数个 eiu我们用变量 `cur` 来表示。
为什么用 0 表示偶数1 表示奇数?
回答这个问题,你需要继续往下看。
其实这个解法还用到了一个性质,这个性质是小学数学知识:
- 如果两个数字奇偶性相同,那么其相减一定是偶数。
- 如果两个数字奇偶性不同,那么其相减一定是奇数。
看到这里,我们再来看上面抛出的问题`为什么用 0 表示偶数1 表示奇数?`。因为这里我们打算用异或运算,而异或的性质是:
如果对两个二进制做异或,会对其每一位进行位运算,如果相同则位 0否则位 1。这和上面的性质非常相似。上面说`奇偶性相同则位偶数,否则为奇数`。因此很自然地`用 0 表示偶数1 表示奇数`会更加方便。
### 代码
代码支持Python3
Python3 Code:
```python
class Solution:
def findTheLongestSubstring(self, s: str) -> int:
mapper = {
"a": 1,
"e": 2,
"i": 4,
"o": 8,
"u": 16
}
seen = {0: -1}
res = cur = 0
for i in range(len(s)):
if s[i] in mapper:
cur ^= mapper.get(s[i])
# 全部奇偶性都相同,相减一定都是偶数
if cur in seen:
res = max(res, i - seen.get(cur))
else:
seen[cur] = i
return res
```
**复杂度分析**
- 时间复杂度:$O(n)$。
- 空间复杂度:$O(n)$
## 关键点解析
- 前缀和
- 状态压缩
## 相关题目
- [掌握前缀表达式真的可以为所欲为!](https://lucifer.ren/blog/2020/01/09/1310.xor-queries-of-a-subarray/)