当前位置: 首页 > news >正文

回溯算法学习笔记(《代码随想录》)

回溯算法学习笔记(《代码随想录》)

本文整理了《代码随想录》中回溯算法的核心题型,包含组合、分割、子集、排列四大类问题,每个题型均标注要点与完整代码,方便对比学习。

一、组合类问题

组合问题的核心是 “选元素、不重复、按顺序”,需重点关注startIndex的使用与剪枝优化,避免无效遍历。

1.1 基础组合(LeetCode 77)

要点
  • 掌握回溯算法的基础模板:定义结果集res与路径集path,通过递归遍历所有可能。
  • 每次递归找到符合长度k的路径后,需创建新列表存入结果(避免引用问题),并回溯撤销最后一个元素。
代码
class Solution {// 定义两个全局变量,存储最终结果与当前路径List<List<Integer>> res = new ArrayList<>();List<Integer> path = new ArrayList<>();public List<List<Integer>> combine(int n, int k) {backtracking(n, k, 1);return res;}// startIndex:控制遍历起始位置,避免重复组合public void backtracking(int n, int k, int startIndex) {// 终止条件:当前路径长度等于目标长度kif (path.size() == k) {res.add(new ArrayList(path)); // 存入新列表,防止后续修改影响结果return;}// 遍历所有可能的元素,从startIndex开始for (int i = startIndex; i <= n; i++) {path.add(i); // 加入当前元素到路径backtracking(n, k, i + 1); // 递归,下一轮从i+1开始(不重复选)path.remove(path.size() - 1); // 回溯:撤销最后一个元素,尝试其他可能}}
}

1.2 组合优化(剪枝)

要点
  • 核心优化点:缩小for循环的遍历范围,减少无效递归。
  • 剪枝逻辑:若剩余可选择的元素数量(n - i + 1)小于 “还需选择的元素数量(k - path.size())”,则无需继续遍历,直接终止。
关键代码(仅修改 for 循环条件)
// 剪枝后的for循环:i的上限从n改为 n - (k - path.size()) + 1
for (int i = startIndex; i <= n - (k - path.size()) + 1; i++) { path.add(i);backtracking(n, k, i + 1);path.remove(path.size() - 1);
}

1.3 组合总和 III(LeetCode 216)

要点
  • 在基础组合的基础上,增加 “总和判断”:终止条件需同时满足 “路径长度为k” 和 “路径元素和为n”。
  • 双重剪枝:
    1. 若当前路径和已大于n,直接终止递归(提前返回)。
    2. for循环中通过9 - (k - path.size()) + 1限制遍历范围(元素仅 1-9)。
代码
class Solution {List<List<Integer>> res = new ArrayList<>();List<Integer> path = new ArrayList<>();public List<List<Integer>> combinationSum3(int k, int n) {backtracking(k, n, 1, 0);return res;}// sum:当前路径的元素和public void backtracking(int k, int n, int startIndex, int sum) {// 剪枝1:总和已超过目标n,无需继续if (sum > n) return;// 终止条件:路径长度为k且总和等于nif (path.size() == k) {if (sum == n) res.add(new ArrayList<>(path));return;}// 剪枝2:限制i的上限(元素仅1-9)for (int i = startIndex; i <= 9 - (k - path.size()) + 1; i++) {sum += i; // 累加当前元素path.add(i);backtracking(k, n, i + 1, sum);sum -= i; // 回溯:撤销总和path.remove(path.size() - 1); // 回溯:撤销元素}}
}

1.4 电话号码的字母组合(LeetCode 17)

要点
  • 需先建立 “数字 - 字母” 映射表(如2→abc),将输入的数字字符串转为对应字母组合。
  • 递归参数用index记录当前处理的数字位置,终止条件为index等于数字字符串长度。
  • 使用StringBuilder存储路径(比List<Character>更高效,便于增删)。
