每日算法系列【LeetCode 115】不同的子序列

题目描述

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

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

示例1

输入:
S = "rabbbit", T = "rabbit"
输出:
3
解释:
如下图所示, 有 3 种可以从 S 中得到 "rabbit" 的方案。
(上箭头符号 ^ 表示选取的字母)

rabbbit
^^^^ ^^
rabbbit
^^ ^^^^
rabbbit
^^^ ^^^

示例2

输入:
S = "babgbag", T = "bag"
输出:
5
解释:
如下图所示, 有 5 种可以从 S 中得到 "bag" 的方案。 
(上箭头符号 ^ 表示选取的字母)

babgbag
^^ ^
babgbag
^^    ^
babgbag
^    ^^
babgbag
  ^  ^^
babgbag
    ^^^

题解

dfs+记忆化搜索

这题要求字符串 s 中有多少子序列正好等于字符串 t ,那么我们不如从最后一个字符看起,假设 s 和 t 的长度分别为 n 和 m 。

如果 s[n-1] 和 t[m-1] 不相等,那么显然只能在 s[0] 到 s[n-2] 之中寻找 t 。

如果 s[n-1] 和 t[m-1] 相等,那么有两种选择。一种是这两个字符对应上,然后在 s[0] 到 s[n-2] 之中寻找 t[0] 到 t[m-2] 。另一种是不用 s[n-1] ,还是和上一种情况一样,在 s[0] 到 s[n-2] 之中寻找 t 。两种选择的方案数加起来就是答案了。

那么递归终止条件是什么呢?如果发现 s 已经空了,但是 t 还有字符没有对应上,那么方案数一定为 0 。如果 t 空了,那么不管 s 还剩多少字符,都说明 t 已经找到对应的子序列了,方案数加 1 。

为了防止重复计算,还要加上记忆化搜索,用数组记录一下每个状态的方案数。状态 (i, j) 就表示在 s[0] 到 s[i] 中寻找 t[0] 到 t[j] 的方案数。

动态规划

当然一般上面那种 dfs 都可以转化成动态规划的写法。

这里动态规划就是从长度比较短的字符串开始求解。初始状态是 dp[i][0] ,表示 s[0] 到 s[i] 之中有多少个 t[0] ,这很容易求出来。

然后两层循环遍历两个的字符串的结尾,跟上面 dfs 方法一样,如果 s[i] 和 t[j] 不相等,那么 dp[i][j] = dp[i-1][j] ;否则的话再加上一个 dp[i-1][j-1] 就行了。

动态规划+空间优化

上面动态规划方法有个问题就是字符串如果太长的话,空间会吃不消。而仔细观察就会发现,当循环到 i ,然后遍历 j 的时候,其实只会用到 i-1 的状态。那么我们就可以去掉第一维,只保存 j 的所有状态就行了。

但是有个注意的点是,第二层循环 j 的顺序要变一下,要从大往小遍历。因为 j 需要用到 (i-1, j-1) 时刻的状态值,如果你从小到大遍历,那么 (i, j-1) 的方案数就会把 (i-1, j-1) 的方案数覆盖掉,之后你获取到的就不是 i-1 时刻的方案数了。

另一个小区别是 dp[i][0] 的计算被移到了第一层循环的最后,而不是初始化就计算好了。这是因为第一维 i 被去掉了,所以只能在用到的时候再更新计算。

动态规划+空间优化+时间优化

其实上面一个方法速度已经不错了,但是时间上还是有优化的余地的。

可以发现,只有当 s[i] = t[j] 的时候,才需要更新 (i, j) 的方案数。所以我们只需要提前预处理出来每个字母 s[i] 在 字符串 t 中的哪些位置出现过就行了。然后遍历的时候只需要直接取出这些位置来更新就行了。

而实际运行中也会发现, dfs 要比动态规划快很多!这是因为 dfs 只会遍历合法的那些状态,而动态规划会把所有状态都计算出来,不管对最后的答案有没有帮助。

举个例子,s = "abcbbbb" , t = "abc" ,因为 t 只在 s 的前三个字母中出现了,所以如果我们寻找 t 的子串 "ab" 在 s 中出现次数的时候,从第二个 b 开始都是没有任何意义的,因为后面都没有 c 给你匹配了。

代码

dfs+记忆化搜索(c++)

class Solution {
public:
    int numDistinct(string s, string t) {
        int n = s.size(), m = t.size();
        if (n < m) return 0;
        vector<vector<int> > dp(n, vector<int>(m, -1));
        return dfs(n-1, m-1, s, t, dp);
    }

    int dfs(int i, int j, string& s, string& t, vector<vector<int> >& dp) {
        if (j < 0) return 1;
        if (i < 0) return 0;
        if (dp[i][j] >= 0) return dp[i][j];
        int res = dfs(i-1, j, s, t, dp);
        if (s[i] == t[j]) res += dfs(i-1, j-1, s, t, dp);
        return dp[i][j]=res;
    }
};

动态规划(c++)

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

动态规划+空间优化(c++)

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

动态规划+空间优化+时间优化(c++)

