【leetcode热题】不同的子序列

给你两个字符串 s 和 t ,统计并返回在 s 的 子序列 中 t 出现的个数,结果需要对 109 + 7 取模。

示例 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

解法一 递归之分治

S 中的每个字母就是两种可能选他或者不选他。我们用递归的常规思路,将大问题化成小问题,也就是分治的思想。

如果我们求 S[0,S_len - 1] 中能选出多少个 T[0,T_len - 1],个数记为 n。那么分两种情况,

  • S[0] == T[0],需要知道两种情况

    • 从 S 中选择当前的字母,此时 S 跳过这个字母, T 也跳过一个字母。

      去求 S[1,S_len - 1] 中能选出多少个 T[1,T_len - 1],个数记为 n1

    • S 不选当前的字母,此时S跳过这个字母,T 不跳过字母。

      去求S[1,S_len - 1] 中能选出多少个 T[0,T_len - 1],个数记为 n2

  • S[0] != T[0]

    S 只能不选当前的字母,此时S跳过这个字母, T 不跳过字母。

    去求S[1,S_len - 1] 中能选出多少个 T[0,T_len - 1],个数记为 n1

也就是说如果求 S[0,S_len - 1] 中能选出多少个 T[0,T_len - 1],个数记为 n。转换为数学式就是

if(S[0] == T[0]){n = n1 + n2;
}else{n = n1;
}

推广到一般情况,我们可以先写出递归的部分代码。

public int numDistinct(String s, String t) {return numDistinctHelper(s, 0, t, 0);
}private int numDistinctHelper(String s, int s_start, String t, int t_start) {int count = 0;//当前字母相等if (s.charAt(s_start) == t.charAt(t_start)) {//从 S 选择当前的字母,此时 S 跳过这个字母, T 也跳过一个字母。count = numDistinctHelper(s, s_start + 1, t, t_start + 1, map)//S 不选当前的字母,此时 S 跳过这个字母,T 不跳过字母。+ numDistinctHelper(s, s_start + 1, t, t_start,  map);//当前字母不相等  }else{ //S 只能不选当前的字母,此时 S 跳过这个字母, T 不跳过字母。count = numDistinctHelper(s, s_start + 1, t, t_start,  map);}return count; 
}

递归出口的话,因为我们的ST的开始下标都是增长的。

如果S[s_start, S_len - 1]中, s_start 等于了 S_len ,意味着S是空串,从空串中选字符串T,那结果肯定是0

如果T[t_start, T_len - 1]中,t_start等于了 T_len,意味着T是空串,从S中选择空字符串T,只需要不选择 S 中的所有字母,所以选法是1

综上,代码总体就是下边的样子

public int numDistinct(String s, String t) {return numDistinctHelper(s, 0, t, 0);
}private int numDistinctHelper(String s, int s_start, String t, int t_start) {//T 是空串,选法就是 1 种if (t_start == t.length()) { return 1;}//S 是空串,选法是 0 种if (s_start == s.length()) {return 0;}int count = 0;//当前字母相等if (s.charAt(s_start) == t.charAt(t_start)) {//从 S 选择当前的字母,此时 S 跳过这个字母, T 也跳过一个字母。count = numDistinctHelper(s, s_start + 1, t, t_start + 1)//S 不选当前的字母,此时 S 跳过这个字母,T 不跳过字母。+ numDistinctHelper(s, s_start + 1, t, t_start);//当前字母不相等  }else{ //S 只能不选当前的字母,此时 S 跳过这个字母, T 不跳过字母。count = numDistinctHelper(s, s_start + 1, t, t_start);}return count; 
}

遗憾的是,这个解法对于如果S太长的 case 会超时。

原因就是因为递归函数中,我们多次调用了递归函数,这会使得我们重复递归很多的过程,解决方案就很简单了,Memoization 技术,把每次的结果利用一个map保存起来,在求之前,先看map中有没有,有的话直接拿出来就可以了。

mapkey的话就标识当前的递归,s_start 和 t_start 联合表示,利用字符串 s_start + '@' + t_start

value的话就保存这次递归返回的count

