【LeetCode每日一题】【动态规划】2022-10-14 940. 不同的子序列 II Java实现


题目

在这里插入图片描述

官方思路(动态规划)

f [ i ] f[i] f[i]表示 s [ i ] s[i] s[i]为最后一个字符的子序列的数目

情况一

如果子序列中只有 s [ i ] s[i] s[i]一个字符, f [ i ] = 1 f[i]=1 f[i]=1

情况二

如果子序列中至少有两个字符,那么可以枚举倒数第二个字符来进行状态转移。可以想到的是:倒数第二个字符可以选择 s [ 0 ] , s [ 1 ] , . . . , s [ i − 1 ] s[0],s[1],...,s[i-1] s[0],s[1],...,s[i1]中的某一个,这样就可以得到如下的状态转移方程:

f [ i ] = f [ 0 ] + f [ 1 ] + . . . + f [ i − 1 ] f[i]=f[0]+f[1]+...+f[i-1] f[i]=f[0]+f[1]+...+f[i1]

然而,这样做会产生重复的计数。

如果 s [ j 0 ] s[j_0] s[j0] s [ j 1 ] s[j_1] s[j1]这两个字符不相同,那么 f [ j 0 ] f[j_0] f[j0] f [ j 1 ] f[j_1] f[j1]对应的子序列是两个不相交的集合。

【我的理解】因为 s [ j 0 ] s[j_0] s[j0] s [ j 1 ] s[j_1] s[j1]这两个字符不相同,所以以 s [ j 0 ] s[j_0] s[j0] s [ j 1 ] s[j_1] s[j1]这两个字符结尾的所有子序列也都不可能相同

但是,如果 s [ j 0 ] s[j_0] s[j0] s [ j 1 ] s[j_1] s[j1]这两个字符相同,那么 f [ j 0 ] f[j_0] f[j0] f [ j 1 ] f[j_1] f[j1]对应的子序列会包含重复的项。最简单的一个重复项就是只包含一个字符的子序列 s [ j 0 ] s[j_0] s[j0]或者 s [ j 1 ] s[j_1] s[j1]本身

【我的理解】因为 s [ j 0 ] s[j_0] s[j0] s [ j 1 ] s[j_1] s[j1]这两个字符相同,假设 j 0 < j 1 j_0 < j_1 j0<j1,那么以 s [ j 1 ] s[j_1] s[j1]结尾的所有可能的子序列一定包含了以 s [ j 0 ] s[j_0] s[j0]结尾的所有可能的子序列

如何防止重复计数?

可以发现,如果 j 0 < j 1 j_0 < j_1 j0<j1,那么 f [ j 0 ] f[j_0] f[j0]一定是 f [ j 1 ] f[j_1] f[j1]的真子集,这是因为:

每一个以 s [ j 0 ] s[j_0] s[j0]为最后一个字符的子序列,都可以把这个字符改成完全相同的 s [ j 1 ] s[j_1] s[j1],计入到 f [ j 1 ] f[j_1] f[j1]中。

【我的理解】就是把 f [ j 0 ] f[j_0] f[j0]的值作为 f [ j 1 ] f[j_1] f[j1]的一部分,加到 f [ j 1 ] f[j_1] f[j1]

因此,对于每一种字符,我们只需要找到其最后一次出现的位置(并且在位置i之前),并将其对应的 f f f值累加进 f [ i ] 即可 f[i]即可 f[i]即可

由于本题中字符串只包含小写字母,我们可以用 l a s t [ k ] last[k] last[k]记录第 k ( 0 ≤ k < 26 ) k(0 \leq k < 26) k(0k<26)个小写字母最后一次出现的位置。如果它还没出现过,那么 l a s t [ k ] = − 1 last[k]=-1 last[k]=1。这样正确的状态转移方程是:

f [ i ] = ∑ 0 ≤ k < 26 , l a s t [ k ] ≠ − 1 f [ l a s t [ k ] ] f[i] = \sum_{0 \leq k <26,last[k] \neq -1} f[last[k]] f[i]=0k<26,last[k]=1f[last[k]]

将两种情况合并在一起

最终的状态转移方程为:

f [ i ] = 1 + ∑ 0 ≤ k < 26 , l a s t [ k ] ≠ − 1 f [ l a s t [ k ] ] f[i] = 1 + \sum_{0 \leq k <26,last[k] \neq -1} f[last[k]] f[i]=1+0k<26,last[k]=1f[last[k]]

再计算完f[i]后,需要记得更新对应的last项。最终答案即为:

∑ 0 ≤ k < 26 , l a s t [ k ] ≠ − 1 f [ l a s t [ k ] ] \sum_{0 \leq k <26,last[k] \neq -1} f[last[k]] 0k<26,last[k]=1f[last[k]]

Java代码

