1653. 使字符串平衡的最少删除次数
题意描述得有点复杂,其实浓缩成一句话就是,求一个最小的删除次数,使得字符串由连续的
a
a
a 和
b
b
b 组成,且如果有
a
a
a ,所有
a
a
a 必须在左侧。
观察两个示例不难发现,想要得到符合要求的字符串,只需要在某个位置,删掉所有左边的
b
b
b 和右边的
a
a
a ,此时需要的操作次数就是这两者数量之和。那怎么得到最小值呢,当然是遍历数组的每一个位置,维护一个最小的
l
e
f
t
_
b
+
r
i
g
h
t
_
a
left\_b+right\_a
left_b+right_a。
那怎么才能知道一个位置的
l
e
f
t
_
b
left\_b
left_b 和
r
i
g
h
t
_
a
right\_a
right_a ,当然不是每次都往前往后统计。这里介绍一下“前缀和”和“后缀和”两个概念。
前缀和多用数组来记录,每个位置记录的是目前为止,前一部分中某种数据的数量。举个例子,对于
“
a
a
b
a
b
b
a
b
”
“aababbab”
“aababbab” :
l
e
f
t
_
a
=
[
1
,
2
,
2
,
3
,
3
,
3
,
4
,
4
]
left\_a=[1,2,2,3,3,3,4,4]
left_a=[1,2,2,3,3,3,4,4] 计算方式也很简单,有动态规划那个味道。
l
e
f
t
_
a
[
i
]
=
{
l
e
f
t
_
a
[
i
−
1
]
s
[
i
]
=
b
l
e
f
t
_
a
[
i
−
1
]
+
1
s
[
i
]
=
a
left\_a[i]=\begin{cases}left\_a[i-1] & s[i] = b\\left\_a[i-1]+1 & s[i] = a\end{cases}
left_a[i]={left_a[i−1]left_a[i−1]+1s[i]=bs[i]=a 后缀和同理,我们只需要倒着遍历一遍就可以得到。加上最后遍历一遍找最小删除次数,只需要循环三次。
然后有一个需要注意的点是,长度为
n
n
n 的字符串,有
n
+
1
n+1
n+1 个切割点,所以计算前后缀和的遍历范围为
[
0
,
s
.
l
e
n
g
t
h
(
)
]
[0,s.length()]
[0,s.length()] 。
class Solution {
public:
int minimumDeletions(string s) {
int pre_b[100005];
int beh_a[100005];
int res=INT_MAX;
// cout<<res<<endl;
if(s.length()==1) return 0;
memset(pre_b,0,sizeof(pre_b));
memset(beh_a,0,sizeof(beh_a));
for(int i=0;i<=s.length()-1;i++){
pre_b[i+1]=pre_b[i];
if(s[i]=='b') pre_b[i+1]++;
}
for(int i=s.length();i>0;i--){
beh_a[i-1]=beh_a[i];
if(s[i]=='a') beh_a[i-1]++;
}
for(int i=0;i<=s.length();i++){
// cout<<pre_b[i]<<" "<<beh_a[i]<<endl;
res=min(res,pre_b[i]+beh_a[i]);
}
return res;
}
};
官解做法和这个一致,但是没有开数组,而是先统计了 a a a 的总数量,然后第二次遍历的时候,统计左边的 b b b 的数量,通过总量递减得到右边的 a a a 的数量,同时维护最小次数,只需要两次循环且只使用了常量。
class Solution {
public:
int minimumDeletions(string s) {
int left_b=0,right_a=0;
for(auto c:s){
if(c=='a')right_a++;
}
int res=right_a;
for(auto c:s){
if(c=='a'){
right_a--;
}
else{
left_b++;
}
res=min(res,left_b+right_a);
}
return res;
}
};
But,灵神总有你意想不到的解法。动态规划一次遍历解决问题。
我们定义 c n t _ b cnt\_b cnt_b 是字符串中 b b b 的个数, d p [ i ] dp[i] dp[i] 为子问题的解。那么对于每一个子问题,考虑 s s s 的最后一个字母:
- 如果是 b b b ,则无需删除, d p [ i ] = d p [ i − 1 ] dp[i]=dp[i-1] dp[i]=dp[i−1]
- 如果是 a a a ,我们要么删掉这个 a a a ,要么删掉前面的所有 b b b, d p [ i ] = m i n ( d p [ i − 1 ] + 1 , c n t _ b ) dp[i]=min(dp[i-1]+1, cnt\_b) dp[i]=min(dp[i−1]+1,cnt_b)
由于 d p [ i ] dp[i] dp[i] 不需要 d p [ i − 1 ] dp[i-1] dp[i−1] 以外的子问题的解参与计算,因此这个数组还能优化成常量。
class Solution {
public:
int minimumDeletions(string s) {
int cnt_b=0,res=0;
for(auto c:s){
if(c=='a'){
res=min(res+1,cnt_b);
}
else cnt_b++;
}
return res;
}
};
优化还是非常明显的!