Files
leetcode-master/problems/0392.判断子序列.md
2025-05-19 17:11:04 +08:00

406 lines
13 KiB
Markdown
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

* [做项目多个C++、Java、Go、测开、前端项目](https://www.programmercarl.com/other/kstar.html)
* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html)
* [背八股40天挑战高频面试题](https://www.programmercarl.com/xunlian/bagu.html)
# 392.判断子序列
[力扣题目链接](https://leetcode.cn/problems/is-subsequence/)
给定字符串 s 和 t ,判断 s 是否为 t 的子序列。
字符串的一个子序列是原始字符串删除一些(也可以不删除)字符而不改变剩余字符相对位置形成的新字符串。(例如,"ace"是"abcde"的一个子序列,而"aec"不是)。
示例 1
* 输入s = "abc", t = "ahbgdc"
* 输出true
示例 2
* 输入s = "axc", t = "ahbgdc"
* 输出false
提示:
* 0 <= s.length <= 100
* 0 <= t.length <= 10^4
两个字符串都只由小写字符组成。
## 算法公开课
**[《代码随想录》算法视频公开课](https://programmercarl.com/other/gongkaike.html)[动态规划,用相似思路解决复杂问题 | LeetCode392.判断子序列](https://www.bilibili.com/video/BV1tv4y1B7ym/),相信结合视频再看本篇题解,更有助于大家对本题的理解**。
## 思路
这道题也可以用双指针的思路来实现时间复杂度也是O(n)
这道题应该算是编辑距离的入门题目,因为从题意中我们也可以发现,只需要计算删除的情况,不用考虑增加和替换的情况。
**所以掌握本题的动态规划解法是对后面要讲解的编辑距离的题目打下基础**
动态规划五部曲分析如下:
1. 确定dp数组dp table以及下标的含义
**dp[i][j] 表示以下标i-1为结尾的字符串s和以下标j-1为结尾的字符串t相同子序列的长度为dp[i][j]**
注意这里是判断s是否为t的子序列。即t的长度是大于等于s的。
有同学问了为啥要表示下标i-1为结尾的字符串呢为啥不表示下标i为结尾的字符串呢
为什么这么定义我在 [718. 最长重复子数组](https://programmercarl.com/0718.最长重复子数组.html) 中做了详细的讲解。
其实用i来表示也可以
但我统一以下标i-1为结尾的字符串来计算这样在下面的递归公式中会容易理解一些如果还有疑惑可以继续往下看。
2. 确定递推公式
在确定递推公式的时候,首先要考虑如下两种操作,整理如下:
* if (s[i - 1] == t[j - 1])
* t中找到了一个字符在s中也出现了
* if (s[i - 1] != t[j - 1])
* 相当于t要删除元素继续匹配
if (s[i - 1] == t[j - 1])那么dp[i][j] = dp[i - 1][j - 1] + 1;因为找到了一个相同的字符相同子序列长度自然要在dp[i-1][j-1]的基础上加1**如果不理解在回看一下dp[i][j]的定义**
if (s[i - 1] != t[j - 1])此时相当于t要删除元素t如果把当前元素t[j - 1]删除那么dp[i][j] 的数值就是 看s[i - 1]与 t[j - 2]的比较结果了dp[i][j] = dp[i][j - 1];
其实这里 大家可以发现和 [1143.最长公共子序列](https://programmercarl.com/1143.最长公共子序列.html) 的递推公式基本那就是一样的,区别就是 本题 如果删元素一定是字符串t而 1143.最长公共子序列 是两个字符串都可以删元素。
3. dp数组如何初始化
从递推公式可以看出dp[i][j]都是依赖于dp[i - 1][j - 1] 和 dp[i][j - 1]所以dp[0][0]和dp[i][0]是一定要初始化的。
这里大家已经可以发现在定义dp[i][j]含义的时候为什么要**表示以下标i-1为结尾的字符串s和以下标j-1为结尾的字符串t相同子序列的长度为dp[i][j]**。
因为这样的定义在dp二维矩阵中可以留出初始化的区间如图
![392.判断子序列](https://file1.kamacoder.com/i/algo/20210303173115966.png)
如果要是定义的dp[i][j]是以下标i为结尾的字符串s和以下标j为结尾的字符串t初始化就比较麻烦了。
dp[i][0] 表示以下标i-1为结尾的字符串与空字符串的相同子序列长度所以为0. dp[0][j]同理。
```CPP
vector<vector<int>> dp(s.size() + 1, vector<int>(t.size() + 1, 0));
```
4. 确定遍历顺序
同理从递推公式可以看出dp[i][j]都是依赖于dp[i - 1][j - 1] 和 dp[i][j - 1],那么遍历顺序也应该是从上到下,从左到右
如图所示:
![392.判断子序列1](https://file1.kamacoder.com/i/algo/20210303172354155.jpg)
5. 举例推导dp数组
以示例一为例输入s = "abc", t = "ahbgdc"dp状态转移图如下
![392.判断子序列2](https://file1.kamacoder.com/i/algo/2021030317364166.jpg)
dp[i][j]表示以下标i-1为结尾的字符串s和以下标j-1为结尾的字符串t 相同子序列的长度所以如果dp[s.size()][t.size()] 与 字符串s的长度相同说明s与t的最长相同子序列就是s那么s 就是 t 的子序列。
图中dp[s.size()][t.size()] = 3 而s.size() 也为3。所以s是t 的子序列返回true。
动规五部曲分析完毕C++代码如下:
```CPP
class Solution {
public:
bool isSubsequence(string s, string t) {
vector<vector<int>> dp(s.size() + 1, vector<int>(t.size() + 1, 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] + 1;
else dp[i][j] = dp[i][j - 1];
}
}
if (dp[s.size()][t.size()] == s.size()) return true;
return false;
}
};
```
* 时间复杂度O(n × m)
* 空间复杂度O(n × m)
## 总结
这道题目算是编辑距离的入门题目(毕竟这里只是涉及到减法),也是动态规划解决的经典题型。
这一类题都是题目读上去感觉很复杂,模拟一下也发现很复杂,用动规分析完了也感觉很复杂,但是最终代码却很简短。
在之前的题目讲解中,我们讲了 [1143.最长公共子序列](https://programmercarl.com/1143.最长公共子序列.html),大家会发现 本题和 1143.最长公共子序列 的相似之处。
编辑距离的题目最能体现出动规精髓和巧妙之处,大家可以好好体会一下。
## 其他语言版本
### Java:
```java
class Solution {
public boolean isSubsequence(String s, String t) {
int length1 = s.length(); int length2 = t.length();
int[][] dp = new int[length1+1][length2+1];
for(int i = 1; i <= length1; i++){
for(int j = 1; j <= length2; j++){
if(s.charAt(i-1) == t.charAt(j-1)){
dp[i][j] = dp[i-1][j-1] + 1;
}else{
dp[i][j] = dp[i][j-1];
}
}
}
if(dp[length1][length2] == length1){
return true;
}else{
return false;
}
}
}
```
> 修改遍历顺序后可以利用滚动数组对dp数组进行压缩
```java
class Solution {
public boolean isSubsequence(String s, String t) {
// 修改遍历顺序外圈遍历t内圈遍历s。使得dp的推算只依赖正上方和左上方方便压缩。
int[][] dp = new int[t.length() + 1][s.length() + 1];
for (int i = 1; i < dp.length; i++) { // 遍历t字符串
for (int j = 1; j < dp[i].length; j++) { // 遍历s字符串
if (t.charAt(i - 1) == s.charAt(j - 1)) {
dp[i][j] = dp[i - 1][j - 1] + 1;
} else {
dp[i][j] = dp[i - 1][j];
}
}
System.out.println(Arrays.toString(dp[i]));
}
return dp[t.length()][s.length()] == s.length();
}
}
```
> 状态压缩
```java
class Solution {
public boolean isSubsequence(String s, String t) {
int[] dp = new int[s.length() + 1];
for (int i = 0; i < t.length(); i ++) {
// 需要使用上一轮的dp[j - 1],所以使用倒序遍历
for (int j = dp.length - 1; j > 0; j --) {
// i遍历的是t字符串j遍历的是dp数组dp数组的长度比s的大1因此需要减1。
if (t.charAt(i) == s.charAt(j - 1)) {
dp[j] = dp[j - 1] + 1;
}
}
}
return dp[s.length()] == s.length();
}
}
```
> 将dp定义为boolean类型dp[i]直接表示s.substring(0, i)是否为t的子序列
```java
class Solution {
public boolean isSubsequence(String s, String t) {
boolean[] dp = new boolean[s.length() + 1];
// 表示 “” 是t的子序列
dp[0] = true;
for (int i = 0; i < t.length(); i ++) {
for (int j = dp.length - 1; j > 0; j --) {
if (t.charAt(i) == s.charAt(j - 1)) {
dp[j] = dp[j - 1];
}
}
}
return dp[dp.length - 1];
}
}
```
### Python
```python
class Solution:
def isSubsequence(self, s: str, t: str) -> bool:
dp = [[0] * (len(t)+1) for _ in range(len(s)+1)]
for i in range(1, len(s)+1):
for j in range(1, len(t)+1):
if s[i-1] == t[j-1]:
dp[i][j] = dp[i-1][j-1] + 1
else:
dp[i][j] = dp[i][j-1]
if dp[-1][-1] == len(s):
return True
return False
```
### JavaScript
```javascript
const isSubsequence = (s, t) => {
// s、t的长度
const [m, n] = [s.length, t.length];
// dp全初始化为0
const dp = new Array(m + 1).fill(0).map(x => new Array(n + 1).fill(0));
for (let i = 1; i <= m; i++) {
for (let j = 1; j <= n; j++) {
// 更新dp[i][j],两种情况
if (s[i - 1] === t[j - 1]) {
dp[i][j] = dp[i - 1][j - 1] + 1;
} else {
dp[i][j] = dp[i][j - 1];
}
}
}
// 遍历结束判断dp右下角的数是否等于s的长度
return dp[m][n] === m ? true : false;
};
```
### TypeScript
> 二维数组
```typescript
function isSubsequence(s: string, t: string): boolean {
/**
dp[i][j]: s的前i-1个t的前j-1个最长公共子序列的长度
*/
const sLen = s.length
const tLen = t.length
const dp: number[][] = new Array(sLen + 1).fill(0).map(_ => new Array(tLen + 1).fill(0))
for (let i = 1; i <= sLen; i++) {
for (let j = 1; j <= tLen; j++) {
if (s[i - 1] === t[j - 1]) dp[i][j] = dp[i - 1][j - 1] + 1
// 只需要取 j-2 的 dp 值即可,不用考虑 i-2
else dp[i][j] = dp[i][j - 1]
}
}
return dp[sLen][tLen] === s.length
}
```
> 滚动数组
```typescript
function isSubsequence(s: string, t: string): boolean {
const sLen = s.length
const tLen = t.length
const dp: number[] = new Array(tLen + 1).fill(0)
for (let i = 1; i <= sLen; i++) {
let prev: number = 0;
let temp: number = 0;
for (let j = 1; j <= tLen; j++) {
// 备份一下当前状态(经过上层迭代后的)
temp = dp[j]
// prev 相当于 dp[j-1](累加了上层的状态)
// 如果单纯 dp[j-1] 则不会包含上层状态
if (s[i - 1] === t[j - 1]) dp[j] = prev + 1
else dp[j] = dp[j - 1]
// 继续使用上一层状态更新参数用于当前层下一个状态
prev = temp
}
}
return dp[tLen] === sLen
}
```
### Go
二维DP
```go
func isSubsequence(s string, t string) bool {
dp := make([][]int, len(s) + 1)
for i := 0; i < len(dp); i ++{
dp[i] = make([]int, len(t) + 1)
}
for i := 1; i < len(dp); i ++{
for j := 1; j < len(dp[i]); j ++{
if s[i - 1] == t[j - 1] {
dp[i][j] = dp[i - 1][j - 1] +1
}else{
dp[i][j] = dp[i][j - 1]
}
}
}
return dp[len(s)][len(t)] == len(s)
}
```
一维DP
```go
func isSubsequence(s string, t string) bool {
dp := make([]int, len(s) + 1)
for i := 1; i <= len(t); i ++ {
for j := len(s); j >= 1; j -- {
if t[i - 1] == s[j - 1] {
dp[j] = dp[j - 1] + 1
}
}
}
return dp[len(s)] == len(s)
}
```
### Rust
```rust
impl Solution {
pub fn is_subsequence(s: String, t: String) -> bool {
let mut dp = vec![vec![0; t.len() + 1]; s.len() + 1];
for (i, char_s) in s.chars().enumerate() {
for (j, char_t) in t.chars().enumerate() {
if char_s == char_t {
dp[i + 1][j + 1] = dp[i][j] + 1;
continue;
}
dp[i + 1][j + 1] = dp[i + 1][j]
}
}
dp[s.len()][t.len()] == s.len()
}
}
```
> 滚动数组
```rust
impl Solution {
pub fn is_subsequence(s: String, t: String) -> bool {
let mut dp = vec![0; t.len() + 1];
let (s, t) = (s.as_bytes(), t.as_bytes());
for &byte_s in s {
let mut pre = 0;
for j in 0..t.len() {
let temp = dp[j + 1];
if byte_s == t[j] {
dp[j + 1] = pre + 1;
} else {
dp[j + 1] = dp[j];
}
pre = temp;
}
}
dp[t.len()] == s.len()
}
}
```