Java中int的取值范围是 [ − 2 31 , 2 31 − 1 ] [-2^{31},2^{31}-1] [231,2311]

所以,用int定义要取余的数是肯定可以的

import java.util.Arrays;

class Solution {
    public int distinctSubseqII(String s) {
        final int MOD = 1000000007;

        //last数组初始化
        int[] last = new int[26];
        Arrays.fill(last, -1);

        //获取字符串的长度
        int n = s.length();

        //f[i]表示以s[i]为最后一个字符的子序列的数目
        int[] f = new int[n];

        //如果每个子序列,只有一个字符,那么只有一种方案,所以将f的每个元素初始化为1
        Arrays.fill(f, 1);

        for (int i = 0; i < n; i++) {
            for (int j = 0; j < 26; j++) {
                //这里不等于-1的last[j],j对应的26个字母,一定是出现在s[i]之前的
                if (last[j] != -1) {
                    f[i] = (f[i] + f[last[j]]) % MOD;
                }
            }
            last[s.charAt(i) - 'a'] = i;
        }

        int ans = 0;
        for (int i = 0; i < 26; i++) {
            if (last[i] != -1) {
                ans = (ans + f[last[i]]) % MOD;
            }
        }

        return ans;
    }
}

举例说明

具体实例结合代码结合理论,这样理解的更快

输入:“abc”

last数组的长度是26,初始化每个元素为-1

f数组,f[i]表示以s[i]为最后一个字符的子序列数目

进入两层for循环:

  • i = 0,这时内层for循环j从0到25,所有的last[j]都是-1,所以不会进入if条件判断,last[0] = 0,表示字母a(last数组中对应的下标0)的最后出现的位置在0

以s[0]结尾的子序列所有可能的情况:

  • a

以s[0]结尾的子序列的数目是1

  • i=1,这时内层for循环j从0到25,除了last[0]=0之外,其余的都是-1,说明s[1]之前的字符种类只有一个(是字符种类,而不是字符个数),所以,以s[1]结尾的子序列的数目f[1]需要更新,除了原有的只有s[1]一个字符这一种情况以外,还需要加上以s[0]作为字符串结尾的子序列的数目,f[1]=f[1]+f[last[0]],其中last[0]=0,所以,f[1]=f[1]+f[0]=1+1=2,最后更新last[1]=1,表示字母b最后出现的位置在1

以s[1]结尾的子序列所有可能的情况:

  • b —> 对应着刚初始化以后的f[1]=1
  • ab —> 对应着f[1] = f[last[0]] + f[1],其中last[0]=0,也就是f[1] = f[0] + f[1] = 1 + 1 = 2

以s[1]结尾的子序列的数目是2

  • i=2,这时内存for循环j从0到25,除了last[0]=0,last[1]=1之外,其余都是-1,说明,s[2]之前的字符种类只有两个(是字符种类,而不是字符个数),所以,以s[2]结尾的子序列的数目f[2]需要更新,除了原有的s[2]一个字符这一种情况以外,还需要加上以s[1]和s[0]作为字符串结尾的子序列的数目,f[2]=f[2]+f[last[0]]+f[last[1]],其中last[0]=0,last[1]=1,所以,f[2]=f[2]+f[0]+f[1]=1+1+2=4,最后更新last[2]=2,表示字母c最后出现在位置2

以s[2]结尾的子序列所有可能的情况

  • c —> 对应着刚初始化以后的f[2]=1
  • ac —> 对应着以s[0]结尾的子序列数目f[last[0]]=f[0]
  • bc、abc —> 对应着以s[1]结尾的子序列数目f[last[1]]=f[1]

以s[2]结尾的子序列数目为f[2]=f[2]+f[0]+f[1]=1+1+2=4

输入:“aba”

  • i=0,i=1的过程和上面一个输入的情况是一样的,这里不在赘述

  • i=2,f[2]也是按照之前的思路进行更新,f[2] = f[2] + f[last[0]] + f[last[1]] = f[2] + f[0] + f[1] = 1 + 1 + 2 = 4,最后更新last[0]=2【这时和上一个输入不一样的地方】,表示字母a最后出现在位置2

以s[2]结尾的子序列所有可能的情况

  • a —> 对应着刚初始化以后的f[2]=1
  • aa —> 对应着以s[0]结尾的子序列数目f[last[0]]=f[0]
  • ba、aba —> 对应着以s[1]结尾的子序列数目f[last[1]]=f[1]

以s[2]结尾的子序列数目为f[2]=f[2]+f[0]+f[1]=1+1+2=4

因为有两个相同的a,last[0]由原来的等于0,现在修改为等于2,这样至少在我们能想到的以a结尾的单个字符只会被计算一遍,所以在最后计算所有可能的子串的数量时,只会加一次字母a在f数组中对应的值

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值