diff --git a/content/post/算法题/dfs回溯.md b/content/post/算法题/dfs回溯.md index e9348b5..dc204c9 100644 --- a/content/post/算法题/dfs回溯.md +++ b/content/post/算法题/dfs回溯.md @@ -1,6 +1,6 @@ --- title: dfs回溯 -description: dfs回溯问题:组合问题、排列问题、回文子串分割 +description: dfs回溯问题:组合问题、排列问题、回文子串分割的所有方案 date: 2025-05-07T01:23:00+08:00 slug: dfs回溯 # image: helena-hertz-wWZzXlDpMog-unsplash.jpg @@ -19,7 +19,7 @@ math: true ## 问题描述 -给定一个无重复元素的数组`nums`和一整数`m`,返回所有可能的`m`个数的组合。 +给定一个无重复元素的数组`num`和一整数`m`,返回所有可能的`m`个数的组合。 ## 代码实现 @@ -125,7 +125,7 @@ public static void dfs(int[] num, int m, int start, List path, List 0$:这意味着最后一块回文子串是 $s[j..i]$。前面的子串是 $s[0..j-1]$。将 $s[0..i]$ 分割的次数就是将 $s[0..j-1]$ 分割的最小次数 $dp[j-1]$,再加上索引 $j-1$ 后面这一刀,所以是 $dp[j-1] + 1$ 次切割。 + +我们要求的是**最小**切割次数 $dp[i]$,所以需要考虑所有可能的起始索引 $j$ ($0 \le j \le i$),只要 $s[j..i]$ 是回文串,它就提供了一种可能的分割方案。$dp[i]$ 就是所有这些有效方案中切割次数的最小值。 + +因此,状态转移方程可以写为: + +$$dp[i] = \min_{0 \le j \le i \atop s[j..i] \text{ 是回文串}} \{ \text{cost}(j, i) \}$$ + +其中,$\text{cost}(j, i)$ 是以 $s[j..i]$ 作为最后一块回文子串时的总切割次数,它是一个分段函数: + +$$ +\begin{cases} +0 & \text{if } j = 0, \\ +dp[j-1] + 1 & \text{if } j > 0 +\end{cases} +$$ + +将 $\text{cost}(j, i)$ 代入方程: + +$$dp[i] = \min \left( \begin{cases} 0 & \text{if } 0 \le j \le i \text{ and } s[j..i] \text{ is palindrome and } j = 0, \\ dp[j-1] + 1 & \text{if } 0 \le j \le i \text{ and } s[j..i] \text{ is palindrome and } j > 0 \end{cases} \right)$$ + +这个分段 min 函数可以简化表达。我们是在所有满足 $s[j..i]$ 是回文串的 $j$ 值中取最小值。如果 $j=0$ 且 $s[0..i]$ 是回文,最小就是 0。否则,如果存在其他 $j>0$ 使得 $s[j..i]$ 是回文,我们就取所有 $dp[j-1]+1$ 中的最小值,然后和可能存在的 0 比较。 + +更简洁的表达方式是: + +对于 $i = 0, 1, \ldots, n-1$: +$$dp[i] = \min_{0 \le j \le i \atop s[j..i] \text{ 是回文串}} \begin{cases} 0 & \text{if } j = 0, \\ dp[j-1] + 1 & \text{if } j > 0 \end{cases}$$ + +**基本情况:** + +$dp[0]$ 的计算:$i=0$。可能的 $j$ 只有 $0$。$s[0..0]$ 是回文串。$j=0$,cost 是 $0$。 +$dp[0] = \min(\{0 \mid j=0, s[0..0] \text{ 是回文} \}) = 0$。 + +在代码中,`dp[i] = i` 的初始化给 `dp[i]` 提供了一个初始上界(最坏情况下,$s[0..i]$ 需要 $i$ 次切割,即每次切割一个字符),然后通过内层循环不断用 `dp[j-1] + 1` 或 0 来尝试更新 `dp[i]` 取到更小值。 + +**需要注意:** + +* 为了使这个 DP 高效,判断 "s[j..i] 是回文串" 需要是 O(1) 操作,这依赖于我们提前进行的 O(n^2) 预处理。 +* DP 计算的顺序是 $i$ 从 $0$ 到 $n-1$,确保在计算 $dp[i]$ 时,$dp[j-1]$ (其中 $j-1 < i$) 的值已经被计算出来。 + +因此我们不能使用上文中的回文串判断方法,而是需要使用动态规划预处理出所有子串是否是回文串。我们建一个二维数组 `isPalindrome`,`isPalindrome[i][j]` 表示 $s[i..j]$ 是否是回文串。根据回文串的性质: + +> 一个字符串是回文串,当且仅当它的首尾字符相同,并且去掉首尾,中间的子串也是回文串。 + +因此我们很自然地分3种情况: + +- 单个字符,显然是回文串。 +- 两个字符,如果相同,则是回文串。 +- 多个字符,如果首尾字符相同,并且去掉首尾后中间的子串也是回文串,则它是回文串。 + +完整代码: + +```java +public static int minCut(String s) { + int len = s.length(); + // 首先构建回文子串表 + boolean[][] isPalindrome = new boolean[len][len]; + // 单个字符肯定是回文 + for(int i=0;i