public int numDistinct(String s, String t) {HashMap<String, Integer> map = new HashMap<>();return numDistinctHelper(s, 0, t, 0, map);
}private int numDistinctHelper(String s, int s_start, String t, int t_start, HashMap<String, Integer> map) {//T 是空串,选法就是 1 种if (t_start == t.length()) { return 1;}//S 是空串,选法是 0 种if (s_start == s.length()) {return 0;}String key = s_start + "@" + t_start;//先判断之前有没有求过这个解if (map.containsKey(key)) {return map.get(key); }int count = 0;//当前字母相等if (s.charAt(s_start) == t.charAt(t_start)) {//从 S 选择当前的字母,此时 S 跳过这个字母, T 也跳过一个字母。count = numDistinctHelper(s, s_start + 1, t, t_start + 1, map)//S 不选当前的字母,此时 S 跳过这个字母,T 不跳过字母。+ numDistinctHelper(s, s_start + 1, t, t_start, map);//当前字母不相等  }else{ //S 只能不选当前的字母,此时 S 跳过这个字母, T 不跳过字母。count = numDistinctHelper(s, s_start + 1, t, t_start, map);}//将当前解放到 map 中map.put(key, count);return count; 
}

解法二 递归之回溯

回溯的思想就是朝着一个方向找到一个解,然后再回到之前的状态,改变当前状态,继续尝试得到新的解。可以类比于二叉树的DFS,一路走到底,然后回到之前的节点继续递归。

对于这道题,和二叉树的DFS很像了,每次有两个可选的状态,选择S串的当前字母和不选择当前字母。

S串的当前字母和T串的当前字母相等,我们就可以选择S的当前字母,进入递归。

递归出来以后,继续尝试不选择S的当前字母,进入递归。

代码可以是下边这样。

public int numDistinct3(String s, String t) { numDistinctHelper(s, 0, t, 0);
}private void numDistinctHelper(String s, int s_start, String t, int t_start) {//当前字母相等,选中当前 S 的字母,s_start 后移一个//选中当前 S 的字母,意味着和 T 的当前字母匹配,所以 t_start 后移一个if (s.charAt(s_start) == t.charAt(t_start)) {numDistinctHelper(s, s_start + 1, t, t_start + 1);}//出来以后,继续尝试不选择当前字母,s_start 后移一个,t_start 不后移numDistinctHelper(s, s_start + 1, t, t_start);
}

递归出口的话,就是两种了。

  • t_start == T_len,那么就意味着当前从S中选择的字母组成了T,此时就代表一种选法。我们可以用一个全局变量countcount计数此时就加一。然后return,返回到上一层继续寻求解。

  • s_start == S_len,此时S到达了结尾,直接 return。

int count = 0;
public int numDistinct(String s, String t) { numDistinctHelper(s, 0, t, 0);return count;
}private void numDistinctHelper(String s, int s_start, String t, int t_start) {if (t_start == t.length()) {count++; return;}if (s_start == s.length()) {return;}//当前字母相等,s_start 后移一个,t_start 后移一个if (s.charAt(s_start) == t.charAt(t_start)) {numDistinctHelper(s, s_start + 1, t, t_start + 1);}//出来以后,继续尝试不选择当前字母,s_start 后移一个,t_start 不后移numDistinctHelper(s, s_start + 1, t, t_start);
}

好吧,这个熟悉的错误又出现了,同样是递归中调用了两次递归,会重复计算一些解。怎么办呢?Memoization 技术。

mapkey和之前一样,标识当前的递归,s_start 和 t_start 联合表示,利用字符串 s_start + '@' + t_start

mapvalue的话?存什么呢。区别于解法一,我们每次都得到了当前条件下的count,然后存起来了。而现在我们只有一个全局变量,该怎么办呢?存全局变量count吗?

如果递归过程中

if (map.containsKey(key)) {... ...
}

遇到了已经求过的解该怎么办呢?

我们每次得到一个解后增加全局变量count,所以我们mapvalue存两次递归后 count 的增量。这样的话,第二次遇到同样的情况的时候,就不用递归了,把当前增量加上就可以了。

if (map.containsKey(key)) {count += map.get(key);return; 
}

综上,代码就出来了

int count = 0;
public int numDistinct(String s, String t) { HashMap<String, Integer> map = new HashMap<>();numDistinctHelper(s, 0, t, 0, map);return count;
}private void numDistinctHelper(String s, int s_start, String t, int t_start, HashMap<String, Integer> map) {if (t_start == t.length()) {count++; return;}if (s_start == s.length()) {return;}String key = s_start + "@" + t_start;if (map.containsKey(key)) {count += map.get(key);return; }int count_pre = count;//当前字母相等,s_start 后移一个,t_start 后移一个if (s.charAt(s_start) == t.charAt(t_start)) {numDistinctHelper(s, s_start + 1, t, t_start + 1, map);}//出来以后,继续尝试不选择当前字母,s_start 后移一个,t_start 不后移numDistinctHelper(s, s_start + 1, t, t_start, map);//将增量存起来int count_increment = count - count_pre;map.put(key, count_increment); 
}

解法三 动态规划