代码
class Solution {// 存储最终结果与当前路径(用StringBuilder优化操作)List<String> res = new ArrayList<>();StringBuilder str = new StringBuilder();public List<String> letterCombinations(String digits) {// 边界条件:输入为空字符串,直接返回空结果if (digits == null || digits.length() == 0) return res;// 数字-字母映射表(索引0-1对应空字符串,2-9对应实际字母)String[] numString = {"", "", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz"};backtracking(digits, numString, 0);return res;}// index:当前处理的数字在digits中的索引public void backtracking(String digits, String[] numString, int index) {// 终止条件:处理完所有数字,将当前路径存入结果if (index == digits.length()) {res.add(str.toString());return;}// 获取当前数字对应的字母字符串(如digits[index]为'2',则temp为"abc")String temp = numString[digits.charAt(index) - '0']; // 字符转数字:'2'-'0'=2// 遍历当前数字对应的所有字母for (int i = 0; i < temp.length(); i++) {str.append(temp.charAt(i)); // 加入当前字母backtracking(digits, numString, index + 1); // 处理下一个数字str.deleteCharAt(str.length() - 1); // 回溯:删除最后一个字母}}
}

1.5 组合总和(LeetCode 39)

要点
  • 与基础组合的区别:元素可重复使用,且无固定选择数量。
  • 关键调整:递归时startIndex不变(允许重复选当前元素),而非i+1
  • 终止条件:当前路径和等于target时存入结果;若和大于target,直接返回(剪枝)。
代码
class Solution {List<List<Integer>> res = new ArrayList<>();List<Integer> path = new ArrayList<>();public List<List<Integer>> combinationSum(int[] candidates, int target) {if (candidates == null) return res;backtracking(candidates, target, 0, 0);return res;}// sum:当前路径和;startIndex:遍历起始位置(控制重复选)public void backtracking(int[] candidates, int target, int sum, int startIndex) {// 终止条件:总和等于targetif (sum == target) {res.add(new ArrayList(path));return;}// 剪枝:总和已超过target,无需继续if (sum > target) return;// 遍历候选数组,从startIndex开始(允许重复选当前元素)for (int i = startIndex; i < candidates.length; i++) {path.add(candidates[i]);sum += candidates[i];backtracking(candidates, target, sum, i); // 此处startIndex为i,而非i+1sum -= candidates[i]; // 回溯:撤销总和path.remove(path.size() - 1); // 回溯:撤销元素}}
}

1.6 组合总和 II(LeetCode 40)

要点
  • 核心挑战:数组含重复元素,需去重(避免出现重复组合),且每个元素仅用一次。
  • 去重思路:
    1. 先对数组排序(使重复元素相邻)。
    2. 两种去重方式:
      • used数组:i>0 && candidates[i]==candidates[i-1] && !used[i-1](树层去重)。
      • 不用used数组:i>startIndex && candidates[i]==candidates[i-1](更简洁,本质是跳过同一层的重复元素)。
代码(用 used 数组去重)
class Solution {List<List<Integer>> res = new ArrayList<>();List<Integer> path = new ArrayList<>();boolean[] used; // 记录元素是否已使用,用于去重public List<List<Integer>> combinationSum2(int[] candidates, int target) {Arrays.sort(candidates); // 排序:使重复元素相邻,便于去重used = new boolean[candidates.length];Arrays.fill(used, false); // 初始化used数组backtracking(candidates, target, 0, 0);return res;}public void backtracking(int[] candidates, int target, int sum, int startIndex) {if (sum == target) {res.add(new ArrayList(path));return;}if (sum > target) return; // 剪枝for (int i = startIndex; i < candidates.length; i++) {// 去重:同一层中,当前元素与前一个元素相同且前一个未使用(树层去重)if (i > 0 && candidates[i] == candidates[i - 1] && !used[i - 1]) {continue;}used[i] = true; // 标记当前元素已使用path.add(candidates[i]);sum += candidates[i];backtracking(candidates, target, sum, i + 1); // 元素仅用一次,startIndex为i+1used[i] = false; // 回溯:取消标记sum -= candidates[i]; // 回溯:撤销总和path.remove(path.size() - 1); // 回溯:撤销元素}}
}

二、分割类问题

分割问题的核心是 “按规则分割字符串”,需通过startIndex控制分割起始位置,同时判断分割后的子串是否符合条件(如回文、IP 规范)。

2.1 分割回文串(LeetCode 131)

要点
  • 需额外实现check方法,判断子串是否为回文串。
  • 递归逻辑:从startIndex开始分割,若分割后的子串是回文串,则加入路径,继续分割剩余部分。
  • 终止条件:startIndex等于字符串长度(分割线到达字符串末尾)。
