6.11 note
二进制化思想
class Solution {
public:
int findTheLongestSubstring(string s) {
vector<int> pre(32,INT_MAX);
pre[0]=-1;
const int N=s.size();
int cur=0;
int ans=0;
for(int i=0;i<N;++i){
switch(s[i]){
case 'a':cur^=1;break;
case 'e':cur^=2;break;
case 'i':cur^=4;break;
case 'o':cur^=8;break;
case 'u':cur^=16;break;
default:break;
}
if(pre[cur]==INT_MAX) pre[cur]=i;
else ans=max(ans,i-pre[cur]);
}
return ans;
}
};
代码是找字符串中最长的子串,使得子串内每个元音字母(a/e/i/o/u)出现偶数次。用生活例子来通俗解释:
核心思路:用“状态压缩”标记元音奇偶性
- 把每个元音字母看作一个“开关”,出现奇数次时打开,偶数次时关闭。
- 用数字 cur 表示当前状态:
- a 对应二进制第0位(1), e 对应第1位(2), i 对应第2位(4), o 对应第3位(8), u 对应第4位(16)。
- 比如:子串中有奇数个a和偶数个e,其他元音0个,状态就是 1 ^ 0 = 1 (二进制00001)。
代码逐段拆解
1. 初始化状态数组
- pre[32] :记录每个状态第一次出现的位置(32是因为5个开关最多2^5=32种状态)。
- pre[0] = -1 :初始状态(所有元音都是偶数次)出现在位置-1(类似“起点前”)。
2. 遍历字符串更新状态
- 遇到元音时,用 cur ^= 对应值 切换状态:
- 比如遇到 a , cur 异或1(1变0,0变1),表示a的奇偶性翻转。
- 非元音字母不影响状态(比如辅音相当于“路过不操作”)。
3. 找最长子串
- 当当前状态 cur 之前出现过时( pre[cur] != INT_MAX ),说明从上次出现的位置到当前位置之间的子串,所有元音奇偶性相同(即偶数次)。
- 子串长度为 i - pre[cur] ,更新最大长度 ans 。
举个例子理解
- 字符串: "aabaoo" (索引0-5)
- 遍历过程:
- i=0(a):cur=1,pre[1]=0
- i=1(a):cur=1^1=0,pre[0]=-1,ans=1-(-1)=2
- i=2(b):cur=0,pre[0]已记录,ans=2-(-1)=3
- i=3(a):cur=0^1=1,pre[1]=0,ans=3
- i=4(o):cur=1^8=9,pre[9]=4
- i=5(o):cur=9^8=1,pre[1]=0,ans=5-0=5
- 最终最长子串是 "aabaoo" 中0-5的部分,长度5,其中a出现2次(偶)、o出现2次(偶),其他元音0次(偶)。
核心原理总结
- 利用“异或”特性:相同状态之间的子串,所有元音奇偶性相减为偶数(即总次数偶数)。
- 用数组记录每个状态首次出现的位置,避免重复计算,时间复杂度O(n),空间O(1)(32是固定大小)。
- 本质是“前缀和+状态压缩”的巧妙结合,把元音奇偶性转化为数字状态,快速定位符合条件的子串。
看到子串,想滑动窗口
奇偶频次求和 最大差值
枚举➕前缀和
class Solution {
public:
int maxDifference(string s, int K) {
int n = s.size();
// f[i][c]:前 i 个字符中,字符 c 出现了几次
int f[n + 1][5];
for (int j = 0; j < 5; j++) f[0][j] = 0;
for (int i = 1; i <= n; i++) {
for (int j = 0; j < 5; j++) f[i][j] = f[i - 1][j];
f[i][s[i - 1] - '0']++;
}
const int INF = 1e9;
// 求出奇数频率的字符为 x,偶数频率的字符为 y 的情况下的最大子数组和
auto gao = [&](int x, int y) {
int ret = -INF;
// g[0/1][0/1] 表示 x 和 y 在前缀里分别出现奇(偶)数次的最小前缀和
int g[2][2];
for (int i = 0; i < 2; i++) for (int j = 0; j < 2; j++) g[i][j] = INF;
// 枚举子数组的右端点 i
// j 是一个单调指针,表示子数组的左端点最大可以到哪
for (int i = 1, j = 0; i <= n; i++) {
while (i - j >= K && f[i][x] != f[j][x] && f[i][y] != f[j][y]) {
int &t = g[f[j][x] & 1][f[j][y] & 1];
t = min(t, f[j][x] - f[j][y]);
j++;
}
int now = f[i][x] - f[i][y];
// x 要出现奇数次,所以在前缀里频率的奇偶性必须不同
// y 要出现偶数次,所以在前缀里频率的奇偶性必须相同
ret = max(ret, now - g[f[i][x] & 1 ^ 1][f[i][y] & 1]);
}
return ret;
};
int ans = -INF;
// 枚举两种字符分别是哪个
for (int i = 0; i < 5; i++) for (int j = 0; j < 5; j++) ans = max(ans, gao(i, j));
return ans;
}
};
解决一个字符串问题,核心目标是找出满足特定条件的子数组,计算其“最大差值”。我用生活化的例子来通俗解释:
整体思路
假设字符串里的字符是0-4这5种“水果”(比如0是苹果,1是香蕉...),我们要找一个长度≥K的子数组,使得其中两种水果(设为A和B)的数量差最大。
关键步骤拆解
1. 统计前缀水果数量
- 用 f[i][c] 记录前i个字符中,水果c出现的次数。比如 f[5][0] 表示前5个字符里苹果的数量。
- 初始化时,前0个字符中所有水果都是0个,然后逐个字符统计:比如第i个字符是水果3,就把 f[i][3] 加1。
2. 定义差值计算函数gao(x,y)
- 这个函数专门计算:当子数组中水果x出现奇数次、水果y出现偶数次时,能得到的最大数量差。
- 举个例子:假设x是苹果,y是香蕉,我们要找一个子数组,其中苹果出现奇数次、香蕉出现偶数次,然后算(苹果数-香蕉数)的最大值。
3. 滑动窗口找最优子数组
- 用 g[a][b] 记录“前缀中x出现a次奇偶性、y出现b次奇偶性”时的最小(x数-y数)值。
- 遍历每个右端点i,用指针j向左滑动,保证子数组长度≥K,同时维护 g 的最小值,这样用当前值 now 减去 g 就能得到最大差值。
4. 枚举所有水果组合
- 因为x和y可以是任意两种水果(包括相同的),所以遍历所有i和j的组合,调用 gao(i,j) 找到最大的差值。
核心逻辑总结
- 先统计每个位置的水果数量前缀和,再通过滑动窗口和奇偶性判断,找到满足条件的子数组,最后枚举所有水果组合求最大值。
- 本质是通过数学上的奇偶性优化,减少计算量,快速找到符合条件的最大差值。