题目:
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),将其变为偶数,继续二分。