算法周赛C题分析-动态规划子序列问题

题目:

Alice和BOB还会再相遇吗?

Alice作为BOB的忠实粉丝,酷爱BOBGOOD字符串,现在有一个字符串s,她想从字符串s中找到一个子序列字符串是以BOBGOOD为前缀,若干个(不能用为0个,否则BOB会生气)!(此!为英文状态下的感叹号)为后缀的字符串。形如BOBGOOD!!!。子序列字符串的定义为按照原来字符串s的顺序抽出0~s.size()个s的一个元素,可间隔性抽取形成的字符串。由于数量太多她实在找不过来,为了和BOB能再相遇!于是她请作为编程高手的你来解决这个问题,由于答案可能很大,请将答案对1e9+7取模后再输出。 

新手帮助之子序列举例:

例如对于字符串 s: BOBGOODB。G,GOB,BOBGB,BOBGOODB都可以作为s的子序列字符串。

输入描述:

共一行,一个字符串s(1<=|s|<200000),保证输入只含B.O.G.D和!共5种字符。

输出描述:

一个非负整数数字,代表最终答案。

 示例1

输入

BOBGOOD!!

输出

 3

 

         对于题目,我们的目的是求出在字符串s中为"BOBGOOD!"的子序列个数。其中题目要求以BOBGOOD为前缀,'!'为后缀,且'!'个数的区间为(0,+∞)。那么隐含的一个条件首先要清楚,只有抽取得到了一个子序列BOBGOOD时,在'D'后面的'!'才会生效,否则'!'无效,即无法组成"BOBGOOD!"子序列。

 思路分析:

        首先,我们需要求的是字符串s中BOBGOOD为前缀,'!'为后缀的子序列数。那么有效'!..'(注意这个!需要列出所有排列方式,这些所有排列方式就是我们需要求的)的子序列个数。

        这里举一个例子:比如"!...!"(总共x个),求它的为'!'的子序列个数,即为       

 

        其次,我们下一步可以取得"BOBGOOD"的序列数p。这一步可以使用动态规划来求子序列数。我尝试了其它很多方法,确实没这个合适。

        最后,ans=p*(2^x-1),即为合乎要求的所有子序列数。

        当然,实际的代码操作实际还会有很多细节需要注意。下面我先展示代码,再详细说明动态规划核心代码,以及需要注意的细节与坑点。

 代码实现:

import java.util.Scanner;
public class Main {
    static int mod = (int) 1e9 + 7;
    static int N=(int) 2e5+7;
    public static void main(String[] args) {
        Scanner sc= new Scanner(System.in);
        String s=sc.nextLine();

        int n = s.length();
        long[] num= new long[N];//记录!的数目
        //计算后缀!的数量
        for(int i = n-1; i >= 0; i--){
            num[i] = num[i + 1];
            if(s.charAt(i) == '!') num[i] ++;
        }
        long ans=0l;
        long[] B=new long[N];
        long[] BO=new long[N];
        long[] BOB=new long[N];
        long[] BOBG=new long[N];
        long[] BOBGO=new long[N];
        long[] BOBGOO=new long[N];
        long[] BOBGOOD=new long[N];
        for (int i = 0; i < n; i++) {
            //动态更新
            if(i!=0) {
                B[i] = B[i - 1];
                BO[i] = BO[i - 1];
                BOB[i] = BOB[i - 1];
                BOBG[i] = BOBG[i - 1];
                BOBGO[i] = BOBGO[i - 1];
                BOBGOO[i] = BOBGOO[i - 1];
                BOBGOOD[i] = BOBGOOD[i - 1];
            }
            if(s.charAt(i)=='B'){
                B[i]++;
                B[i]%=mod;
                BOB[i]+=BO[i];
                BOB[i]%=mod;
            }
            if(s.charAt(i)=='O'){
                BO[i]+=B[i];
                BO[i]%=mod;

                BOBGOO[i]+=BOBGO[i];
                BOBGOO[i]%=mod;
                //先添加BOBGOO序列数
                BOBGO[i]+=BOBG[i];
                BOBGO[i]%=mod;
            }
            if(s.charAt(i)=='G'){
                BOBG[i]+=BOB[i];
                BOBG[i]%=mod;
            }
            if(s.charAt(i)=='D'){
                BOBGOOD[i]+=BOBGOO[i];
                BOBGOOD[i]%=mod;
                if(BOBGOOD[i]!=0) {
                    ans =(ans+(BOBGOO[i] *(lsm(2, num[i]) - 1))%mod)%mod;
                }
            }
        }
        System.out.println((int)ans);
    }
    public static long lsm(long a,long b){
        long ans=1;
        while(b>0){
            if(b%2==1) ans=ans*a%mod;
            a=a*a%mod;
            b/=2;
        }
        return ans;
    }
}

 代码分析:

