题目描述:
给你一个字符串 s,请你对 s 的子串进行检测。
每次检测,待检子串都可以表示为 queries[i] = [left, right, k]。我们可以 重新排列 子串 s[left], ..., s[right],并从中选择 最多 k 项替换成任何小写英文字母。
如果在上述检测过程中,子串可以变成回文形式的字符串,那么检测结果为 true,否则结果为 false。
返回答案数组 answer[],其中 answer[i] 是第 i 个待检子串 queries[i] 的检测结果。
注意:在替换时,子串中的每个字母都必须作为 独立的 项进行计数,也就是说,如果 s[left..right] = "aaa" 且 k = 2,我们只能替换其中的两个字母。(另外,任何检测都不会修改原始字符串 s,可以认为每次检测都是独立的)
关键的信息是可以重新排序,那就没必要使用传统的检测回文串的方法。我们想一下回文串有什么特点?
比如aabaa,a出现了4次,b出现了1次
又比如abccba,a,b,c各出现了2次
再比如acbdbca,a,b,c出现了2次,d出现了一次
到这可以看出,回文串各字母出现的次数,要么全是偶数,要么只有一个字母是奇数的,其他也都是偶数。
再看一下题目所说可以替换字母。如果我现在有很多字母都是奇数个数,同样也有很多偶数个数字母,如果我把一个原本是奇数个数的字母中的一个换成了原本是偶数个数的字母,那整体的奇偶其实没变。
所以肯定是把一个奇数个数字母换成另一个同样是奇数个数的字母,这样就可以让两个字母的个数变成偶数个数。
就以odd记为奇数个数字母的数量,最多替换k次,那最多就可以消灭2k个奇数个数的字母,如果odd-2k<=1,就满足我们之前说的回文串的规律,那answe就是true
说了那么多,关键就是计算出子串奇数个数的字母数量呗,那么直接上代码
class Solution {
public:
vector<bool> canMakePaliQueries(string s, vector<vector<int>>& queries) {
vector<bool> answer;
for(int i-0;i<queries.size();i++){
int left=queries[i][0],right=queries[i][1],k=queries[i][2];
answer.push_back(res(s.substr(left,right-left+1),k));
}
}
private:
bool res(string s,int k){
map<char,int> mmp;
for(auto c:s){
map[c]++;//记录每个字母出现的个数
}
int odd=0;
for(auto p:mmp){
if(p.second!=0&&p.second%2==1)
odd++;//记录奇数个数字母的数量
}
if(odd==0||odd==1) return true;
if(odd-2*k>1) return false;
else return true;
}
};
代码不出意外的出了意外,最终超时
超时很能理解,因为每次都要算一回子串,很麻烦。
如果给出很多个left和right,那么很有可能代码要用到前缀和,这样就不用每次都算一回子串,那我们这里能不能用前缀和?
首先我们不需要真的算每个字母出现的次数,只需要动态地改变每个字母的奇偶性,字母每出现一次,奇偶性就更改,以0表示偶数次,1表示奇数次。可知异或1可以更改变量本身的值,比如0^1=1;1^1=0;只要让自己异或1就可以改变奇偶性;
用来记录前缀和的数组需要记录每个字母的奇偶性,所以可以整个二维数组,第一维度表示string中的位置,第二维度表示字母,sum[i][j]表示[0,i+1)中字母j的奇偶性
那么如何计算我们需要的odd呢,用sum[right+1][j]^sum[left][j],可以得到j字母的奇偶性。比如j在left=3的位置为1,即奇数次,j在right+1=9的地方为1,也为奇数次,那两者异或为0即偶数次,也表示区间里出现的次数是偶数次,因为奇数减奇数得到偶数,其他情况同理。
再把得到的数加到odd上,如果是奇数次即为1,表示又多了一个奇数个数的字母。
(注意题目里的区间是[left,right],对应sum的区间[left,right+1),而不是[left,right),在sum里是左闭右开的)
class Solution {
public:
vector<bool> canMakePaliQueries(string s, vector<vector<int>> &queries) {
int n = s.length(), q = queries.size();
vector<array<int, 26>> sum(n + 1);//sum[i][j]表示[0,i+1)中字母j的奇偶性
for (int i = 0; i < n; i++) {
sum[i + 1] = sum[i];
sum[i + 1][s[i] - 'a'] ^= 1; // 奇数变偶数,偶数变奇数
}
vector<bool> ans(q);
for (int i = 0; i < q; i++) {
auto &query = queries[i];
int left = query[0], right = query[1], k = query[2], odd = 0;
for (int j = 0; j < 26; j++)
odd+= sum[right + 1][j] ^ sum[left][j];
ans[i] = odd<=2*k+1;
}
return ans;
}
};
这个代码就已经可以通过了,但是还有更神奇的代码,我是不可能想出来,但是毕竟有解析,我可以理解一下思路
这回就先看代码了,直接copy的
class Solution {
public:
vector<bool> canMakePaliQueries(string s, vector<vector<int>>& queries) {
int n = s.size();
vector<int> count(n + 1);
for (int i = 0; i < n; i++) {
count[i + 1] = count[i] ^ (1 << (s[i] - 'a'));
}
vector<bool> res;
for (auto& query : queries) {
int l = query[0], r = query[1], k = query[2];
int odd = 0, x = count[r + 1] ^ count[l];
while (x > 0) {
x &= x - 1;
odd++;
}
res.push_back(odd <= k * 2 + 1);
}
return res;
}
};
总思想依然是前缀和,看到<<那肯定就是位运算了,这是怎么搞的?
由于长为 26的数组中只存储 0和 1,可以压缩到一个二进制数中,二进制数从低到高第 i 个比特存储着0和1 的信息。
例如二进制 10010 表示 b 和 e 出现奇数次,其余字母出现偶数次。
在计算前缀和时(准确地说是异或前缀和):
修改 a 出现次数的奇偶性,可以异或二进制 1;
修改 b 出现次数的奇偶性,可以异或二进制 10;
修改 c 出现次数的奇偶性,可以异或二进制 100;
依此类推。(都是本体异或上面的二进制)
此外,由于异或可以「并行计算」,对前缀和中的两个二进制数直接异或,便得到了子串中每种字母出现次数的奇偶性。再计算这个二进制数中的 1 的个数,便得到了 odd。
例如 10010⊕01110=11100,说明有 3 种字母出现奇数次。(比如10010是right+1的值,01110是left的值,那么他们异或就得到[left,right]区间里每个字母的奇偶性)
我们来解读代码
count代表前缀和的数组,数组中存的都是二进制数,用一个二进制数来表示所有字母的奇偶性,这样比上一个代码就节省了很多空间
所谓的1<<(s[i]-'a')就是得到要修改对应字母奇偶性的二进制数字,如果s[i]为b,s[i]-'a'得1,该式子得到10,就是之前讲的修改b奇偶性要用到二进制数字,再异或本身,奇偶性就进行了修改。
x = count[r + 1] ^ count[l];即使计算对应区间的二进制数,就像10010⊕01110=11100的例子那样
while 循环那里是计算二进制数字有多少个1,这我是真没搞懂,不过有讲解链接,想搞透的请移步leetcode:强者的链接
那么这道题就算讲完了,每段代码起码思想全都讲清楚了