力扣:动态规划之不同的子序列

1,944 阅读6分钟

前言

又到了我们的动态规划时间,今天让你们再次感受一下动态规划的恐怖之处。我也是刚刚从里面走出来,就立马迫不及待地和你们分享,我可不想让自己白白掉这么多头发,应该让各位一起来掉掉头发,才能维持我内心的平衡。话不多说,上题!

不对,上题之前还是得看看咱们的动态规划四部曲

动态四部曲

1. 确定dp的状态 : 定义为一维二维还是三维?表示什么含义?

2. 确定状态转移方程

3. 初始化,也就是dp数组可以取得的元素的值

4. 自底向上的方式计算dp,并得出最优值(遍历的顺序)

不同的子序列

题目

给定一个字符串 s 和一个字符串 t ,计算在 s 的子序列中 t 出现的个数。

字符串的一个 子序列 是指,通过删除一些(也可以不删除)字符且不干扰剩余字符相对位置所组成的新字符串。(例如,"ACE" 是 "ABCDE" 的一个子序列,而 "AEC" 不是)

题目数据保证答案符合 32 位带符号整数范围。

示例 :

输入:s = "rabbbit", t = "rabbit"
输出:3
解释:
如下图所示, 有 3 种可以从 s 中得到 "rabbit" 的方案。
rabbbit
rabbbit
rabbbit

思路分析

看完题目是不是感觉自己有一种泉思如涌的错觉?感觉我好像会做,但是思绪万千又不知道从哪里写起的那种慌乱感?是不是感觉经过自己缜密的计算和绝妙的分析,想到了通过比对两个字符串的不同之处,然后进行所谓的变换,就能得出正确答案?

src=http---5b0988e595225.cdn.sohucs.com-images-20181024-9a1a2e1015154ee4a1321ecdc94c3727.jpeg&refer=http---5b0988e595225.cdn.sohucs.jpg

唉,这都是你的错觉!!!

小编我一开始就是这种想法,以为不用动态规划也能做,但是最后还是作罢,不知道哪个大佬能这么做,发出算法来让我好好学习一下,反正我做不出来的,后面没办法,还是采用了动态规划。

确定dp数组

看到字符串问题,我们就需要想到,dp数组定义为一维数组一般都是解决不了的,就算解决的了,那也是比较复杂或者很难理解,这种代码大佬就写的出。我就只能默默地用最low的做法,将dp定义为二维数组dp[i][j]。然后我们开始聚焦在问题的求解上,这种dp数组的含义直接或间接的和我们的问题求解对象存在某种联系,不然怎么遍历然后得出结果呢,对吧?用脚趾头都能想到的事情,于是我们疯狂的把i``j含义往题目上靠。

于是我们慢慢总结,应该是字符串s中前 i 位包含了多少种字符串tj 位的个数的结果,最后我采用了力扣大佬代码随想录的结论, 以 i-1 为结尾的 s 子序列中出现以 j-1 为结尾的 t 的个数为dp[i][j]。因为这个确实比较香,不会让你觉得绕,更方便计算.

确定状态转移方程

接下来我们就得走入核心了

该怎么得到状态方程呢?我们就要看看dp[i][j]是怎么推出来的了。这个时候我们需要注意到具体细节了,根据以往的经验,字母配对情况一般都是需要考虑i与j所指着的字符相等和不相等的时候。这种经验都是以往做题体验出来的,所以说,刷题也是有一定的重要性的。

这个时候我们依据比较简单的例子来分析,当然也需要包含多种情况,也不能是特例

比如s = rabb 以及t = rab 我们就从这个整体开始,我们想象一下,在rabb找到多少个rab我们真的需要做这种所谓的删除或者增添操作吗?很显然不用,只需要统计种数即可,所以我们开始推导这两种情况,

  • s[i - 1] 与 t[j - 1]相等
  • s[i - 1] 与 t[j - 1] 不相等

当s[i - 1] 与 t[j - 1]相等时,dp[i][j]可以有两部分组成。比如当前的 st,此时这个时候我们需要找rab中包含几个ra?由于最后一位相等,那么我们就需要考虑是否用上最后一位,如果用s[i - 1]来匹配了,也就是说最后一位相等,那么我们只需要找出 rab 当中有多少个 ra 即可,因为最后一位字母 b 已经共有了,那么此时个数为dp[i - 1][j - 1]; 如果是不用s[i - 1]来匹配,也就是说我们需要看看rab当中包含多少个rab,这是在之前的重复项,那么此时的个数为dp[i - 1][j]

所以当s[i - 1] 与 t[j - 1]相等时,dp[i][j] = dp[i - 1][j - 1] + dp[i - 1][j];

当s[i - 1] 与 t[j - 1]不相等时dp[i][j]只有一部分组成,不用s[i - 1]来匹配,也就是说最后一位不相同,只需要看看t前面的字符串是否包含t,继承前面的状态即可,即:dp[i - 1][j]

所以递推公式为:dp[i][j] = dp[i - 1][j];

初始化

初始化的时候我们需要回顾一下dp数组的定义: 以i-1为结尾的s子序列中包含以j-1为结尾的t的个数为dp[i][j]

那么怎么初始化呢?找找特殊情况,dp[0][0]、dp[0][j]、dp[i][0]、,dp[0][0]表示s前0包含j前0位的个数,想想是多少,这不就是字母a里面包含多少个a吗?这是一样的思想,所以应该为 1. 而dp[0][j]表示s前0位包含t前j位的个数,显然就是,-0, dp[i][0] 表示s前i位包含t前0位的个数,这个思想也是这样,全部删除不就变成t了吗?所以还是 1

遍历顺序

在确定遍历顺序的时候,我们要看递推公式是怎么样的,dp[i][j] = dp[i - 1][j] , dp[i][j] = dp[i - 1][j - 1] + dp[i - 1][j],这些环节层层相扣,相互联系,所以说,正序逃不了了。

具体代码

class Solution {
public:
    int numDistinct(string s, string t) {
        vector<vector<uint64_t>> dp(s.size() + 1, vector<uint64_t>(t.size() + 1));
        for (int i = 0; i < s.size(); i++) dp[i][0] = 1;
        for (int j = 1; j < t.size(); j++) dp[0][j] = 0;
        for (int i = 1; i <= s.size(); i++) {
            for (int j = 1; j <= t.size(); j++) {
                if (s[i - 1] == t[j - 1]) {
                    dp[i][j] = dp[i - 1][j - 1] + dp[i - 1][j];
                } else {
                    dp[i][j] = dp[i - 1][j];
                }
            }
        }
        return dp[s.size()][t.size()];
    }
};

代码分析

首先对数组初始化,之后两次正序遍历,而i-1的用处也在于此,可以让数组从下标1处开始,就能代表0,当然个人习惯,只是便于分析,防止绕晕,最后遍历完成,返回dp[i][j]便可得出结论

总结

其实动态规划难,也是难在他的dp数组以及dp状态方程的确定,对于这些动态规划的解题,其实也有套路。我们还是需要好好学习!

我是小白,我们一起学习!