前言
为什么引入前缀和,如何利用二进制数描述状态,条件如何一步步等价转化。引入哈希表存储状态,查找哈希表为了寻找满足条件的 pair
题目意思
给你一个字符串 s ,请你返回满足以下条件的最长子字符串的长度:每个元音字母,即 ‘a’,‘e’,‘i’,‘o’,‘u’ ,在子字符串中都恰好出现了偶数次
引入 前缀和:将双变量转为单变量
-
[i, j][i,j] 区间确定出一个子串,题目条件:[i, j][i,j] 内出现的元音均为偶数
-
变量有 2 个,如果找齐所有区间需要两层循环
-
其实这类“子串”、“子数组”问题,可以利用【前缀和相减】转化为:
[0, j][0,j] 的元音次数 -− [0, i - 1][0,i−1] 对应的元音次数 == 偶数
-
求 [0,x][0,x] 的元音次数,变量就只有 1 个了
我们只关心【差值】是不是偶数,不关心偶数等于几
奇数 - 奇数 = 偶数;偶数 - 偶数 = 偶数;一个偶数一个奇数之差一定是奇数
因此条件等价于:
[0, j][0,j] 出现的元音次数的奇偶,相同于,[0, i - 1][0,i−1] 对应的元音次数的奇偶
【特别注意】出现 0 次(没有出现),也是出现了偶次~
非奇即偶,奇偶是相对的状态
- 两个相反、相对的状态,可以抽象为 0 和 1 —— 用 0 代表偶数,1 代表奇数
- [0,x][0,x] 的 u o i e a 各自出现的奇偶次数,都用 0/1 表示,组成一个 5 位二进制数。譬如,00001,代表其中 u o i e 都出现偶次(包括0),a 出现奇次
- 管这叫 [0,x][0,x] 区间的 state ,它包含 [0,x][0,x] 中 5 个元音出现次数的奇偶信息
题目再次等价转化
条件等价转化为:
[0, j][0,j] 的 state 和 [0, i - 1][0,i−1] 的 state 相同,即两个二进制数相等!
遍历字符串,不断求出 [0, x][0,x] 的 state ,看看 哪些 state 是相同的,并找出位置 离得最远 的
怎么求前缀区间的 state?
- 假设 [0,2][0,2] 的 state 是 00110 ,代表出现 e 和 i 奇数次,假如下一字符是 ii,则 [0,3][0,3] 区间出现的 ii 次数变为偶数, state 为 00010
- 遇到 ii ,从 00110 变到 00010 ,第三位从 1 翻转为 0,其他位不变
使特定的位翻转 正是 异或 的作用,第三位翻转,就是异或了 00100
+异或 相当于 不带进位的二进制加法,所以有 00110 ^ 00100 = 00010 - 元音字母 u o i e a ,分别对应了:10000、01000、00100 ……
所以,当前前缀区间的 state 等于 前一个求出 state 异或 当前字符对应的二进制数。好比累乘、累加,只是这是 累异或.
预置 -1 的情况,使通式普遍适用
-
[i,j][i,j] 的 u o i e a 出现偶次 <=> [0, j][0,j] 的 state 相同于 [0, i - 1][0,i−1] 的 state
-
ii 显然可以为 0 ,则 i-1i−1 为 -1 ,特别地,我们让 [0,-1][0,−1] 的 state 是 00000 ,表示在字符串 -1 的位置,所有元音都出现 0 次(偶数)
-
为了让边界情况也能套用通式,即 i= 0i=0 时,[0,j][0,j] 的元音都出现 偶次 <=> [0, j][0,j] 的 state 等于 00000,通式成立!
-
前缀区间的 state 怎么存
-
可以选择存到 数组 里,数组的索引和字符位置一一对应
-
也可以用 哈希Map,存键值对
key: state 值
value:对应在字符串中的位置 -
我们选择 Map ,将逐个求出的 [0, x][0,x] 的 state 存入 Map
-
为了书写方便,转成十进制,00110 就存 6 ,是等价的
寻找满足条件的 state
-
遍历过程中,边存 state ,要边在 Map 中查找:
-
看看有没有 之前存过的,与当前 state 相同的 state
-
如果有,则可能不止一个,要根据 value 值,求出它离当前位置的距离,找出有着最长距离的那个,就是我们想要的最大子串长度
主要思路准备工作
-
vowel 表,对照表,一个元音对应一个二进制数
-
Map 对象,初始放入 0: -1 键值对,代表 [0,-1][0,−1] 对应的 state 为 00000 ,即十进制 0
-
state 变量,保存当前前缀区间的 state ,初始值 0
-
遍历字符串,如果当前字符是元音,在 vowel 表获取对应的二进制数,异或 上一次求出的 state ,求出当前的 state
-
不断往 Map 存入 state
-
边存边查看,和当前 state 相等的,之前存过的 state 的 位置
-
求出它与当前位置的距离,这个距离代表 满足条件的子串的长度
-
在 遍历的过程 中,让这个距离 不断挑战 res ,如果比 res 大就更新 res
-
最后返回 res
代码
class Solution {
public:
int findTheLongestSubstring(string s) {
int res = 0,dis = 0;
/*前缀区间定义,00000代表五元音全偶*/
int bitArray = 0;
map<int, int> bit_to_site;
map<int, int>::iterator iter;
bit_to_site.insert({0,-1});
for(int i = 0; i < s.size(); i++){
char c = s[i];
switch(c){
case 'a': bitArray ^= 1; break;
case 'e': bitArray ^= 2; break;
case 'i': bitArray ^= 4; break;
case 'o': bitArray ^= 8; break;
case 'u': bitArray ^= 16; break;
default:break;
}
iter = bit_to_site.find(bitArray);
if(iter == bit_to_site.end()){
bit_to_site.insert({bitArray, i});
}
dis = i - bit_to_site[bitArray];
res = max(dis,res);
}
return res;
}
};