让我们来回想一下解法一做了什么。s_start 和 t_start 不停的增加,一直压栈,压栈,直到

//T 是空串,选法就是 1 种
if (t_start == t.length()) { return 1;
}
//S 是空串,选法是 0 种
if (s_start == s.length()) {return 0;
}

T 是空串或者 S 是空串,我们就直接可以返回结果了,接下来就是不停的出栈出栈,然后把结果通过递推关系取得。

递归的过程就是由顶到底再回到顶。

动态规划要做的就是去省略压栈的过程,直接由底向顶。

这里我们用一个二维数组 dp[m][n] 对应于从 S[m,S_len) 中能选出多少个 T[n,T_len)

当 m == S_len,意味着S是空串,此时dp[S_len][n],n 取 0 到 T_len - 1的值都为 0

当 n == T_len,意味着T是空串,此时dp[m][T_len],m 取 0 到 S_len的值都为 1

然后状态转移的话和解法一分析的一样。如果求dp[s][t]

  • S[s] == T[t],当前字符相等,那就对应两种情况,选择S的当前字母和不选择S的当前字母

    dp[s][t] = dp[s+1][t+1] + dp[s+1][t]

  • S[s] != T[t],只有一种情况,不选择S的当前字母

    dp[s][t] = dp[s+1][t]

代码就可以写了。

public int numDistinct(String s, String t) {int s_len = s.length();int t_len = t.length();int[][] dp = new int[s_len + 1][t_len + 1];//当 T 为空串时,所有的 s 对应于 1for (int i = 0; i <= s_len; i++) {dp[i][t_len] = 1;}//倒着进行,T 每次增加一个字母for (int t_i = t_len - 1; t_i >= 0; t_i--) {dp[s_len][t_i] = 0; // 这句可以省去,因为默认值是 0//倒着进行,S 每次增加一个字母for (int s_i = s_len - 1; s_i >= 0; s_i--) {//如果当前字母相等if (t.charAt(t_i) == s.charAt(s_i)) {//对应于两种情况,选择当前字母和不选择当前字母dp[s_i][t_i] = dp[s_i + 1][t_i + 1] + dp[s_i + 1][t_i];//如果当前字母不相等} else {dp[s_i][t_i] = dp[s_i + 1][t_i];}}}return dp[0][0];
}

对比于解法一和解法二,如果Memoization 技术我们不用hash,而是用一个二维数组,会发现其实我们的递归过程,其实就是在更新下图中的二维表,只不过更新的顺序没有动态规划这么归整。这也是不用Memoization 技术会超时的原因,如果把递归的更新路线画出来,会发现很多路线重合了,意味着我们进行了很多没有必要的递归,从而造成了超时。

我们画一下动态规划的过程。

S = "babgbag", T = "bag"

T 为空串时,所有的 s 对应于 1。 S 为空串时,所有的 t 对应于 0。

此时我们从 dp[6][2] 开始求。根据公式,因为当前字母相等,所以 dp[6][2] = dp[7][3] + dp[7][2] = 1 + 0 = 1 。

接着求dp[5][2],当前字母不相等,dp[5][2] = dp[6][2] = 1

一直求下去。

求当前问号的地方的值的时候,我们只需要它的上一个值和斜对角的值。

换句话讲,求当前列的时候,我们只需要上一列的信息。比如当前求第1列,第3列的值就不会用到了。

所以我们可以优化算法的空间复杂度,不需要二维数组,需要一维数组就够了。

此时需要解决一个问题,就是当求上图的dp[1][1]的时候,需要dp[2][1]dp[2][2]的信息。但是如果我们是一维数组,dp[2][1]之前已经把dp[2][2]的信息覆盖掉了。所以我们需要一个pre变量保存之前的值。

public int numDistinct(String s, String t) {int s_len = s.length();int t_len = t.length();int[]dp = new int[s_len + 1];for (int i = 0; i <= s_len; i++) {dp[i] = 1;}//倒着进行,T 每次增加一个字母for (int t_i = t_len - 1; t_i >= 0; t_i--) {int pre = dp[s_len];dp[s_len] = 0; //倒着进行,S 每次增加一个字母for (int s_i = s_len - 1; s_i >= 0; s_i--) {int temp = dp[s_i];if (t.charAt(t_i) == s.charAt(s_i)) {dp[s_i] = dp[s_i + 1] + pre;} else {dp[s_i] = dp[s_i + 1];}pre = temp;}}return dp[0];
}

利用temppre两个变量实现了保存之前的值。

其实动态规划优化空间复杂度的思想,在 5题,10题,53题,72题 等等都已经用了,是非常经典的。