代码
class Solution {List<List<String>> res = new ArrayList<>();List<String> path = new ArrayList<>();public List<List<String>> partition(String s) {backtracking(s, 0, new StringBuilder());return res;}// sb:临时存储当前分割的子串public void backtracking(String s, int startIndex, StringBuilder sb) {// 终止条件:分割线到达字符串末尾if (startIndex == s.length()) {res.add(new ArrayList(path));return;}// 从startIndex开始,尝试所有可能的分割位置for (int i = startIndex; i < s.length(); i++) {sb.append(s.charAt(i)); // 拼接当前字符,形成子串// 若当前子串是回文串,加入路径并继续分割if (check(sb)) {path.add(sb.toString());backtracking(s, i + 1, new StringBuilder()); // 下一轮从i+1开始分割path.remove(path.size() - 1); // 回溯:撤销路径}}}// 判断字符串是否为回文串public boolean check(StringBuilder sb) {for (int i = 0; i < sb.length() / 2; i++) {// 对称位置字符不相等,不是回文串if (sb.charAt(i) != sb.charAt(sb.length() - i - 1)) {return false;}}return true;}
}

2.2 复原 IP 地址(LeetCode 93)

要点
  • IP 地址规则:由 4 段数字组成,每段 0-255,且不能以 0 开头(除非段内只有 0,如 “0” 合法,“01” 不合法)。
  • 递归参数:pointSum记录已添加的小数点数量,终止条件为pointSum==3(需判断最后一段是否符合 IP 规则)。
  • 剪枝:输入字符串长度超过 12(如 “1234567890123”)时,直接返回空结果(IP 最长为 12 位,如 “255.255.255.255”)。
