@@ -10,7 +10,6 @@ Tag : 「回文串」、「线性 DP」
1010
1111返回符合要求的 最少分割次数 。
1212
13- 1413示例 1:
1514```
1615输入:s = "aab"
@@ -34,107 +33,98 @@ Tag : 「回文串」、「线性 DP」
3433
3534---
3635
37- ### 动态规划解法
36+ ### 动态规划
3837
39- 如果在 [ 131. 分割回文串] ( https://leetcode-cn. com/problems/palindrome-partitioning/ ) 你有使用到 DP 进行预处理的话。
38+ 如果在 [ 131. 分割回文串] ( https://mp.weixin.qq. com/s?__biz=MzU4NDE3MTEyMA==&mid=2247487047&idx=1&sn=117c48f20778868442fce44e100d2ea8&chksm=fd9ca558caeb2c4eb1bff4f0878ff796feabe523657c2aafea0b2d1c7026e1c0572ab1e6d205&token=635532356&lang=zh_CN#rd ) 你有使用到 DP 进行预处理的话。
4039
41- 这道题就很简单了,就是一道常规的动态规划题目 。
40+ 这道题就很简单了,就是一道常规的动态规划题 。
4241
43- #### 递推「最小分割次数」思路
42+ 为了方便,我们约定所有下标从 1ドル$ 开始。
4443
45- 我们定义 ` f[i] ` 为以下标为 ` i ` 的字符作为结尾的最小分割次数,那么最终答案为 ` f[n - 1] ` 。
44+ 即对于长度为 $n$ 的字符串,我们使用 $ [ 1,n ] $ 进行表示。估计不少看过三叶题解的同学都知道,这样做的目的是为了减少边界情况判断,这本身也是对于「哨兵」思想的运用 。
4645
47- 不失一般性的考虑第 ` j ` 字符的分割方案:
46+ * ** 递推「最小分割次数」思路 **
4847
49- 1 . 从起点字符到第 ` j ` 个字符能形成回文串,那么最小分割次数为 0。此时有 ` f[j] = 0 `
50- 2 . 从起点字符到第 ` j ` 个字符不能形成回文串:
51- 1 . 该字符独立消耗一次分割次数。此时有 ` f[j] = f[j - 1] + 1 `
52- 2 . 该字符不独立消耗一次分割次数,而是与前面的某个位置 ` i ` 形成回文串,` [i, j] ` 作为整体消耗一次分割次数。此时有 ` f[j] = f[i - 1] + 1 `
48+ 我们定义 $f[ r] $ 为将 $[ 1,r] $ 这一段字符分割为若干回文串的最小分割次数,那么最终答案为 $f[ n] $。
5349
54- 在 2.2 中满足回文要求的位置 ` i ` 可能有很多,我们在所有方案中取一个 min 即可。
50+ 不失一般性的考虑 $f [ r ] $ 如何转移:
5551
56- #### 快速判断「任意一段子串是否回文」思路
52+ 1 . 从「起点字符」到「第 $r$ 个字符」能形成回文串。那么最小分割次数为 0,此时有 $f[ r] = 0$
53+ 2 . 从「起点字符」到「第 $r$ 个字符」不能形成回文串。此时我们需要枚举左端点 $l,ドル如果 $[ l,r] $ 这一段是回文串的话,那么有 $f[ r] = f[ l - 1] + 1$
5754
58- 剩下的问题是,我们如何快速判断连续一段 ` [i, j] ` 是否为回文串,做法和昨天的 [ 131. 分割回文串 ] ( https://leetcode-cn.com/problems/palindrome-partitioning/ ) 一模一样 。
55+ 在 2ドル$ 中满足回文要求的左端点位置 $l$ 可能有很多个,我们在所有方案中取一个 $\min$ 即可 。
5956
60- * PS. 昨天的题目,数据范围只有 16,因此我们可以不使用 DP 进行预处理,而是使用双指针来判断是否回文也能过。但是该题数据范围为 2000(数量级为 10ドル^3$),使用朴素做法判断是否回文的话,复杂度会去到 $O(n^3)$(计算量为 10ドル^9$),必然超时。 *
57+ * ** 快速判断「任意一段子串是否回文」思路 * *
6158
62- 因此我们不可能每次都使用双指针去线性扫描一遍 ` [i, j] ` 判断是否回文 。
59+ 剩下的问题是,我们如何快速判断连续一段 $ [ l, r ] $ 是否为回文串,做法和昨天的 [ 131. 分割回文串 ] ( https://mp.weixin.qq.com/s?__biz=MzU4NDE3MTEyMA==&mid=2247487047&idx=1&sn=117c48f20778868442fce44e100d2ea8&chksm=fd9ca558caeb2c4eb1bff4f0878ff796feabe523657c2aafea0b2d1c7026e1c0572ab1e6d205&token=635532356&lang=zh_CN#rd ) 一模一样 。
6360
64- 一个直观的做法是,我们先预处理除所有的 ` f[i][j] ` , ` f[i][j] ` 代表 ` [i, j] ` 这一段是否为回文串。
61+ * PS. 在 [ 131. 分割回文串 ] ( https://mp.weixin.qq.com/s?__biz=MzU4NDE3MTEyMA==&mid=2247487047&idx=1&sn=117c48f20778868442fce44e100d2ea8&chksm=fd9ca558caeb2c4eb1bff4f0878ff796feabe523657c2aafea0b2d1c7026e1c0572ab1e6d205&token=635532356&lang=zh_CN#rd ) ,数据范围只有 16ドル,ドル因此我们可以不使用 DP 进行预处理,而是使用双指针来判断是否回文也能过。但是该题数据范围为 2000ドル$(数量级为 10ドル^3$),使用朴素做法判断是否回文的话,复杂度会去到 $O(n^3)$(计算量为 10ドル^9$),必然超时。 *
6562
66- 预处理 ` f[i][j] ` 的过程可以用递推去做 。
63+ 因此我们不可能每次都使用双指针去线性扫描一遍 $ [ l, r ] $ 判断是否回文 。
6764
68- 要想 ` f[i][j] == true ` ,必须满足以下两个条件:
65+ 一个合理的做法是,我们先预处理出所有的 $g [ l ] [ r ] ,ドル$g [ l ] [ r ] $ 代表 $ [ l,r ] $ 这一段是否为回文串。
6966
70- 1 . ` f[i + 1][j - 1] == true `
71- 2 . ` s[i] == s[j] `
67+ 预处理 $g[ l] [ r ] $ 的过程可以用递推去做。
7268
73- 由于状态 ` f[i][j] ` 依赖于状态 ` f[i + 1][j - 1] ` ,因此需要我们左端点 ` i ` 是 ** 从大到小 ** 进行遍历;而右端点 ` j ` 是 ** 从小到大 ** 进行遍历。
69+ 要想 $g [ l ] [ r ] = true$ ,必须满足以下两个条件:
7470
75- 我们的遍历过程可以整理为:** 右端点 ` j ` 一直往右移动(从小到大),在 ` j ` 固定情况下,左端点 ` i ` 在 ` j ` 在左边开始,一直往左移动(从大到小)**
71+ 1 . $g[ l + 1] [ r - 1 ] = true$
72+ 2 . $s[ i] = s[ j] $
7673
77- 代码:
74+ 由于状态 $f [ l ] [ r ] $ 依赖于状态 $f [ l + 1 ] [ r - 1 ] ,ドル因此需要我们左端点 $l$ 是「从大到小」进行遍历;而右端点 $r$ 是「从小到大」进行遍历。
7875
79- ``` java []
76+ 因此最终的遍历过程可以整理为:** 右端点 $r$ 一直往右移动(从小到大),在 $r$ 固定情况下,左端点 $l$ 在 $r$ 在左边开始,一直往左移动(从大到小)**
77+ 78+ 代码:
79+ ``` Java []
8080class Solution {
8181 public int minCut (String s ) {
8282 int n = s. length();
8383 char [] cs = s. toCharArray();
8484
85- // 预处理出 st,st[i][j] 表示区间 [i,j] 是否为回文串
86- boolean [][] st = new boolean [n][n];
87- for (int j = 0 ; j < n; j ++ ) {
88- for (int i = j; i >= 0 ; i -- ) {
89- // 当 [i, j] 只有一个字符时,必然是回文串
90- if (i == j ) {
91- st[i][j ] = true ;
85+ // g[l][r] 代表 [l,r] 这一段是否为回文串
86+ boolean [][] g = new boolean [n+ 1 ][n+ 1 ];
87+ for (int r = 1 ; r <= n; r ++ ) {
88+ for (int l = r; l >= 1 ; l -- ) {
89+ // 如果只有一个字符,则[l,r]属于回文
90+ if (l == r ) {
91+ g[l][r ] = true ;
9292 } else {
93- // 当 [i, j] 长度为 2 时,满足 cs[i] == cs[j] 即回文串
94- if (j - i + 1 == 2 ) {
95- st[i][j] = cs[i] == cs[j];
96- 97- // 当 [i, j] 长度大于 2 时,满足 (cs[i] == cs[j] && f[i + 1][j - 1]) 即回文串
98- } else {
99- st[i][j] = cs[i] == cs[j] && st[i + 1 ][j - 1 ];
93+ // 在 l 和 r 字符相同的前提下
94+ if (cs[l - 1 ] == cs[r - 1 ]) {
95+ // 如果 l 和 r 长度只有 2;或者 [l+1,r-1] 这一段满足回文,则[l,r]属于回文
96+ if (r - l == 1 || g[l + 1 ][r - 1 ]) {
97+ g[l][r] = true ;
98+ }
10099 }
101100 }
102101 }
103102 }
104103
105- // f(i) 代表考虑下标为 i 的字符为结尾的最小分割次数
106- int [] f = new int [n];
107- for (int j = 1 ; j < n; j++ ) {
108- 109- // 如果 [0,j] 这一段直接构成回文,则无须分割
110- if (st[0 ][j]) {
111- f[j] = 0 ;
112- 113- // 如果无法直接构成回文
114- // 那么对于第 j 个字符,有使用分割次数,或者不使用分割次数两种选择
115- } else {
116- // 下边两种决策也能够合到一个循环当中去做,但是需要先将 f[i] 预设为一个足够大的数,因此干脆拆开来做
117- 118- // 独立使用一次分割次数
119- f[j] = f[j - 1 ] + 1 ;
120- 121- // 第 j 个字符本身不独立使用分割次数
122- // 代表要与前面的某个位置 i 形成区间 [i,j],使得 [i, j] 形成回文,[i, j] 整体消耗一次分割次数
123- for (int i = 1 ; i < j; i++ ) {
124- if (st[i][j]) {
125- f[j] = Math . min(f[j], f[i - 1 ] + 1 );
126- }
127- }
104+ // f[r] 代表将 [1,r] 这一段分割成若干回文子串所需要的最小分割次数
105+ int [] f = new int [n + 1 ];
106+ for (int r = 1 ; r <= n; r++ ) {
107+ // 如果 [1,r] 满足回文,不需要分割
108+ if (g[1 ][r]) {
109+ f[r] = 0 ;
110+ } else {
111+ // 先设定一个最大分割次数(r 个字符最多消耗 r - 1 次分割)
112+ f[r] = r - 1 ;
113+ // 在所有符合 [l,r] 回文的方案中取最小值
114+ for (int l = 1 ; l <= r; l++ ) {
115+ if (g[l][r]) f[r] = Math . min(f[r], f[l - 1 ] + 1 );
116+ }
128117 }
129118 }
130- return f[n - 1 ];
119+ 120+ return f[n];
131121 }
132122}
133123```
134124* 时间复杂度:$O(n^2)$
135125* 空间复杂度:$O(n^2)$
136126
137- ***
127+ ---
138128
139129### 关于「如何确定 DP 状态定义」的分享
140130
@@ -146,11 +136,11 @@ DP 的状态定义,基本上是考经验的(猜的),猜对了 DP 的状
146136
147137虽然大多数情况都是猜的,但也不是毫无规律,相当一部分是定义是与「结尾」和「答案」有所关联的。
148138
149- ** 例如本题定义 f[ i] 为以下标为 i 的字符作为结尾(结尾)的最小分割次数(答案)。**
139+ ** 例如本题定义 $ f[ i] $ 为以下标为 $i$ 的字符作为结尾(结尾)的最小分割次数(答案)。**
150140
151141因此对于那些你没见过的 DP 模型题,可以从这两方面去「猜」。
152142
153- ***
143+ ---
154144
155145### Manacher 算法(非重要补充)
156146
@@ -174,7 +164,7 @@ DP 的状态定义,基本上是考经验的(猜的),猜对了 DP 的状
174164
175165在这个系列文章里面,除了讲解解题思路以外,还会尽可能给出最为简洁的代码。如果涉及通解还会相应的代码模板。
176166
177- 为了方便各位同学能够电脑上进行调试和提交代码,我建立了相关的仓库:https://github.com/SharingSource/LogicStack-LeetCode。
167+ 为了方便各位同学能够电脑上进行调试和提交代码,我建立了相关的仓库:https://github.com/SharingSource/LogicStack-LeetCode 。
178168
179169在仓库地址里,你可以看到系列文章的题解链接、系列文章的相应代码、LeetCode 原题链接和其他优选题解。
180170
0 commit comments