上边的动态规划是从字符串末尾倒着进行的,其实我们只要改变dp数组的含义,用dp[m][n]表示S[0,m)T[0,n),然后两层循环我们就可以从 1 往末尾进行了,思想是类似的,leetcode 高票答案也都是这样的,如果理解了上边的思想,代码其实也很好写。这里只分享下代码吧。

public int numDistinct(String s, String t) {int s_len = s.length();int t_len = t.length();int[] dp = new int[s_len + 1];for (int i = 0; i <= s_len; i++) {dp[i] = 1;}for (int t_i = 1; t_i <= t_len; t_i++) {int pre = dp[0];dp[0] = 0;for (int s_i = 1; s_i <= s_len; s_i++) {int temp = dp[s_i];if (t.charAt(t_i - 1) == s.charAt(s_i - 1)) {dp[s_i] = dp[s_i - 1] + pre;} else {dp[s_i] = dp[s_i - 1];}pre = temp;}}return dp[s_len];
}

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://xiahunao.cn/news/2806348.html

如若内容造成侵权/违法违规/事实不符,请联系瞎胡闹网进行投诉反馈,一经查实,立即删除!

相关文章

Ps:明度直方图

明度 Luminosity直方图显示了图像中各个亮度级别的像素分布情况。 与 RGB 直方图不同&#xff0c;“明度”直方图专注于图像的亮度信息&#xff0c;而不是单独的颜色信息。 在“直方图”面板的通道中选择“明度”。 “明度”直方图提供了一种量化的方式来理解图像的整体明暗结构…

数字滚动实现

介绍 vue-countup-v3 插件是一个基于 Vue3 的数字动画插件&#xff0c;用于在网站或应用程序中创建带有数字动画效果的计数器。通过该插件&#xff0c;我们可以轻松地实现数字的递增或递减动画&#xff0c;并自定义其样式和动画效果。该插件可以用于许多场景&#xff0c;例如展…

MYSQL安装及卸载

目录 一、下载 二、解压 三、配置 1. 添加环境变量 2. 初始化MySQL 3. 注册MySQL服务 4. 启动MySQL服务 5. 修改默认账户密码 四、登录MySQL 五、卸载MySQL 一、下载 点开下面的链接&#xff1a;MySQL :: Download MySQL Community Server 点击Download 就可以下载对…

【深度学习目标检测】十八、基于深度学习的人脸检测系统-含GUI和源码(python,yolov8)

人脸检测是计算机视觉中的一个重要方向&#xff0c;也是一个和人们生活息息相关的研究方向&#xff0c;因为人脸是人最重要的外貌特征。人脸检测技术的重要性主要体现在以下几个方面&#xff1a; 人脸识别与安全&#xff1a;人脸检测是人脸识别系统的一个关键部分&#xff0c;是…

人工智能 — 特征选择、特征提取、PCA

目录 一、特征选择1、定义2、原因3、做法4、生成过程5、停止条件 二、特征提取三、PCA 算法1、零均值化&#xff08;中心化&#xff09;2、方差3、协方差4、协方差矩阵5、对协方差矩阵求特征值、特征矩阵6、对特征值进行排序7、评价模型8、代码实现9、sklearn 库10、鸢尾花实例…

Flink join详解(含两类API及coGroup、connect详解)

Flink SQL支持对动态表进行复杂而灵活的连接操作。 为了处理不同的场景&#xff0c;需要多种查询语义&#xff0c;因此有几种不同类型的 Join。 默认情况下&#xff0c;joins 的顺序是没有优化的。表的 join 顺序是在 FROM 从句指定的。可以通过把更新频率最低的表放在第一个、…

Python 实现 BRAR 指标计算(情绪指标):股票技术分析的利器系列(11)

Python 实现 BRAR 指标计算&#xff08;情绪指标&#xff09;&#xff1a;股票技术分析的利器系列&#xff08;11&#xff09; 介绍算法公式 代码rolling函数介绍核心代码计算BR计算AR 完整代码 介绍 BRAR 是一种情绪指标&#xff0c;用于衡量特定金融市场中的买卖情绪。它代表…

高考志愿辅助填报系统

高考志愿辅助填报系统 获取源码——》公主号&#xff1a;计算机专业毕设大全

一文搞懂TCP三次握手与四次挥手

什么是TCP协议&#xff1f; TCP&#xff08;Transmission control protocol&#xff09;即传输控制协议&#xff0c;是一种面向连接、可靠的数据传输协议&#xff0c;它是为了在不可靠的互联网上提供可靠的端到端字节流而专门设计的一个传输协议。 面向连接&#xff1a;数据传…

Java智慧工地云综合管理平台SaaS源码 助力工地实现精细化管理