class Solution {
public:
    int numDistinct(string s, string t) {
        int n = s.size(), m = t.size();
        if (n < m) return 0;
        vector<long> dp(m, 0);
        vector<vector<int> > pos(300);
        for (int j = 1; j < m; ++j) {
            pos[t[j]].push_back(j);
        }
        dp[0] = (s[0] == t[0]);
        for (int i = 1; i < n; ++i) {
            int sz = pos[s[i]].size();
            for (int j = sz-1; j >= 0; --j) {
                int p = pos[s[i]][j];
                dp[p] += dp[p-1];
            }
            if (s[i] == t[0]) dp[0]++;
        }
        return dp[m-1];
    }
};

dfs+记忆化搜索(python)

class Solution:
    def numDistinct(self, s: str, t: str) -> int:
        n, m = len(s), len(t)
        if n < m:
            return 0
        self.dp = [[-1]*m for _ in range(n)]
        return self.dfs(n-1, m-1, s, t)
    
    def dfs(self, i, j, s, t):
        if j < 0:
            return 1
        if i < 0:
            return 0
        if self.dp[i][j] >= 0:
            return self.dp[i][j]
        res = self.dfs(i-1, j, s, t)
        if s[i] == t[j]:
            res += self.dfs(i-1, j-1, s, t)
        self.dp[i][j] = res
        return res

动态规划(python)

class Solution:
    def numDistinct(self, s: str, t: str) -> int:
        n, m = len(s), len(t)
        if n < m:
            return 0
        dp = [[0]*m for _ in range(n)]
        if s[0] == t[0]:
            dp[0][0] = 1
        for i in range(1, n):
            dp[i][0] = dp[i-1][0] + (1 if s[i] == t[0] else 0)
        for i in range(1, n):
            for j in range(1, m):
                dp[i][j] = dp[i-1][j]
                if s[i] == t[j]:
                    dp[i][j] += dp[i-1][j-1]
        return dp[n-1][m-1]

动态规划+空间优化(python)

class Solution:
    def numDistinct(self, s: str, t: str) -> int:
        n, m = len(s), len(t)
        if n < m:
            return 0
        dp = [0] * m
        if s[0] == t[0]:
            dp[0] = 1
        for i in range(1, n):
            for j in range(m-1, 0, -1):
                if s[i] == t[j]:
                    dp[j] += dp[j-1]
            if s[i] == t[0]:
                dp[0] += 1
        return dp[m-1]

动态规划+空间优化+时间优化(python)

class Solution:
    def numDistinct(self, s: str, t: str) -> int:
        n, m = len(s), len(t)
        if n < m:
            return 0
        dp = [0] * m
        pos = [[] for _ in range(300)]
        for j in range(1, m):
            pos[ord(t[j])].append(j)
        if s[0] == t[0]:
            dp[0] = 1
        for i in range(1, n):
            for j in pos[ord(s[i])][::-1]:
                if s[i] == t[j]:
                    dp[j] += dp[j-1]
            if s[i] == t[0]:
                dp[0] += 1
        return dp[m-1]

后记

这题还有个坑爹的地方,就是用动态规划写的时候,数组数据类型必须定义成 long 类型,否则会爆 int 范围,但是最终的答案又在 int 范围内。这其实就是因为动态规划计算了很多无用的状态,这些状态里有超出 int 范围的。而 dfs 用 int 就完全没有问题。

本题还是非常不错的,带你一步步从最好写的 dfs ,然后转化成动态规划,然后优化空间,减少数组维度,最后优化时间。优化时间在 c++ 上面提升不明显,但是 python 提升非常大,直接超过了 100% 的方法。

时间上还有一些小 trick ,我这里没有考虑,留给大家思考。比如对于状态 (i, j) ,如果 n-i < m-j ,那么就没必要更新了,因为 s 中剩下来的字符串都不够 t 剩下来的去匹配的。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,684评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,143评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,214评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,788评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,796评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,665评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,027评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,679评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,346评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,664评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,766评论 1 331
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,412评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,015评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,974评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,073评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,501评论 2 343

推荐阅读更多精彩内容

  • 题目描述(困难难度) 黑色的看成墙,蓝色的看成水,宽度一样,给定一个数组,每个数代表从左到右墙的高度,求出能装多少...
    windliang阅读 551评论 0 0
  • Python 内置函数:不需要引用库可以直接使用。 看了一些常见内置函数的使用。
    5c8e2b8217ae阅读 113评论 0 1
  • 场景:小明大学毕业后去一家大型公司应聘。这家公司的待遇条件非常好。可和谷歌相媲美! 造句: The company...
    梦想_9bd9阅读 328评论 0 0
  • 1.创建 2.绑定变量 3.只读 4.设置为密码框 5.属性 fg/bg/relief/width/height/...
    zmqqq阅读 1,186评论 0 0
  • 我跟你不是一个层次的人。所以我们很难结合到一起。为什么要硬逼自己呢。在物质条件上,你从来没有吃过苦,也许是你对他们...
    别推给时间阅读 894评论 0 0