Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit b5ed31b

Browse files
update latest content
1 parent b07478f commit b5ed31b

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

62 files changed

+10865
-10201
lines changed

‎README.md

Lines changed: 180 additions & 106 deletions
Large diffs are not rendered by default.

‎动态规划系列/LCS.md

Lines changed: 326 additions & 22 deletions
Large diffs are not rendered by default.

‎动态规划系列/动态规划之博弈问题.md

Lines changed: 64 additions & 249 deletions
Large diffs are not rendered by default.

‎动态规划系列/动态规划之正则表达.md

Lines changed: 212 additions & 123 deletions
Large diffs are not rendered by default.

‎动态规划系列/动态规划设计:最长递增子序列.md

Lines changed: 164 additions & 270 deletions
Large diffs are not rendered by default.

‎动态规划系列/动态规划详解进阶.md

Lines changed: 179 additions & 360 deletions
Large diffs are not rendered by default.

‎动态规划系列/单词拼接.md

Lines changed: 523 additions & 16 deletions
Large diffs are not rendered by default.

‎动态规划系列/团灭股票问题.md

Lines changed: 118 additions & 60 deletions
Large diffs are not rendered by default.
Lines changed: 267 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,29 @@
11
# 动态规划之子序列问题解题模板
22

3-
<p align='center'>
4-
<a href="https://github.com/labuladong/fucking-algorithm" target="view_window"><img alt="GitHub" src="https://img.shields.io/github/stars/labuladong/fucking-algorithm?label=Stars&style=flat-square&logo=GitHub"></a>
5-
<a href="https://labuladong.online/algo/" target="_blank"><img class="my_header_icon" src="https://img.shields.io/static/v1?label=精品课程&message=查看&color=pink&style=flat"></a>
6-
<a href="https://www.zhihu.com/people/labuladong"><img src="https://img.shields.io/badge/%E7%9F%A5%E4%B9%8E-@labuladong-000000.svg?style=flat-square&logo=Zhihu"></a>
7-
<a href="https://space.bilibili.com/14089380"><img src="https://img.shields.io/badge/B站-@labuladong-000000.svg?style=flat-square&logo=Bilibili"></a>
8-
</p>
3+
94

