1. 前言
最近看见周围同学不少已经有了好去处,顿感菜菜,写写力扣复习下408吧那就。
2. 题目
2.1 题意
对字符串s做多次查询,每次查询:字符串下标[i,j]之间,最多替换 k 个字符成,且可调整顺序,最后是否可以变成回文串。
2.2 分析
时间复杂度在O(n)
1 <= s.length, queries.length <= 10^5
可以想到肯定遍历一遍字符串将信息存起来,每次查询直接使用。
- 怎么查询
- 关键是怎么查询,关系到怎么存信息
- 使用记录的下标i,j的信息
- 统计已经出现落单的字符个数, 成对的可以放在两边凑回文串,如果查询长度为奇数还可以将一个落单的字符放在字符串中间
- 落单的个数 - 查询长度是否为奇数 <= 可变化的字符数 * 2 满足为true
- 为什么不等式右边乘2,因为字符变一半就可以和另一半相同
- 怎么存信息?
- 题中只有小写字母,可以直接用数组存,mp[i]记录字符串到下标i的信息,具体可以开一个大小26的数组mp[i][0],表示字符串到下标i时字符a出现的次数
2.3 我的解法
class Solution {
public:
vector<bool> canMakePaliQueries(string s, vector<vector<int>>& queries) {
int n = s.length();
int m = queries.size();
vector<vector<int> > mp(n+1, vector(26, 0));
// 遍历获取信息
// 注意细节:下标
// vector多开1方便下面查询 避免越界
// mp[i] 对应得信息实际上是到下标i-1的信息
for(int i=1; i<=n; i++){
for(int j=0; j<26;j++){
mp[i][j] = mp[i-1][j];
}
mp[i][s[i-1]-'a']++;
}
// 查询获得结果
vector<bool> ans;
for(int i=0; i<m; i++){
// 是否奇数长度
int oddFlag = (queries[i][1] - queries[i][0] + 1)%2;
// 落单个数
int oddNum = 0;
for(int j=0; j<26; j++){
// 细节,下标
// 是queries[i][1]+1 和 queries[i][0]
// 因为queries[i][1]+1记录的是到下标queries[i][1]的信息,需要用到
// queries[i][0]记录的是到下标queries[i][0]-1的信息
// 两者相减才能包括到左边端点的信息
oddNum += ( (mp[queries[i][1]+1][j] - mp[queries[i][0]][j])%2 );
}
ans.emplace_back(bool(oddNum-oddFlag <= 2*(queries[i][2] ) ) );
}
return ans;
}
};
2.4 学习题解反思
我的时间复杂度O(26n), 空间复杂度O(26n) (即为O(n))
题解中对存的信息进行了压缩,并且使用异或计算很巧妙。
统计落单的个数,实际就是字符串中字符数目为奇数的字符个数,实际只用看奇偶,不用记录个数,只需要用1位01记录1个字符,比如0表示字符有偶数个,1表示奇数个;又共26个字符,使用26位用one-hot编码记录,也就是每个下标只需要一个int就可以记录信息。
另外统计信息时使用异或运算, count[i + 1] = count[i] ^ (1 << (s[i] - ‘a’)); 因为当前下标较于前一个下标只多一个变化,只需要将变化的位改变,(1 << (s[i] - ‘a’))中,要改变的位为1,1异或等于取反符合奇变偶、偶变奇; 而其他位置异或的数为0,不变。
虽然题解的时间空间复杂度渐进意义下和我的解法是相同,但是实际上性能是比我的写法好的。
题解的查询方法也很有特色: count[r + 1] ^ count[l]然后统计1的个数
可以这么做是因为这样如果两个位置都是1或是0,那么这段字符串中肯定是偶数个该字符,反之奇数个字符,和异或相同。
(实际这里的统计1的个数也给时间复杂度加上了一个常数26)
2.5 bug日记
2.5.1 不等式推导错误
落单的个数 - 查询长度是否为奇数 <= 可变化的字符数 * 2 满足为true不等式推导错
(右边没有乘2
3. 后记
仅分享自己的想法,有意见和指点非常感谢