目录 智慧工地系统介绍 1、可视化大屏 2、视频监控 3、Wi-Fi安全教育 4、环境监测 5、高支模监测 6、深基坑监测 7、智能水电监测 8、塔机升降安全监测 智慧工地系统功能模块 1、基础数据管理 2、考勤管理 3、安全隐患管理 4、视频监控 5、塔吊监控 6、升降机监…

泰迪智能科技中职大数据专业建设解决方案

泰迪智能科技基于十余年的数据智能产业实践经验&#xff0c;专注于大数据和人工智能方向&#xff0c;构建“产、岗、课、赛、证、文”融通的特色职业人才培养模式&#xff0c;助力中国职业教育高质量发展。 面相中职学校的大数据岗位群 目前就业市场上&#xff0c;大数据相关…

Python奇幻之旅(从入门到入狱高级篇)——面向对象进阶篇(下)

目录 引言 3. 面向对象高级和应用 3.1. 继承【补充】 3.1.1. mro和c3算法 c3算法 一句话搞定继承关系 3.1.2. py2和py3区别 3.3. 异常处理 3.3.1. 异常细分 3.3.2. 自定义异常&抛出异常 3.3.3. 特殊的finally 3.4. 反射 3.4.1. 一些皆对象 3.4.2. import_modu…

第十四章[面向对象]:14.8:枚举类

一,定义枚举类 1,把一个类定义为枚举类: 只需要让它继承自 enum 模块中的 Enum 类即可。 例如在下面的例子中,Weekday 类继承自 Enum 类, 则表明这是一个枚举类 枚举类的每个成员都由 2 部分组成,分别是 name 和 value, 其中 name 属性值为该枚举值的变量名(如下例中: …

微信小程序 ---- 生命周期

目录 生命周期 1. 小程序运行机制 2. 小程序更新机制 3. 生命周期介绍 4. 应用级别生命周期 5. 页面级别生命周期 6. 生命周期两个细节补充说明 7. 组件生命周期 总结 生命周期 1. 小程序运行机制 冷启动与热启动&#xff1a; 小程序启动可以分为两种情况&#xff0…

flutter插件开发基础教程

前言 虽然现在已经有很多插件了&#xff0c;但是有时候还是需要自己开发一个插件。因此打算学习一下如何开发一个插件。这里只考虑安卓&#xff0c;安卓使用kotlin&#xff0c;kotlin不会也没事&#xff0c;我也不会。 参考项目&#xff1a;https://github.com/TBoyLi/flutte…

【更换yarn的位置】解决yarn和nodejs不在同一盘下产生的某些命令应用失败问题

具体问题我记得是command fail什么error&#xff0c;记不太清楚了&#xff0c;文章主要写了如何替换yarn路径&#xff0c;希望可以帮助到大家。

【YOLO系列算法人员摔倒检测】

YOLO系列算法人员摔倒检测 模型和数据集下载YOLO系列算法的人员摔倒检测数据集可视化数据集图像示例&#xff1a; 模型和数据集下载 yolo行人跌倒检测一&#xff1a; 1、训练好的行人跌倒检测权重以及PR曲线&#xff0c;loss曲线等等&#xff0c;map达90%多&#xff0c;在行人跌…

测试需求平台7-产品管理服务接口一篇搞定

✍此系列为整理分享已完结入门搭建《TPM提测平台》系列的迭代版&#xff0c;拥抱Vue3.0将前端框架替换成字节最新开源的arco.design&#xff0c;其中约60%重构和20%新增内容&#xff0c;定位为从 0-1手把手实现简单的测试平台开发教程&#xff0c;内容将囊括基础、扩展和实战&a…

VSCode中打开md文件的智能提示

VSCode中打开md文件的智能提示 vscode中md的只能提示是默认关闭的,要打开必须要做些设置. 搜了好多文章,都是坑! 明明没设置成功,参数类型不对还信誓旦旦的坑自己同胞! 也难怪国内人学的那么难,反而国外学的很简单! 找了以下外面的资料,还是隔壁的人认真,给出了以下方法,测试成…

《TCP/IP详解 卷一》第6章 DHCP

目录 6.1 引言 6.2 DHCP 6.2.1 地址池和租用 6.2.2 DHCP和BOOTP消息格式 6.2.3 DHCP和BOOTP选项 6.2.4 DHCP协议操作 6.2.5 DHCPv6 6.2.6 DCHP中继 6.2.7 DHCP认证 6.2.8 重新配置扩展 6.2.9 快速确认 6.2.10 位置信息&#xff08;LCI和LoST&#xff09; 6.2.11 移…