【题目】*1371. 每个元音包含偶数次的最长子字符串
*1371. 每个元音包含偶数次的最长子字符串
*1248. 统计「优美子数组」
给你一个字符串 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 只包含小写英文字母。
【解题思路1】异或运算 前缀和 + 状态压缩
class Solution {
public int findTheLongestSubstring(String s) {
int n = s.length();
int[] pos = new int[1 << 5];//5个元音字母,就是00000-11111,2^5种情况,或者叫状态
Arrays.fill(pos, -1);//用-1填充是怕00000这种情况,避免混淆
int ans = 0, status = 0;
pos[0] = 0;
for (int i = 0; i < n; i++) {
char ch = s.charAt(i);
/*-------------开始----------------*/
//在这里主要是用当前字符去更新上一个子串的状态,因为当前这个字符可能是元音字符或者不是
//用异或的原因就是,我们只关心奇偶性,异或相同为0,不同为1,那么只要将上一个状态对应位与
//1 << (0-4)一下就可以了。也就是第一次来是奇数,那么第二次就是偶数,第三次是奇数...
if (ch == 'a') {
status ^= (1 << 0);
} else if (ch == 'e') {
status ^= (1 << 1);
} else if (ch == 'i') {
status ^= (1 << 2);
} else if (ch == 'o') {
status ^= (1 << 3);
} else if (ch == 'u') {
status ^= (1 << 4);
}
/*--------------结束---------------*/
//得到一个新的状态值,如果这个状态值作索引对应位置的值大于0,
//那么就说明第一次出现该值的位置到当前位置所对应的字符串就是当前
//符合要求的子串,这是因为如果在i的位置和j的位置对应的状态值相等,
//那么这两个子串的奇偶性肯定相等,既然奇偶性相同了,那么中间范围对应的子串就是我们要求的
//同时我们要和之前符合要求的子串比较一下长度,因为我们要取最长的。
if (pos[status] >= 0) {
ans = Math.max(ans, i + 1 - pos[status]);
} else {
//pos只存放每一个状态值第一个出现的位置。
pos[status] = i + 1;
}
}
return ans;
}
}
class Solution {
public int findTheLongestSubstring(String s) {
int res = 0;
char[] c = s.toCharArray();
// Key为前i项的前缀和,value为i
HashMap<Integer,Integer> map = new HashMap<>();
int[] dp = new int[c.length+1];
dp[0] = 0;
for(int i = 0; i < c.length; i++) {
// 当遇到元音时进行异或运算,两个相同字母异或运算为0
if( c[i] == 'a' ||
c[i] == 'e' ||
c[i] == 'i' ||
c[i] == 'o' ||
c[i] == 'u')
dp[i+1] = dp[i] ^ c[i];
// 如果遇到非元音字母则保持前项结果
else
dp[i+1] = dp[i];
// 如果前项和为0,则说明此字串为满足题意要求的子串
if (dp[i+1] == 0) {
res = i + 1;
continue;
}
// 如果当前map中存在当前的前缀和,则当前前缀和与前部前缀和异或运算也为0
if(map.containsKey(dp[i+1])) {
res = Math.max(res,i - map.get(dp[i+1]));
}
// 若不含当前字串前缀和,将其前缀和作为key,i作为value加入map中
else
map.put(dp[i+1],i);
}
return res;
}
}
public int findTheLongestSubstring(String s) {
char[] arr = s.toCharArray();
int even = 0b0, max = 0;
int[] dp = new int[1 << 5]; // "各种"情况,首次出现的索引
Arrays.fill(dp, -1);
dp[even] = 0; // 现在是完美平衡
for (int i = 0; i < arr.length; i++) {
char c = arr[i];
if (c == 'a') even ^= 1;
else if (c == 'e') even ^= (1 << 1);
else if (c == 'i') even ^= (1 << 2);
else if (c == 'o') even ^= (1 << 3);
else if (c == 'u') even ^= (1 << 4);
if (dp[even] == -1) dp[even] = i + 1;
else max = Math.max(max, i + 1 - dp[even]); // 计算距离,首次出现这种情况的索引
}
return max;
}
【解题思路2】动态规划
- 我们把dp[i]作为以字符串i位作为结尾的、满足要求的最长子串长度
- 显然目前的dp[i]没办法进行递推,因为遇到元音字母可能会改变后面的状态,因此加入一维状态,记录元音字母情况
- 将aeiou分别对应到二进制的某一位,这样可以生成一个32以内的int,用这个int来表示另一个状态:当前对应的元音aeiou是奇数还是偶数,原有的dp[i]升级,成为dp[i][j],其中j就是这个int
- 那么很容易想得到状态转移方程:
如果i不是元音,那么dp[i][j]=dp[i-1][j]+1
如果i是元音,那么找到它对应的位,假设是x,那么dp[i][j] = dp[i-1][j xor x] - 扫描所有的dp[i][0],其最大值就是我们想要的答案
class Solution {
public int findTheLongestSubstring(String s) {
char[] chars = new char[]{'a', 'e', 'i', 'o', 'u'};
int[] nums = new int[]{16, 8, 4, 2, 1};
int[][] dp = new int[s.length() + 1][32];
for (int i = 0; i <= s.length(); i++) for (int j = 0; j < 32; j++) dp[i][j] = -1;
dp[0][0] = 0;
int result = Integer.MIN_VALUE;
for (int i = 0; i < s.length(); i++) {
boolean found = false;
for (int j = 0; j < chars.length; j++)
if (chars[j] == s.charAt(i)) {
found = true;
for (int k = 0; k < 32; k++) if (dp[i][k] != -1) dp[i + 1][k ^ nums[j]] = dp[i][k] + 1;
dp[i + 1][0] = Math.max(0, dp[i + 1][0]);
}
if (!found) {
dp[i + 1][0] = dp[i][0] + 1;
for (int k = 1; k < 32; k++) if (dp[i][k] != -1) dp[i + 1][k] = dp[i][k] + 1;
}
result = Math.max(result, dp[i + 1][0]);
}
return result;
}
}