【LeetCode 每日一题】1653. 使字符串平衡的最少删除次数(medium)

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=[12233344]  计算方式也很简单,有动态规划那个味道。
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[i1]left_a[i1]+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()] [0s.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[i1]
  • 如果是 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[i1]+1,cnt_b)

由于 d p [ i ] dp[i] dp[i] 不需要 d p [ i − 1 ] dp[i-1] dp[i1] 以外的子问题的解参与计算,因此这个数组还能优化成常量。

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;
    }
};

在这里插入图片描述

优化还是非常明显的!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值