代码随想录——回溯算法
组合#
题目 中等
class Solution {
List<List<Integer>> result = new ArrayList<>();
LinkedList<Integer> path = new LinkedList<>();
public List<List<Integer>> combine(int n, int k) {
combineHelper(n, k, 1);
return result;
}
/**
* 每次从集合中选取元素,可选择的范围随着选择的进行而收缩,调整可选择的范围,就是要靠startIndex
* @param startIndex 用来记录本层递归的中,集合从哪里开始遍历(集合就是[1,...,n] )。
*/
private void combineHelper(int n, int k, int startIndex){
//终止条件
if (path.size() == k){
result.add(new ArrayList<>(path));
return;
}
for (int i = startIndex; i <= n - (k - path.size()) + 1; i++){
path.add(i);
combineHelper(n, k, i + 1);
path.removeLast();
}
}
}
组合总和 III#
题目 中等
class Solution {
List<List<Integer>> result = new ArrayList<>();
LinkedList<Integer> path = new LinkedList<>();
public List<List<Integer>> combinationSum3(int k, int n) {
backTracking(n, k, 1, 0);
return result;
}
private void backTracking(int targetSum, int k, int startIndex, int sum) {
// 剪枝
if (sum > targetSum) {
return;
}
if (path.size() == k) {
if (sum == targetSum) result.add(new ArrayList<>(path));
return;
}
// 剪枝 9 - (k - path.size()) + 1
for (int i = startIndex; i <= 9 - (k - path.size()) + 1; i++) {
path.add(i);
sum += i;
backTracking(targetSum, k, i + 1, sum);
//回溯
path.removeLast();
//回溯
sum -= i;
}
}
}
电话号码的字母组合#
题目 中等
class Solution {
//设置全局列表存储最后的结果
List<String> list = new ArrayList<>();
public List<String> letterCombinations(String digits) {
if (digits == null || digits.length() == 0) {
return list;
}
//初始对应所有的数字,为了直接对应2-9,新增了两个无效的字符串""
String[] numString = {"", "", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz"};
//迭代处理
backTracking(digits, numString, 0);
return list;
}
//每次迭代获取一个字符串,所以会设计大量的字符串拼接,所以这里选择更为高效的 StringBuild
StringBuilder temp = new StringBuilder();
//比如digits如果为"23",num 为0,则str表示2对应的 abc
public void backTracking(String digits, String[] numString, int num) {
//遍历全部一次记录一次得到的字符串
if (num == digits.length()) {
list.add(temp.toString());
return;
}
//str 表示当前num对应的字符串
String str = numString[digits.charAt(num) - '0'];
for (int i = 0; i < str.length(); i++) {
temp.append(str.charAt(i));
//c
backTracking(digits, numString, num + 1);
//剔除末尾的继续尝试
temp.deleteCharAt(temp.length() - 1);
}
}
}
组合总和#
题目 中等
在求和问题中,排序之后加剪枝是常见的套路!
// 剪枝优化
class Solution {
public List<List<Integer>> combinationSum(int[] candidates, int target) {
List<List<Integer>> res = new ArrayList<>();
Arrays.sort(candidates); // 先进行排序
backtracking(res, new ArrayList<>(), candidates, target, 0, 0);
return res;
}
public void backtracking(List<List<Integer>> res, List<Integer> path, int[] candidates, int target, int sum, int idx) {
// 找到了数字和为 target 的组合
if (sum == target) {
res.add(new ArrayList<>(path));
return;
}
for (int i = idx; i < candidates.length; i++) {
// 如果 sum + candidates[i] > target 就终止遍历
if (sum + candidates[i] > target) break;
path.add(candidates[i]);
backtracking(res, path, candidates, target, sum + candidates[i], i);
path.remove(path.size() - 1); // 回溯,移除路径 path 最后一个元素
}
}
}
组合总和 II#
题目 中等
注意去重!
class Solution {
List<List<Integer>> res = new ArrayList<>();
LinkedList<Integer> path = new LinkedList<>();
int sum = 0;
public List<List<Integer>> combinationSum2(int[] candidates, int target) {
//为了将重复的数字都放到一起,所以先进行排序
Arrays.sort(candidates);
backTracking(candidates, target, 0);
return res;
}
private void backTracking(int[] candidates, int target, int start) {
if (sum == target) {
res.add(new ArrayList<>(path));
return;
}
for (int i = start; i < candidates.length && sum + candidates[i] <= target; i++) {
//正确剔除重复解的办法
//跳过同一树层使用过的元素
if (i > start && candidates[i] == candidates[i - 1]) {
continue;
}
sum += candidates[i];
path.add(candidates[i]);
// i+1 代表当前组内元素只选取一次
backTracking(candidates, target, i + 1);
int temp = path.getLast();
sum -= temp;
path.removeLast();
}
}
}
分割回文串#
题目 中等
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.List;
public class Solution {
public List<List<String>> partition(String s) {
int len = s.length();
List<List<String>> res = new ArrayList<>();
if (len == 0) {
return res;
}
char[] charArray = s.toCharArray();
// 预处理
// 状态:dp[i][j] 表示 s[i][j] 是否是回文
boolean[][] dp = new boolean[len][len];
// 状态转移方程:在 s[i] == s[j] 的时候,dp[i][j] 参考 dp[i + 1][j - 1]
for (int right = 0; right < len; right++) {
// 注意:left <= right 取等号表示 1 个字符的时候也需要判断
for (int left = 0; left <= right; left++) {
if (charArray[left] == charArray[right] && (right - left <= 2 || dp[left + 1][right - 1])) {
dp[left][right] = true;
}
}
}
Deque<String> stack = new ArrayDeque<>();
dfs(s, 0, len, dp, stack, res);
return res;
}
private void dfs(String s, int index, int len, boolean[][] dp, Deque<String> path, List<List<String>> res) {
if (index == len) {
res.add(new ArrayList<>(path));
return;
}
for (int i = index; i < len; i++) {
if (dp[index][i]) {
path.addLast(s.substring(index, i + 1));
dfs(s, i + 1, len, dp, path, res);
path.removeLast();
}
}
}
}
作者:liweiwei1419
链接:https://leetcode.cn/problems/palindrome-partitioning/solutions/54233/hui-su-you-hua-jia-liao-dong-tai-gui-hua-by-liweiw/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
复原 IP 地址#
题目 中等
class Solution {
List<String> result = new ArrayList<String>();
StringBuilder stringBuilder = new StringBuilder();
public List<String> restoreIpAddresses(String s) {
restoreIpAddressesHandler(s, 0, 0);
return result;
}
// number表示stringbuilder中ip段的数量
public void restoreIpAddressesHandler(String s, int start, int number) {
// 如果start等于s的长度并且ip段的数量是4,则加入结果集,并返回
if (start == s.length() && number == 4) {
result.add(stringBuilder.toString());
return;
}
// 如果start等于s的长度但是ip段的数量不为4,或者ip段的数量为4但是start小于s的长度,则直接返回
if (start == s.length() || number == 4) {
return;
}
// 剪枝:ip段的长度最大是3,并且ip段处于[0,255]
for (int i = start; i < s.length() && i - start < 3 && Integer.parseInt(s.substring(start, i + 1)) >= 0
&& Integer.parseInt(s.substring(start, i + 1)) <= 255; i++) {
// 如果ip段的长度大于1,并且第一位为0的话,continue
if (i + 1 - start > 1 && s.charAt(start) - '0' == 0) {
continue;
}
stringBuilder.append(s.substring(start, i + 1));
// 当stringBuilder里的网段数量小于3时,才会加点;如果等于3,说明已经有3段了,最后一段不需要再加点
if (number < 3) {
stringBuilder.append(".");
}
number++;
restoreIpAddressesHandler(s, i + 1, number);
number--;
// 删除当前stringBuilder最后一个网段,注意考虑点的数量的问题
stringBuilder.delete(start + number, i + number + 2);
}
}
}
子集#
题目 中等
要清楚子集问题和组合问题、分割问题的的区别,子集是收集树形结构中树的所有节点的结果。
而组合问题、分割问题是收集树形结构中叶子节点的结果。
class Solution {
List<List<Integer>> result = new ArrayList<>();// 存放符合条件结果的集合
LinkedList<Integer> path = new LinkedList<>();// 用来存放符合条件结果
public List<List<Integer>> subsets(int[] nums) {
subsetsHelper(nums, 0);
return result;
}
private void subsetsHelper(int[] nums, int startIndex){
result.add(new ArrayList<>(path));//「遍历这个树的时候,把所有节点都记录下来,就是要求的子集集合」。
if (startIndex >= nums.length){ //终止条件可不加
return;
}
for (int i = startIndex; i < nums.length; i++){
path.add(nums[i]);
subsetsHelper(nums, i + 1);
path.removeLast();
}
}
}
子集 II#
题目 中等
class Solution {
List<List<Integer>> res = new ArrayList<>();
LinkedList<Integer> path = new LinkedList<>();
public List<List<Integer>> subsetsWithDup(int[] nums) {
Arrays.sort(nums);
subsetsWithDupHelper(nums, 0);
return res;
}
private void subsetsWithDupHelper(int[] nums, int start) {
res.add(new ArrayList<>(path));
for (int i = start; i < nums.length; i++) {
// 跳过当前树层使用过的、相同的元素
if (i > start && nums[i - 1] == nums[i]) {
continue;
}
path.add(nums[i]);
subsetsWithDupHelper(nums, i + 1);
path.removeLast();
}
}
}
递增子序列#
题目 中等
注意:此时不能排序了,原来的去重逻辑不通用了,得用set(或数组)来去重了。
class Solution {
private List<Integer> path = new ArrayList<>();
private List<List<Integer>> res = new ArrayList<>();
public List<List<Integer>> findSubsequences(int[] nums) {
backtracking(nums,0);
return res;
}
private void backtracking (int[] nums, int start) {
if (path.size() > 1) {
res.add(new ArrayList<>(path));
}
int[] used = new int[201];
for (int i = start; i < nums.length; i++) {
if (!path.isEmpty() && nums[i] < path.get(path.size() - 1) ||
(used[nums[i] + 100] == 1)) continue;
used[nums[i] + 100] = 1;
path.add(nums[i]);
backtracking(nums, i + 1);
path.remove(path.size() - 1);
}
}
}
全排列#
题目 中等
class Solution {
List<List<Integer>> result = new ArrayList<>();// 存放符合条件结果的集合
LinkedList<Integer> path = new LinkedList<>();// 用来存放符合条件结果
boolean[] used;
public List<List<Integer>> permute(int[] nums) {
if (nums.length == 0){
return result;
}
used = new boolean[nums.length];
permuteHelper(nums);
return result;
}
private void permuteHelper(int[] nums){
if (path.size() == nums.length){
result.add(new ArrayList<>(path));
return;
}
for (int i = 0; i < nums.length; i++){
if (used[i]){
continue;
}
used[i] = true;
path.add(nums[i]);
permuteHelper(nums);
path.removeLast();
used[i] = false;
}
}
}
全排列 II#
题目 中等
要知道 used[i - 1] == false 也行而 used[i - 1] == true 也行的本质原因
class Solution {
//存放结果
List<List<Integer>> result = new ArrayList<>();
//暂存结果
List<Integer> path = new ArrayList<>();
public List<List<Integer>> permuteUnique(int[] nums) {
boolean[] used = new boolean[nums.length];
Arrays.fill(used, false);
Arrays.sort(nums);
backTrack(nums, used);
return result;
}
private void backTrack(int[] nums, boolean[] used) {
if (path.size() == nums.length) {
result.add(new ArrayList<>(path));
return;
}
for (int i = 0; i < nums.length; i++) {
// used[i - 1] == true,说明同⼀树⽀nums[i - 1]使⽤过
// used[i - 1] == false,说明同⼀树层nums[i - 1]使⽤过
// 如果同⼀树层nums[i - 1]使⽤过则直接跳过
if (i > 0 && nums[i] == nums[i - 1] && used[i - 1] == false) {
continue;
}
//如果同⼀树⽀nums[i]没使⽤过开始处理
if (used[i] == false) {
used[i] = true;//标记同⼀树⽀nums[i]使⽤过,防止同一树枝重复使用
path.add(nums[i]);
backTrack(nums, used);
path.remove(path.size() - 1);//回溯,说明同⼀树层nums[i]使⽤过,防止下一树层重复
used[i] = false;//回溯
}
}
}
}
重新安排行程#
题目 困难
用TreeMap,第一次找到的即为字典序最小,即刻返回。否则用普通 HashMap 不排序,全部遍历会超时!
class Solution {
private Deque<String> res;
private Map<String, Map<String, Integer>> map;
private boolean backTracking(int ticketNum){
if(res.size() == ticketNum + 1){
return true;
}
String last = res.getLast();
if(map.containsKey(last)){//防止出现null
for(Map.Entry<String, Integer> target : map.get(last).entrySet()){
int count = target.getValue();
if(count > 0){
res.add(target.getKey());
target.setValue(count - 1);
if(backTracking(ticketNum)) return true;
res.removeLast();
target.setValue(count);
}
}
}
return false;
}
public List<String> findItinerary(List<List<String>> tickets) {
map = new HashMap<String, Map<String, Integer>>();
res = new LinkedList<>();
for(List<String> t : tickets){
Map<String, Integer> temp;
if(map.containsKey(t.get(0))){
temp = map.get(t.get(0));
temp.put(t.get(1), temp.getOrDefault(t.get(1), 0) + 1);
}else{
temp = new TreeMap<>();//升序Map
temp.put(t.get(1), 1);
}
map.put(t.get(0), temp);
}
res.add("JFK");
backTracking(tickets.size());
return new ArrayList<>(res);
}
}
N 皇后#
题目 困难
核心为:棋盘的宽度就是 for 循环的长度,递归的深度就是棋盘的高度。这样就可以套进回溯法的模板里了。
class Solution {
List<List<String>> res = new ArrayList<>();
public List<List<String>> solveNQueens(int n) {
char[][] chessboard = new char[n][n];
for (char[] c : chessboard) {
Arrays.fill(c, '.');
}
backTrack(n, 0, chessboard);
return res;
}
public void backTrack(int n, int row, char[][] chessboard) {
if (row == n) {
res.add(Array2List(chessboard));
return;
}
for (int col = 0;col < n; ++col) {
if (isValid (row, col, n, chessboard)) {
chessboard[row][col] = 'Q';
backTrack(n, row+1, chessboard);
chessboard[row][col] = '.';
}
}
}
public List Array2List(char[][] chessboard) {
List<String> list = new ArrayList<>();
for (char[] c : chessboard) {
list.add(String.copyValueOf(c));
}
return list;
}
public boolean isValid(int row, int col, int n, char[][] chessboard) {
// 检查列
for (int i=0; i<row; ++i) { // 相当于剪枝
if (chessboard[i][col] == 'Q') {
return false;
}
}
// 检查45度对角线
for (int i=row-1, j=col-1; i>=0 && j>=0; i--, j--) {
if (chessboard[i][j] == 'Q') {
return false;
}
}
// 检查135度对角线
for (int i=row-1, j=col+1; i>=0 && j<=n-1; i--, j++) {
if (chessboard[i][j] == 'Q') {
return false;
}
}
return true;
}
}
方法二更快一点:
// 方法2:使用boolean数组表示已经占用的直(斜)线
class Solution {
List<List<String>> res = new ArrayList<>();
boolean[] usedCol, usedDiag45, usedDiag135; // boolean数组中的每个元素代表一条直(斜)线
public List<List<String>> solveNQueens(int n) {
usedCol = new boolean[n]; // 列方向的直线条数为 n
usedDiag45 = new boolean[2 * n - 1]; // 45°方向的斜线条数为 2 * n - 1
usedDiag135 = new boolean[2 * n - 1]; // 135°方向的斜线条数为 2 * n - 1
//用于收集结果, 元素的index表示棋盘的row,元素的value代表棋盘的column
int[] board = new int[n];
backTracking(board, n, 0);
return res;
}
private void backTracking(int[] board, int n, int row) {
if (row == n) {
//收集结果
List<String> temp = new ArrayList<>();
for (int i : board) {
char[] str = new char[n];
Arrays.fill(str, '.');
str[i] = 'Q';
temp.add(new String(str));
}
res.add(temp);
return;
}
for (int col = 0; col < n; col++) {
if (usedCol[col] | usedDiag45[row + col] | usedDiag135[row - col + n - 1]) {
continue;
}
board[row] = col;
// 标记该列出现过
usedCol[col] = true;
// 同一45°斜线上元素的row + col为定值, 且各不相同
usedDiag45[row + col] = true;
// 同一135°斜线上元素row - col为定值, 且各不相同
// row - col 值有正有负, 加 n - 1 是为了对齐零点
usedDiag135[row - col + n - 1] = true;
// 递归
backTracking(board, n, row + 1);
usedCol[col] = false;
usedDiag45[row + col] = false;
usedDiag135[row - col + n - 1] = false;
}
}
}
解数独#
题目 困难
这题其实我自己的代码就写的蛮好的,代码随想录上那个代码不大行。下面是力扣某题解的优化版本(虽然感觉还是我的更好一点)
class Solution {
public void solveSudoku(char[][] board) {
if (board == null || board.length != 9 || board[0].length != 9) return;
boolean[][] row = new boolean[9][9], col = new boolean[9][9], box = new boolean[9][9];
for (int i = 0; i < 9; i++) {
for (int j = 0; j < 9; j++) {
if (board[i][j] == '.') continue;
int num = board[i][j] - '1', k = (i / 3) * 3 + j / 3;
row[i][num] = col[j][num] = box[k][num] = true;
}
}
solveSudokuHelper(board, 0, row, col, box);
}
boolean solveSudokuHelper(char[][] board, int n, boolean[][] row, boolean[][] col, boolean[][] box) {
if (n == 81) return true;
int i = n / 9, j = n % 9;
if (board[i][j] != '.') return solveSudokuHelper(board, n + 1, row, col, box);
int k = (i / 3) * 3 + j / 3;
for (int num = 0; num < 9; num++) {
if (row[i][num] || col[j][num] || box[k][num]) continue;
board[i][j] = (char) (num + '1');
row[i][num] = col[j][num] = box[k][num] = true;
if (solveSudokuHelper(board, n + 1, row, col, box)) return true;
row[i][num] = col[j][num] = box[k][num] = false;
}
board[i][j] = '.';
return false;
}
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· AI 智能体引爆开源社区「GitHub 热点速览」
· 三行代码完成国际化适配,妙~啊~
· .NET Core 中如何实现缓存的预热?