105
![](https://labuladong.online/algo/images/souyisou1.png)
116

12-
**通知:[新版网站会员](https://labuladong.online/algo/intro/site-vip/) 即将涨价;已支持老用户续费~另外,建议你在我的 [网站](https://labuladong.online/algo/) 学习文章,体验更好。**
7+
**通知:为满足广大读者的需求,网站上架 [速成目录](https://labuladong.online/algo/intro/quick-learning-plan/),如有需要可以看下,谢谢大家的支持~另外,建议你在我的 [网站](https://labuladong.online/algo/) 学习文章,体验更好。**
138

149

1510

1611
读完本文,你不仅学会了算法套路,还可以顺便解决如下题目:
1712

1813
| LeetCode | 力扣 | 难度 |
1914
| :----: | :----: | :----: |
20-
| [1312. Minimum Insertion Steps to Make a String Palindrome](https://leetcode.com/problems/minimum-insertion-steps-to-make-a-string-palindrome/) | [1312. 让字符串成为回文串的最少插入次数](https://leetcode.cn/problems/minimum-insertion-steps-to-make-a-string-palindrome/) | 🔴
21-
| [516. Longest Palindromic Subsequence](https://leetcode.com/problems/longest-palindromic-subsequence/) | [516. 最长回文子序列](https://leetcode.cn/problems/longest-palindromic-subsequence/) | 🟠
15+
| [1312. Minimum Insertion Steps to Make a String Palindrome](https://leetcode.com/problems/minimum-insertion-steps-to-make-a-string-palindrome/) | [1312. 让字符串成为回文串的最少插入次数](https://leetcode.cn/problems/minimum-insertion-steps-to-make-a-string-palindrome/) | 🔴|
16+
| [516. Longest Palindromic Subsequence](https://leetcode.com/problems/longest-palindromic-subsequence/) | [516. 最长回文子序列](https://leetcode.cn/problems/longest-palindromic-subsequence/) | 🟠|
2217

2318
**-----------**
2419

20+
21+
22+
> [!NOTE]
23+
> 阅读本文前,你需要先学习:
24+
>
25+
> - [动态规划核心框架](https://labuladong.online/algo/essential-technique/dynamic-programming-framework/)
26+
2527
子序列问题是常见的算法问题,而且并不好解决。
2628

2729
首先,子序列问题本身就相对子串、子数组更困难一些,因为前者是不连续的序列,而后两者是连续的,就算穷举你都不一定会,更别说求解相关的算法问题了。
@@ -34,72 +36,282 @@
3436

3537
既然要用动态规划,那就要定义 `dp` 数组,找状态转移关系。我们说的两种思路模板,就是 `dp` 数组的定义思路。不同的问题可能需要不同的 `dp` 数组定义来解决。
3638

37-
### 一、两种思路
39+
## 一、两种思路
3840

3941

4042

41-
<hr>
42-
<details class="hint-container details">
43-
<summary><strong>引用本文的文章</strong></summary>
4443

45-
- [动态规划设计:最长递增子序列](https://labuladong.online/algo/dynamic-programming/longest-increasing-subsequence/)
46-
- [如何判断回文链表](https://labuladong.online/algo/data-structure/palindrome-linked-list/)
47-
- [对动态规划进行降维打击](https://labuladong.online/algo/dynamic-programming/space-optimization/)
48-
- [最优子结构原理和 dp 数组遍历方向](https://labuladong.online/algo/dynamic-programming/faq-summary/)
49-
- [经典动态规划:最长公共子序列](https://labuladong.online/algo/dynamic-programming/longest-common-subsequence/)
5044

51-
</details><hr>
5245

5346

47+
**1、第一种思路模板是一个一维的 `dp` 数组**:
5448

49+
```java
50+
int n = array.length;
51+
int[] dp = new int[n];
5552

53+
for (int i = 1; i < n; i++) {
54+
for (int j = 0; j < i; j++) {
55+
dp[i] = 最值(dp[i], dp[j] + ...)
56+
}
57+
}
58+
```
5659

57-
**_____________**
60+
比如我们写过的 [最长递增子序列](https://labuladong.online/algo/dynamic-programming/longest-increasing-subsequence/)[最大子数组和](https://labuladong.online/algo/dynamic-programming/maximum-subarray/) 都是这个思路。
5861

59-
本文为会员内容,请扫码关注公众号或 [点这里](https://labuladong.online/algo/dynamic-programming/subsequence-problem/) 查看:
62+
在这个思路中 `dp` 数组的定义是:
6063

61-
![](https://labuladong.online/algo/images/qrcode.jpg)
64+
**在子数组 `arr[0..i]` 中,以 `arr[i]` 结尾的子序列的长度是 `dp[i]`**
6265

63-
======其他语言代码======
66+
为啥最长递增子序列需要这种思路呢?前文说得很清楚了,因为这样符合归纳法,可以找到状态转移的关系,这里就不具体展开了。
6467

65-
[516.最长回文子序列](https://leetcode-cn.com/problems/longest-palindromic-subsequence)
68+
**2、第二种思路模板是一个二维的 `dp` 数组**:
6669

67-
### javascript
70+
```java
71+
int n = arr.length;
72+
int[][] dp = new dp[n][n];
6873

69-
```js
70-
/**
71-
* @param {string} s
72-
* @return {number}
73-
*/
74-
var longestPalindromeSubseq = function (s) {
75-
let l = s.length;
76-
if (l <= 1) {
77-
return l;
74+
for (int i = 0; i < n; i++) {
75+
for (int j = 0; j < n; j++) {
76+
if (arr[i] == arr[j])
77+
dp[i][j] = dp[i][j] + ...
78+
else
79+
dp[i][j] = 最值(...)
7880
}
79-
80-
// 初始化一个 dp[l][l]
81-
let dp = new Array(l);
82-
for (let i = 0; i < l; i++) {
83-
dp[i] = new Array(l);
84-
dp[i].fill(0, 0, l)
85-
// // base case
86-
dp[i][i] = 1
81+
}
82+
```
83+
84+
这种思路运用相对更多一些,尤其是涉及两个字符串/数组的子序列时,比如前文讲的 [最长公共子序列](https://labuladong.online/algo/dynamic-programming/longest-common-subsequence/)[编辑距离](https://labuladong.online/algo/dynamic-programming/edit-distance/);这种思路也可以用于只涉及一个字符串/数组的情景,比如本文讲的回文子序列问题。
85+
86+
**2.1 涉及两个字符串/数组的场景**,`dp` 数组的定义如下:
87+
88+
**在子数组 `arr1[0..i]` 和子数组 `arr2[0..j]` 中,我们要求的子序列长度为 `dp[i][j]`**
89+
90+
**2.2 只涉及一个字符串/数组的场景**,`dp` 数组的定义如下:
91+
92+
**在子数组 `array[i..j]` 中,我们要求的子序列的长度为 `dp[i][j]`**
93+
94+
下面就看看最长回文子序列问题,详解一下第二种情况下如何使用动态规划。
95+
96+
## 二、最长回文子序列
97+
98+
之前解决了 [最长回文子串](https://labuladong.online/algo/essential-technique/array-two-pointers-summary/) 的问题,这次提升难度,看看力扣第 516 题「最长回文子序列」,求最长回文子序列的长度:
99+
100+
输入一个字符串 `s`,请你找出 `s` 中的最长回文子序列长度,函数签名如下:
101+
102+
```java
103+
int longestPalindromeSubseq(String s);
104+
```
105+
106+
比如说输入 `s = "aecda"`,算法返回 3,因为最长回文子序列是 `"aca"`,长度为 3。
107+
108+
我们对 `dp` 数组的定义是:**在子串 `s[i..j]` 中,最长回文子序列的长度为 `dp[i][j]`**。一定要记住这个定义才能理解算法。
109+
110+
为啥这个问题要这样定义二维的 `dp` 数组呢?我在 [最长递增子序列](https://labuladong.online/algo/dynamic-programming/longest-increasing-subsequence/) 提到,找状态转移需要归纳思维,说白了就是如何从已知的结果推出未知的部分。而这样定义能够进行归纳,容易发现状态转移关系。
111+
112+
具体来说,如果我们想求 `dp[i][j]`,假设你知道了子问题 `dp[i+1][j-1]` 的结果(`s[i+1..j-1]` 中最长回文子序列的长度),你是否能想办法算出 `dp[i][j]` 的值(`s[i..j]` 中,最长回文子序列的长度)呢?
113+
114+
![](https://labuladong.online/algo/images/lps/1.jpg)
115+
116+
可以!这取决于 `s[i]``s[j]` 的字符:
117+
118+
**如果它俩相等**,那么它俩加上 `s[i+1..j-1]` 中的最长回文子序列就是 `s[i..j]` 的最长回文子序列:
119+
120+
![](https://labuladong.online/algo/images/lps/2.jpg)
121+
122+
**如果它俩不相等**,说明它俩**不可能同时**出现在 `s[i..j]` 的最长回文子序列中,那么把它俩**分别**加入 `s[i+1..j-1]` 中,看看哪个子串产生的回文子序列更长即可:
123+
124+
![](https://labuladong.online/algo/images/lps/3.jpg)
125+
126+
以上两种情况写成代码就是这样:
127+
128+
129+
130+
131+
132+
```java
133+
if (s[i] == s[j])
134+
// 它俩一定在最长回文子序列中
135+
dp[i][j] = dp[i + 1][j - 1] + 2;
136+
else
137+
// s[i+1..j] 和 s[i..j-1] 谁的回文子序列更长?
138+
dp[i][j] = max(dp[i + 1][j], dp[i][j - 1]);
139+
```
140+
141+
142+
143+
至此,状态转移方程就写出来了,根据 dp 数组的定义,我们要求的就是 `dp[0][n - 1]`,也就是整个 `s` 的最长回文子序列的长度。
144+
145+
## 三、代码实现
146+
147+
首先明确一下 base case,如果只有一个字符,显然最长回文子序列长度是 1,也就是 `dp[i][j] = 1 (i == j)`
148+
149+
因为 `i` 肯定小于等于 `j`,所以对于那些 `i > j` 的位置,根本不存在什么子序列,应该初始化为 0。
150+
151+
另外,看看刚才写的状态转移方程,想求 `dp[i][j]` 需要知道 `dp[i+1][j-1]`,`dp[i+1][j]`,`dp[i][j-1]` 这三个位置;再看看我们确定的 base case,填入 `dp` 数组之后是这样:
152+
153+
![](https://labuladong.online/algo/images/lps/4.jpg)
154+
155+
**为了保证每次计算 `dp[i][j]`,左下右方向的位置已经被计算出来,只能斜着遍历或者反着遍历**:
156+
157+
![](https://labuladong.online/algo/images/lps/5.jpg)
158+
159+
> [!TIP]
160+
> 关于 `dp` 数组的遍历方向,详情见 [动态规划答疑篇](https://labuladong.online/algo/dynamic-programming/faq-summary/)
161+
162+
我选择反着遍历,代码如下:
163+
164+
```java
165+
class Solution {
166+
public int longestPalindromeSubseq(String s) {
167+
int n = s.length();
168+
// dp 数组全部初始化为 0
169+
int[][] dp = new int[n][n];
170+
// base case
171+
for (int i = 0; i < n; i++) {
172+
dp[i][i] = 1;
173+
}
174+
// 反着遍历保证正确的状态转移
175+
for (int i = n - 1; i >= 0; i--) {
176+
for (int j = i + 1; j < n; j++) {
177+
// 状态转移方程
178+
if (s.charAt(i) == s.charAt(j)) {
179+
dp[i][j] = dp[i + 1][j - 1] + 2;
180+
} else {
181+
dp[i][j] = Math.max(dp[i + 1][j], dp[i][j - 1]);
182+
}
183+
}
184+
}
185+
// 整个 s 的最长回文子串长度
186+
return dp[0][n - 1];
87187
}
88-
89-
// 从右下角开始,逐渐往上推
90-
for (let i = l - 2; i >= 0; i--) {
91-
for (let j = i + 1; j <= l - 1; j++) {
92-
if (s[i] === s[j]) {
93-
dp[i][j] = dp[i + 1][j - 1] + 2;
94-
} else {
95-
dp[i][j] = Math.max(
96-
dp[i + 1][j],
97-
dp[i][j - 1]
98-
)
188+
}
189+
```
190+
191+
<visual slug='longest-palindromic-subsequence'/>
192+
193+
194+
195+
至此,最长回文子序列的问题就解决了。
196+
197+
## 四、拓展延伸
198+
199+
虽然回文相关的问题没有什么特别广泛的使用场景,但是你会算最长回文子序列之后,一些类似的题目也可以顺手做掉。
200+
201+
比如力扣第 1312 题「计算让字符串成为回文串的最少插入次数」:
202+
203+
输入一个字符串 `s`,你可以在字符串的任意位置插入任意字符。如果要把 `s` 变成回文串,请你计算最少要进行多少次插入?
204+
205+
函数签名如下:
206+
207+
```java
208+
int minInsertions(String s);
209+
```
210+
211+
比如说输入 `s = "abcea"`,算法返回 2,因为可以给 `s` 插入 2 个字符变成回文串 `"abeceba"` 或者 `"aebcbea"`。如果输入 `s = "aba"`,则算法返回 0,因为 `s` 已经是回文串,不用插入任何字符。
212+
213+
这也是一道单字符串的子序列问题,所以我们也可以使用一个二维 `dp` 数组,其中 `dp[i][j]` 的定义如下:
214+
215+
**对字符串 `s[i..j]`,最少需要进行 `dp[i][j]` 次插入才能变成回文串**
216+
217+
根据 `dp` 数组的定义,base case 就是 `dp[i][i] = 0`,因为单个字符本身就是回文串,不需要插入。
218+
219+
然后使用数学归纳法,假设已经计算出了子问题 `dp[i+1][j-1]` 的值了,思考如何推出 `dp[i][j]` 的值:
220+
221+
![](https://labuladong.online/algo/images/palindrome-insert/1.jpeg)
222+
223+
实际上和最长回文子序列问题的状态转移方程非常类似,这里也分两种情况:
224+
225+
226+
227+
228+
229+
```java
230+
if (s[i] == s[j]) {
231+
// 不需要插入任何字符
232+
dp[i][j] = dp[i + 1][j - 1];
233+
} else {
234+
// 把 s[i+1..j] 和 s[i..j-1] 变成回文串,选插入次数较少的
235+
// 然后还要再插入一个 s[i] 或 s[j],使 s[i..j] 配成回文串
236+
dp[i][j] = min(dp[i + 1][j], dp[i][j - 1]) + 1;
237+
}
238+
```
239+
240+
241+
242+
最后,我们依然采取倒着遍历 `dp` 数组的方式,写出代码:
243+
244+
```java
245+
class Solution {
246+
public int minInsertions(String s) {
247+
int n = s.length();
248+
// dp[i][j] 表示把字符串 s[i..j] 变成回文串的最少插入次数
249+
// dp 数组全部初始化为 0
250+
int[][] dp = new int[n][n];
251+
// 反着遍历保证正确的状态转移
252+
for (int i = n - 1; i >= 0; i--) {
253+
for (int j = i + 1; j < n; j++) {
254+
// 状态转移方程
255+
if (s.charAt(i) == s.charAt(j)) {
256+
dp[i][j] = dp[i + 1][j - 1];
257+
} else {
258+
dp[i][j] = Math.min(dp[i + 1][j], dp[i][j - 1]) + 1;
259+
}
99260
}
100261
}
262+
// 整个 s 的最少插入次数
263+
return dp[0][n - 1];
101264
}
102-
return dp[0][l - 1]
103-
};
265+
}
104266
```
105267

268+
至此,这道题也使用子序列解题模板解决了,整体逻辑和最长回文子序列非常相似,那么这个问题是否可以直接复用回文子序列的解法呢?
269+
270+
其实是可以的,我们甚至都不用写状态转移方程,你仔细想想:
271+
272+
**我先算出字符串 `s` 中的最长回文子序列,那些不在最长回文子序列中的字符,不就是需要插入的字符吗**?
273+
274+
所以这道题可以直接复用之前实现的 `longestPalindromeSubseq` 函数:
275+
276+
```java
277+
class Solution {
278+
// 计算把 s 变成回文串的最少插入次数
279+
public int minInsertions(String s) {
280+
return s.length() - longestPalindromeSubseq(s);
281+
}
282+
283+
// 计算 s 中的最长回文子序列长度
284+
int longestPalindromeSubseq(String s) {
285+
// 见上文
286+
}
287+
}
288+
```
289+
290+
好了,子序列相关的算法就讲到这里,希望对你有启发。
291+
292+
293+
294+
295+
296+
297+
298+
<hr>
299+
<details class="hint-container details">
300+
<summary><strong>引用本文的文章</strong></summary>
301+
302+
- [动态规划设计:最长递增子序列](https://labuladong.online/algo/dynamic-programming/longest-increasing-subsequence/)
303+
- [对动态规划进行降维打击](https://labuladong.online/algo/dynamic-programming/space-optimization/)
304+
- [最优子结构原理和 dp 数组遍历方向](https://labuladong.online/algo/dynamic-programming/faq-summary/)
305+
- [经典动态规划:最长公共子序列](https://labuladong.online/algo/dynamic-programming/longest-common-subsequence/)
306+
307+
</details><hr>
308+
309+
310+
311+
312+
313+
**_____________**
314+
315+
316+
317+
![](https://labuladong.online/algo/images/souyisou2.png)

0 commit comments

Comments
(0)

AltStyle によって変換されたページ (->オリジナル) /