为什么要用动态规划?

        首先,对于求子序列数的问题,最优解就是利用动态规划。同时这里会解释一些,关于此题中运用动态规划的好处。    

        因为'!'的存在分为“有效”和“无效”两种情况。例如字符串s为"BOB!GOOD!!"其中第一个'!'就为无效叹号,无法成为"BOBGOOD"的后缀叹号。

        所以,我们不能直接遍历计数'!'的数量得到x,带入2^x-1的公式求出组合结果。

        因此,我们就用一个巧妙的方法来确定'!'的数量以及所在字符串索引的位置,来解决无效叹号的问题。

        

for(int i = n-1; i >= 0; i--){
            num[i] = num[i + 1];
            if(s.charAt(i) == '!') num[i] ++;
        }

        我们定义一个整型数组num来对不同位置的叹号进行计数。首先需要注意的是,我们需要逆序计数,因为在最后一个子序列"BOBGOOD"存在的情况下的其后面'!'一定是有效的,且当选取到一个"BOBGOOD"子序列的时候,在选取'!'的时候只能选取'D'之后的'!'。

        这里是将'!'与"BOBGOOD"子序列分开来讲,为什么要用到动态规划?用动态规划确实很容易解决”有效与无效“的问题。除了它们之间,'B'与'BO','BO'与'BOB'等这些子序列也会有这些问题。同时动态规划确实很好地适应解决选取子序列这种问题,但作者理解还不够深刻,难以表达,如果有大佬对动态规划解决选取子序列问题有更深层次的理解,还希望可以在评论区指导一二。

       !和BOBGOOD虽然在两个for循环,但是他们共用了索引i,其实相当于他们两个在整体上属于”同一个动态规划“。

 为什么要注意子序列叠加顺序?

if(s.charAt(i)=='O'){
                BO[i]+=B[i];
                BO[i]%=mod;

                BOBGOO[i]+=BOBGO[i];
                BOBGOO[i]%=mod;
                //先添加BOBGOO序列数
                BOBGO[i]+=BOBG[i];
                BOBGO[i]%=mod;
            }

比如这里,为什么要先添加BOBGOO的序列数而不是BOBGO呢?

当指针移动到字符'O'时,如果指针指的是GOOD中的第一个O,那么BOBGO应该+BOBG,此时如果后添加BOBGOO的话,这一个O便重复添加了两次。结果很明显是不对的。

利用快速幂算法求幂函数

public static long lsm(long a,long b){
        long ans=1;
        while(b>0){
            if(b%2==1) ans=ans*a%mod;
            a=a*a%mod;
            b/=2;
        }
        return ans;
    }

        如果直接使用pow()函数,结果会溢出,导致题目AC不了。所以我们使用快速幂的算法,便于取模操作,避免数据溢出,同时也大大降低了算法时间复杂度。(从O(b)到O(logb))。

        至于该算法的原理就是对幂b不断二分,a平方取模,如果b取余2为1,则给ans分出一个a(即b-1),将其变为偶数,继续二分。

        详细可参考这篇文章:https://blog.csdn.net/m0_51507437/article/details/131421361?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522169620972016800180635449%2522%252C%2522scm%2522%253A%252220140713.130102334..%2522%257D&request_id=169620972016800180635449&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~all~sobaiduend~default-1-131421361-null-null.142^v94^insert_down28v1&utm_term=%E5%BF%AB%E9%80%9F%E5%B9%82%E8%AE%A1%E7%AE%97&spm=1018.2226.3001.4187

 

  • 3
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值