diff --git a/content/post/算法题/dfs回溯.md b/content/post/算法题/dfs回溯.md new file mode 100644 index 0000000..e9348b5 --- /dev/null +++ b/content/post/算法题/dfs回溯.md @@ -0,0 +1,260 @@ +--- +title: dfs回溯 +description: dfs回溯问题:组合问题、排列问题、回文子串分割 +date: 2025-05-07T01:23:00+08:00 +slug: dfs回溯 +# image: helena-hertz-wWZzXlDpMog-unsplash.jpg +categories: + - 算法题 +tags: [ + "dfs", + "回溯", + "递归", + "算法", + "回文串" +] +math: true +--- +# 组合问题 + +## 问题描述 + +给定一个无重复元素的数组`nums`和一整数`m`,返回所有可能的`m`个数的组合。 + +## 代码实现 + +咱们的目标是用深度优先搜索(DFS)配合回溯策略,来找出所有可能的数字组合。 + +**`combination` 方法都干了啥:** +1. **准备一个`result`列表**: 这个列表 (`List>`) 用来装所有找出来的组合。每个组合它自己也是一个整数列表。 +2. **再准备一个`path`列表**: 这个列表 (`List`) 临时记录咱们在DFS过程中,当前正在凑的这个组合长啥样。 +3. **调用`dfs`干活**: `dfs`是真正干搜索和凑组合的递归函数。 + * `num`: 就是输入的那个原始数组。 + * `m`: 咱们要凑的组合里有几个数。 + * `0`: 从数组的第0个位置开始选数。 + * `path`: 当前凑到一半的组合。 + * `result`: 最终装结果的那个列表。 +4. **返回`result`**: 等`dfs`跑完了,`result`里面就装满了所有大小为`m`的组合,把它返回就行。 + +**`dfs` (深度优先搜索) 是回溯算法的精髓所在。** +1. **啥时候停呢?(递归的出口)**: + * `if(path.size()==m)`: 如果当前`path`列表里的数字个数,正好等于咱们想要的组合大小`m`,那说明一个组合凑齐了。 + * `result.add(new ArrayList(path))`: 赶紧把这个凑好的`path`存到`result`里。**特别注意**:这里一定要 `new ArrayList<>(path)` 创建一个新的列表再存进去。为啥呢?因为`path`这个列表在后面的回溯步骤里还会被修改(删掉元素)。如果不创建副本,那`result`里存的所有组合都会指向同一个不断变化的`path`对象,最后结果就全乱套了。 + * `return;`: 找到了一个组合,这条递归的路就走到头了,返回。 + +2. **递归探索和"反悔"(回溯)的过程**: + * `for(int i=start; i> combination(int[] num, int m){ + List> result = new ArrayList<>(); + List path = new ArrayList<>(); + dfs(num,m,0,path,result); + + return result; + +} + +// 深度优先搜索 +public static void dfs(int[] num, int m, int start, List path, List> result) { + if(path.size()==m) { + result.add(new ArrayList(path)); + return; + } + + for(int i=start;i> permutation(int[] num, int m){ + boolean[] visited = new boolean[num.length]; + List path = new ArrayList<>(); + List> result = new ArrayList<>(); + backtrack(num,m,visited,path,result); + return result; +} + +// 回溯 +public static void backtrack(int[] num, int m, boolean[] visited, List path, List> result) { + if(m==path.size()) { + result.add(new ArrayList<>(path)); + return; + } + for(int i=0;i path, List> results)` 这个函数是核心**: + * `s`:就是咱们要切的那个原字符串。 + * `start`:这是个数字,告诉我们当前要从字符串 `s` 的第几个字符开始找下一段回文子串。 + * `path`:一个临时的 `List`,像个小本本,记录着咱们当前这一趟切下来的一串回文子串是哪些。 + * `results`:一个 `List>`,这是最终的成果篮子,所有成功的切法(每一套切法都是一个回文子串列表)都会装到这里面。 + +2. **啥时候算切完了呢?(递归的出口)**: + * 当 `start` 这个数字等于字符串 `s` 的总长度 `s.length()` 的时候,就说明咱们已经从头到尾把整个字符串 `s` 都成功切成了回文串啦! + * 这时候,`path` 小本本里记的就是一套完整的、成功的切割方案。赶紧把它存到 `results` 成果篮子里。**特别注意**:这里一定要 `new ArrayList<>(path)` 创建一个新的列表再存进去。为啥呢?因为`path`这个小本本在后面的回溯步骤里还会被修改(删掉里面的子串)。如果不创建副本,那`results`里存的所有方案都会指向同一个不断变化的`path`对象,最后结果就全乱套了。 + +3. **探索和"反悔"(回溯)的过程是咋样的**: + * 函数里有个 `for` 循环,它会从当前的 `start` 位置开始,一直到字符串末尾,尝试所有可能的切割点 `i`。 + * 对于每一个可能的切割点 `i`,咱们就先切出一段子串 `substr = s.substring(start, i + 1)`。这段 `substr` 就是从当前 `start` 位置开始,到 `i` 位置(包括 `i` 本身)结束的这么一段。 + * **第一步,判断一下是不是回文**:喊小助手 `isPalindrome(substr)` 来帮忙看看这段 `substr` 是不是个回文串。 + * **如果 `substr` 确实是回文串,那接下来**: + * **做选择 (选它!)**:把它加到咱们的 `path` 小本本里:`path.add(substr)`。 + * **继续往下切 (深入探索)**:递归调用 `dfs(s, i + 1, path, results)`。注意,新的起始点变成了 `i + 1`,因为 `substr` 已经把从 `start` 到 `i` 这些字符给占了,下一次就从 `i` 的下一个位置开始切。 + * **反悔操作 (撤销选择/回溯)**:当上面那行 `dfs` 调用结束返回了,说明基于当前这个 `substr` 开头的所有后续切法都已经试完了。为了能试试其他的可能性(比如,从同一个 `start` 位置开始,尝试切一个更长的回文子串;或者,在更早的切割步骤里,选择一个不一样的初始子串),咱们就得把刚才加入 `path` 的那个 `substr` 给删掉:`path.remove(path.size() - 1)`。这个操作就像是"撤销"了刚才的选择,程序退回到选 `substr` 之前的状态。 + +4. **`isPalindrome(String s)`**: + * 这个函数很简单,就是专门判断一个字符串 `s` 是不是回文的。 + * 我们用"双指针"的方法:一个指针指着字符串的开头,另一个指着字符串的末尾。两个指针同时往中间跑,一边跑一边比较它们指着的字符是不是一样。如果跑到中间或者相遇了,所有对应位置的字符都一样,那这个字符串就是回文的。 + * (题目通常会说,空字符串或单个字符的串也算回文——代码里也通常会处理这种情况)。 + +通过这种"一路切下去,不合适就退回来换条路再切"的深度优先搜索和回溯机制,这个算法就能把所有可能的切割方式都系统地试一遍,然后把那些每一段都是回文串的成功方案都收集起来。 + + +```java +public static List> divide(String s){ + List path = new ArrayList<>(); + List> results = new ArrayList<>(); + dfs(s, 0, path, results); + return results; +} + +public static void dfs(String s, int start, List path, List> results) { + if(start == s.length()) { // 此时start应该已经移到字符串末字符的下一个位置了,所以不是length-1 + results.add(new ArrayList<>(path)); // 必须添加一个副本而不是引用本身 + } + + for(int i=start;i