题目列表
3083. 字符串及其反转中是否存在同一子字符串
3084. 统计以给定字符开头和结尾的子字符串总数
3085. 成为 K 特殊字符串需要删除的最少字符数
3086. 拾起 K 个 1 需要的最少行动次数
一、字符串及其反转中是否存在同一子字符串
直接暴力枚举即可,代码如下
class Solution {
public:
bool isSubstringPresent(string s) {
bool vis[26][26]={0};// vis[x][y] 表示(s[i-1],s[i])是否出现过
for(int i=1;i<s.size();i++){
int x = s[i-1]-'a',y = s[i]-'a';
vis[x][y]=true;
if(vis[y][x])
return true;
}
return false;
}
};
// 用位运算优化
class Solution {
public:
bool isSubstringPresent(string s) {
int vis[26]={0};
for(int i=1;i<s.size();i++){
int x = s[i-1]-'a',y = s[i]-'a';
vis[x]|=(1<<y);
if(vis[y]>>x & 1)
return true;
}
return false;
}
};
二、统计以给定字符开头和结尾的子字符串的总数
这题就是纯数学题,就是要求我们找出特定字符的个数,然后从中选出两个组成子字符串,这题单一字符也符合条件,所以答案为n*(n-1)/2+n,n表示字符串中特定字符的出现次数
代码如下
class Solution {
public:
long long countSubstrings(string s, char c) {
long long n = count(s.begin(),s.end(),c);
return n*(n-1)/2+n;
}
};
三、成为K特殊字符串需要删除的最少字符数
仔细读完题目,你会发现这题其实和字符没多大关系,关键是频率,思路如下:
首先我们用freq[26]统计各个字符出现的次数(即频率),然后对freq进行排序,一共就26个字母的频率,我们可以暴力枚举以freq[i]为左端点,freq[i]+k为右端点的频率区间,在该区间内的字符不需要被修改,出现次数在该区间左边的字符要全被删除,出现次数在该区间右边的字符要被减少到freq[i]+k,枚举26次就能得到答案。
如何快速找到freq[i]+k对应的freq数组下标?用二分
如何快速得到前i个字符的出现次数?用前缀和
当然这题的freq数组不是很大,也就26个数,我们也可以直接遍历数组,不用二分+前缀和
代码如下
class Solution {
public:
int minimumDeletions(string word, int k) {
//统计频率
int freq[26]={0};
for(const auto&e:word)
freq[e-'a']++;
sort(freq,freq+26);
int pre[27]={0};
for(int i=0;i<26;i++) pre[i+1]=pre[i]+freq[i];
int ans = INT_MAX;
for(int i=0;i<26;i++){
if(freq[i]==0) continue;
int target=freq[i]+k;
int l=i,r=25;
while(l<=r){
int m=l+(r-l)/2;
if(freq[m]>target) r=m-1;
else l=m+1;
}
ans=min(ans,pre[i]+pre[26]-pre[l]-target*(26-l));
}
return ans;
}
};
class Solution {
public:
int minimumDeletions(string word, int k) {
int freq[26]={0};
for(const auto&e:word)
freq[e-'a']++;
sort(freq,freq+26);
int ans = 0;
for(int i=0;i<26;i++){
if(freq[i]==0) continue;
int res = 0;
for(int j=i;j<26;j++)
res += min(freq[j],freq[i]+k);
ans=max(ans,res);// 求能保持不变的最大字符数量
}
return word.size()-ans;
}
};
四、拾起K个1需要的最少的行动次数
设 i 为Alice的站立位置的下标,j 为 1 所在位置下标
根据贪心,我们可以归纳出以下几个步骤(按照优先级排序):
a、拿 i 左右两边的1(如果 i 左右是1的话)--- 只需行动 | i - j | = 1次
b、执行操作1将maxChanges个1放在 i 的左边/右边,再执行操作2,拿到1 --- 只需行动2次
c、执行操作2将被 i 附近的1拿到 --- 需要行动 | i - j | >= 2
(如果 i 本身就是1,那么不需要操作就直接拿到一个1,该行动在代码中与步骤a和并了,可以暂不考虑)
如果需要拿出的K个1可以在前两个步骤之内完成,可以直接计算出答案(具体看代码)
否则,我们在前两步得到的x个1的基础上,再得到 K - x 个1即可,很多人都会有这样的思维惯性,但是这样是不正确的,因为步骤a和步骤c是密切相关的,我们在得到x个1时,就已经确定了Alice的位置,但是这个位置不一定是最优的,因为它还会影响步骤c,所以我们应该把步骤a和步骤c放在一起考虑,步骤b的操作次数单独计算。
如何计算步骤a和步骤c得到的 K - maxChanges 个1需要的最少操作次数?
其实观察它们的表达是| i - j | 我们就能知道,我们要求的是 K - maxChanges 个1 到达某个位置的最短的距离和,用中位数贪心【中位数贪心的证明如下】
代码如下
class Solution {
public:
long long minimumMoves(vector<int>& nums, int k, int maxChanges) {
int n = nums.size();
vector<int>pos;
// 求连续1的个数
int mx = 0;
for(int i = 0; i < n; i++){
if(nums[i]==0) continue;
pos.push_back(i);//记录1出现的位置
int j=i++;
while(i<n&&nums[i]){
pos.push_back(i);//记录1出现的位置
i++;
}
mx=max(mx,i-j);
}
mx = min(3,min(k,mx));
if(mx+maxChanges>=k)
return max(mx-1,0)+(k-mx)*2LL;// 只用步骤a/步骤a+步骤b的操作次数
int size = k - maxChanges; // 步骤a+步骤c需要的1的个数
int m = pos.size();
long long pre[m+1]; pre[0] = 0;
for(int i = 0; i < m; i++) pre[i+1] = pre[i] + pos[i];
long long ans = LLONG_MAX;
for(int i = 0; i + size-1 < m; i++){
int l = i, r = i + size - 1;
int m = l + (r-l)/2;// pos[m]是中位数
long long left = 1LL*(m-l)*pos[m]-(pre[m]-pre[l]);
long long right = pre[r+1]-pre[m+1]-1LL*(r-m)*pos[m];
ans = min(ans, left+right);
}
return ans+2LL*maxChanges;
}
};