代码
class Solution {List<String> res = new ArrayList<>();public List<String> restoreIpAddresses(String s) {// 剪枝:IP地址最长12位(4段×3位),超过直接返回if (s.length() > 12) {return res;}backtracking(s, 0, 0);return res;}// pointSum:已添加的小数点数量public void backtracking(String s, int startIndex, int pointSum) {// 终止条件:已添加3个小数点,判断最后一段是否合法if (pointSum == 3) {if (isVal(s, startIndex, s.length() - 1)) {res.add(s);}return;}// 遍历分割位置,判断当前段是否合法for (int i = startIndex; i < s.length(); i++) {if (isVal(s, startIndex, i)) {// 在i后添加小数点(字符串拼接)s = s.substring(0, i + 1) + '.' + s.substring(i + 1);pointSum += 1;// 下一轮从i+2开始(跳过小数点)backtracking(s, i + 2, pointSum);// 回溯:删除小数点s = s.substring(0, i + 1) + s.substring(i + 2);pointSum -= 1;} else {// 当前段不合法,后续分割也不合法,直接终止循环break;}}}// 判断s[startIndex..end]是否符合IP段规则public boolean isVal(String s, int startIndex, int end) {if (startIndex > end) return false;// 规则1:不能以0开头(除非段内只有0)if (s.charAt(startIndex) == '0' && startIndex != end) {return false;}// 规则2:数字范围0-255,且无非法字符(非数字)int num = 0;for (int i = startIndex; i <= end; i++) {// 非法字符(非0-9)if (s.charAt(i) > '9' || s.charAt(i) < '0') {return false;}num = num * 10 + (s.charAt(i) - '0');// 数字超过255if (num > 255) {return false;}}return true;}
}

三、子集类问题

子集问题的核心是 “收集所有可能的子集”,与组合 / 分割的区别是:每个递归节点的路径都需存入结果(而非仅终止条件时存入)。

3.1 基础子集(LeetCode 78)

要点
  • 无需额外终止条件(除startIndex越界时返回),每次递归先将当前路径存入结果。
  • 遍历逻辑与组合类似,通过startIndex控制不重复选元素(子集是无序的,如[1,2][2,1]是同一个子集)。
代码
class Solution {List<List<Integer>> res = new ArrayList<>();List<Integer> path = new ArrayList<>();public List<List<Integer>> subsets(int[] nums) {backtracking(nums, 0);return res;}public void backtracking(int[] nums, int startIndex) {// 关键:每个节点的路径都存入结果(子集)res.add(new ArrayList(path));// 终止条件:startIndex越界,无元素可选if (startIndex >= nums.length) return;// 遍历所有可能的元素,从startIndex开始for (int i = startIndex; i < nums.length; i++) {path.add(nums[i]);backtracking(nums, i + 1); // 下一轮从i+1开始(不重复选)path.remove(path.size() - 1); // 回溯:撤销元素}return;}
}

3.2 子集 II(LeetCode 90)

要点
  • 数组含重复元素,需去重(避免重复子集,如[1,2]出现多次)。
  • 去重步骤:
    1. 先对数组排序(使重复元素相邻)。
    2. 遍历中判断:i>startIndex && nums[i]==nums[i-1]时,跳过当前元素(同一层的重复元素,避免重复子集)。
代码
class Solution {List<List<Integer>> res = new ArrayList<>();List<Integer> path = new ArrayList<>();public List<List<Integer>> subsetsWithDup(int[] nums) {Arrays.sort(nums); // 排序:便于去重backtracking(nums, 0);return res;}public void backtracking(int[] nums, int startIndex) {// 每个节点的路径都存入结果res.add(new ArrayList(path));if (startIndex >= nums.length) return;for (int i = startIndex; i < nums.length; i++) {// 去重:同一层中,当前元素与前一个元素相同,跳过if (i > startIndex && nums[i] == nums[i - 1]) {continue;}path.add(nums[i]);backtracking(nums, i + 1);path.remove(path.size() - 1); // 回溯:撤销元素}return;}
}

3.3 递增子序列(LeetCode 491)

要点
  • 需满足两个条件:子序列长度≥2,且元素严格递增(非递减)。
  • 去重与递增控制:
    1. HashSet记录当前层已使用的元素(避免同一层重复选择,如[4,6,7,7]中第二个 7 跳过)。
    2. 递增判断:若路径非空,当前元素需≥路径最后一个元素(否则跳过,不满足递增)。
代码
class Solution {List<List<Integer>> res = new ArrayList<>();List<Integer> path = new ArrayList<>();public List<List<Integer>> findSubsequences(int[] nums) {backtracking(nums, 0);return res;}public void backtracking(int[] nums, int startIndex) {// 终止条件:路径长度≥2,存入结果(不终止递归,继续收集更长的子序列)if (path.size() >= 2) {res.add(new ArrayList<>(path));}// 用HashSet记录当前层已使用的元素,避免重复子序列HashSet<Integer> hs = new HashSet<>();for (int i = startIndex; i < nums.length; i++) {// 跳过条件:1.不满足递增;2.当前元素已在当前层使用过if (!path.isEmpty() && path.get(path.size() - 1) > nums[i] || hs.contains(nums[i])) {continue;}hs.add(nums[i]); // 标记当前元素在当前层已使用path.add(nums[i]);backtracking(nums, i + 1); // 下一轮从i+1开始(不重复选)path.remove(path.size() - 1); // 回溯:撤销元素}}
}

四、排列类问题

排列问题的核心是 “元素无顺序要求,每个元素仅用一次”,与组合的区别是:递归时从 0 开始遍历,用used数组标记已使用的元素(而非startIndex)。

4.1 基础全排列(LeetCode 46)

要点
  • startIndex,每次遍历从 0 开始(排列是有序的,如[1,2][2,1]是不同排列)。
  • used数组记录元素是否已使用,避免重复选择(如nums[0]已用,则跳过)。
  • 终止条件:路径长度等于数组长度(收集到一个完整排列)。
代码
class Solution {List<List<Integer>> res = new ArrayList<>();List<Integer> path = new ArrayList<>();boolean[] used; // 标记元素是否已使用public List<List<Integer>> permute(int[] nums) {used = new boolean[nums.length];backtracking(nums, used);return res;}public void backtracking(int[] nums, boolean[] used) {// 终止条件:路径长度等于数组长度(完整排列)if (path.size() == nums.length) {res.add(new ArrayList<>(path));return;}// 从0开始遍历所有元素(排列无序)for (int i = 0; i < nums.length; i++) {if (used[i] == true) continue; // 元素已使用,跳过used[i] = true; // 标记当前元素已使用path.add(nums[i]);backtracking(nums, used);used[i] = false; // 回溯:取消标记path.remove(path.size() - 1); // 回溯:撤销元素}}
}

4.2 全排列 II(LeetCode 47)

要点
  • 数组含重复元素,需去重(避免重复排列,如[1,1,2]的重复排列)。
  • 去重步骤:
    1. 先对数组排序(使重复元素相邻)。
    2. 去重条件:i>0 && nums[i]==nums[i-1] && !used[i-1](树层去重,避免同一层重复选择)。
    3. 同时保留used[i]==true的判断(避免重复使用同一元素)。
代码
class Solution {List<List<Integer>> res = new ArrayList<>();List<Integer> path = new ArrayList<>();boolean[] used;public List<List<Integer>> permuteUnique(int[] nums) {Arrays.sort(nums); // 排序:便于去重used = new boolean[nums.length];backtracking(nums, used);return res;}public void backtracking(int[] nums, boolean[] used) {if (path.size() == nums.length) {res.add(new ArrayList<>(path));return;}for (int i = 0; i < nums.length; i++) {// 去重:同一层中,当前元素与前一个元素相同且前一个未使用(树层去重)if (i > 0 && nums[i] == nums[i - 1] && used[i - 1] == false) {continue;}// 元素已使用,跳过if (used[i] == true) {continue;}used[i] = true;path.add(nums[i]);backtracking(nums, used);used[i] = false;path.remove(path.size() - 1);}}
}

回溯算法核心模板总结

所有回溯问题均可基于以下模板扩展,关键区别在于终止条件遍历范围去重 / 剪枝逻辑

// 1. 定义全局/成员变量(结果集、路径集)
List<结果类型> res = new ArrayList<>();
路径类型 path = new 路径类型();// 2. 主方法(初始化并调用回溯)
public 结果类型 solve(输入参数) {backtracking(输入参数, 辅助参数); // 辅助参数如startIndex、used、sum等return res;
}// 3. 回溯方法
public void backtracking(输入参数, 辅助参数) {// 终止条件:判断是否收集结果if (终止条件) {res.add(new 结果类型(path)); // 注意深拷贝return;}// 遍历所有可能的选择for (int i = 起始位置; i < 遍历范围; i++) {// 剪枝/去重:跳过无效选择if (剪枝/去重条件) {continue;}// 选择:将当前元素加入路径path.add(当前元素);// 递归:处理下一层backtracking(输入参数, 新辅助参数); // 新辅助参数如i+1、used更新等// 回溯:撤销选择path.remove(path.size() - 1);}
}
http://www.dtcms.com/a/446739.html

相关文章:

  • 深圳外贸网站推广公司站酷网下载
  • 第十八周周报
  • 漳州企业网站建设制作购物型网站模板
  • 惠州网站建设php网站开发试题
  • MySQL复制拓扑管理核心知识点总结
  • 【含文档+PPT+源码】基于Java的宠物医院管理系统的设计与实现
  • 关于网站开发的技术博客小程序代理加盟前景
  • 松江网站开发培训班网站中英文域名
  • 4-8〔O҉S҉C҉P҉ ◈ 研记〕❘ WEB应用攻击▸命令注入漏洞
  • 【Linux网络】IP协议
  • 做网站流行的网站做流量推广的方式
  • 网站如何做淘宝客网站建设简介是什么意思
  • 家具网站 模板禅城网站建设
  • 子目录创建网站网站建设钅金手指排名十五
  • 网站建设项目需求分析深圳宣传片制作排名前十名
  • 计算机网络(四):数据链路层(功能概述、组帧/封装成帧、差错控制、流量控制与可靠传输机制)
  • C++ 面试总结
  • Netty面试重点-1
  • php 8.4.8 更新日志
  • 高明网站设计制作建造师
  • 网站建设哪家公司好一点59做网站
  • JavaWeb基础,Spring框架核心:IOC与AOP解析
  • P13978题解
  • Easyx使用(番外篇)
  • 【LaTeX】 10 LaTeX 数学公式笔记
  • 早熟收敛(Premature Convergence):遗传算法中的局部最优陷阱
  • 设计网站平台风格网站扫码怎么做的
  • 【Redis】免费Redis图形化客户端全攻略
  • Socket网络编程(1)——Echo Server
  • 怎么自己做企业网站建网站 京公网安