回溯
//关于ArrayList和LinkedList应该使用哪个:
//频繁头部/尾部插入/删除(如记录路径的track),就使用LinkedList
//频繁添加(不在乎头和尾),最终需要返回结果(如存放结果的res),使用ArrayList

//private的使用:
//添加private之后,可以保证在下面方法引用中使用不到private修饰的它们,保证方法的安全。
//在实际工作中需要注意这方面的安全性

LC46

package com.wang.leetcode.DFS;

import java.util.LinkedList;
import java.util.List;

//全排列
//给定一个不含重复数字的数组 nums ,返回其所有可能的全排列
//你可以 按任意顺序 返回答案。
//回溯算法的精髓:尝试一个选择 → 深入探索 → 回退 → 尝试下一个选择。
class Solution46{
   private List<List<Integer>>res=new ArrayList<>();
    // 主函数,输入一组不重复的数字,返回它们的全排列
   List<List<Integer>> permute(int[] nums){
        // 记录「路径」
        LinkedList<Integer> track = new LinkedList<>();
        // 「路径」中的元素会被标记为 true,避免重复使用
        boolean[] used = new boolean[nums.length];
        backtrack(nums, track, used);// 开始回溯
        return res;
    }
    // 路径:记录在 track 中
    // 选择列表:nums 中不存在于 track 的那些元素(used[i] 为 false)
    // 结束条件:nums 中的元素全都在 track 中出现
    private void backtrack(int[] nums, LinkedList<Integer> track, boolean[] used){
        // 结束条件
        if (track.size()== nums.length){
            res.add(new LinkedList<>(track));
            return;
        }

        // 遍历所有可能的选择
        for (int i = 0; i < nums.length; i++) {
            if (used[i]) continue;// 跳过已使用的数字

            track.add(nums[i]);//把当前数字加入路径的数组中
            used[i]=true;//标记该数字使用过了

            backtrack(nums, track, used);// 继续构建剩余部分的排列

            //回溯到上一步,尝试其他可能性。可以在同一层级尝试其他数字
            track.removeLast();// 移除最后加入的数字
            used[i]=false; // 取消该数字的使用标记
        }
    }
}
public class LC46 {
    public static void main(String[] args) {
        Solution46 solution46=new Solution46();
        int[]nums={1,2,3};
        System.out.println(solution46.permute(nums));
    }
}

LC78

package com.wang.leetcode.DFS;

import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;

//子集
//给你一个整数数组 nums ,数组中的元素 互不相同 。返回该数组所有可能的子集(幂集)。
//解集 不能 包含重复的子集。你可以按 任意顺序 返回解集。
class solution78{
    private List<List<Integer>>res=new ArrayList<>();
    private LinkedList<Integer>track=new LinkedList<>();
    // 主函数
    public List<List<Integer>> subsets(int[] nums){
        backtrack(nums, 0);
        return res;
    }
    // 回溯算法核心函数,遍历子集问题的回溯树
    //start:确保每次只考虑当前位置之后的数字,防止生成重复子集
    private void backtrack(int[] nums, int start){
        //不需要判断长度
        res.add(new LinkedList<>(track)); // 添加当前子集到结果集,包括空集
        for (int i = start; i < nums.length; i++) {
            track.addLast(nums[i]);
            backtrack(nums,i+1);// 递归进入下一层:基于当前选择,继续向后探索
            track.removeLast();//撤销选择,回溯到上一层状态,尝试其他可能性
        }
    }
}
/*
2. 递归调用栈的返回过程
以 nums = [1, 2] 为例:
初始调用 backtrack(nums, 0):
记录子集 []。
i=0:选择 1 → 进入递归 backtrack(nums, 1)。

第二层递归 backtrack(nums, 1):
记录子集 [1]。
i=1:选择 2 → 进入递归 backtrack(nums, 2)。

第三层递归 backtrack(nums, 2):
记录子集 [1, 2]。
i=2 不满足 i < nums.length,循环直接结束,函数返回。

返回到第二层:
执行 removeLast() → 移除 2,track = [1]。
i=1 的循环结束,函数返回。

返回到第一层:
执行 removeLast() → 移除 1,track = []。
i=0 的循环继续,i=1:选择 2 → 进入递归 backtrack(nums, 2)。

再次进入第二层 backtrack(nums, 2):
记录子集 [2]。
i=2 不满足条件,循环结束,函数返回。
最终返回到初始调用,生成所有子集 [[], [1], [1,2], [2]]。
 */
public class LC78 {
    public static void main(String[] args) {
        solution78 solution78=new solution78();
        int[]nums={1,2,3};
        System.out.println(solution78.subsets(nums));
    }
}

LC39

package com.wang.leetcode.DFS;

import java.util.LinkedList;
import java.util.List;

//组合总和
//给你一个 无重复元素 的整数数组 candidates 和一个目标整数 target
//找出 candidates 中可以使数字和为目标数 target 的 所有 不同组合
//并以列表形式返回。你可以按 任意顺序 返回这些组合
//candidates 中的 同一个 数字可以 无限制重复被选取
class solution39{
    private List<List<Integer>> res = new ArrayList<>();
    // 记录回溯的路径
    private LinkedList<Integer> track = new LinkedList<>();//使用link,因为要频繁的添加,删除

    public List<List<Integer>> combinationSum(int[] candidates, int target){
        if (candidates.length==0)return res;
        backtrack(candidates, 0, target, 0);// 从索引0开始回溯
        return res;
    }

    // 回溯算法主函数
    private void backtrack(int[] candidates, int start, int target, int sum){
        if (sum==target){//终止条件1:找到有效组合
            res.add(new LinkedList<>(track));
            return;
        }

        if (sum>target)return;// 终止条件2:当前和已超过目标值

        for (int i = start; i < candidates.length; i++) {
            track.add(candidates[i]);
            sum+=candidates[i];// 更新当前和
            backtrack(candidates, i, target, sum);//递归调用,start=i是因为允许重复使用
            sum-=candidates[i];// 撤销选择
            track.removeLast();
        }
    }
}
public class LC39 {
    public static void main(String[] args) {
        solution39 solution39=new solution39();
        int[]nums={2,3,6,7};
        int ta=7;
        System.out.println(solution39.combinationSum(nums,ta));
    }
}

LC79

package com.wang.leetcode.DFS;
//单词搜索
//给定一个 m x n 二维字符网格 board 和一个字符串单词 word
//如果 word 存在于网格中,返回 true ;否则,返回 false
//单词必须按照字母顺序,通过相邻的单元格内的字母构成,其中“相邻”单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母不允许被重复使用
class solution79{
    private boolean found = false;// 全局标志,表示是否找到单词
    public boolean exist(char[][] board, String word){
        int m = board.length, n = board[0].length;
        // 遍历网格中的每个单元格作为起点
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                dfs(board, i, j, word, 0);// 从(i,j)开始深度优先搜索
                if (found) {// 如果找到立即返回
                    return true;
                }
            }
        }
        return false;
    }

    // 深度优先搜索函数
    // 从 (i, j) 开始向四周搜索,试图匹配 word[p..]
    private void dfs(char[][] board, int i, int j, String word, int p){
        // 结束条件:已匹配完整单词
        if (p==word.length()){
            found=true;
            return;
        }
        if (found)return; // 提前终止,如果其他路径已找到

        int m = board.length, n = board[0].length;
        if (i<0||j<0||i>=m||j>=n)return; // 检查边界

        if (board[i][j]!=word.charAt(p))return; // 检查当前字符是否匹配,不匹配则返回到上一层

        // 已经匹配过的字符,我们给它添一个负号作为标记,避免走回头路
        board[i][j]=(char) (-board[i][j]);//可以通过再次取负恢复
        // 向四个方向递归搜索
        //每次递归调用p+1表示匹配下一个字符
        dfs(board, i+1, j, word, p+1);  // 向下搜索
        dfs(board, i, j+1, word, p+1);  // 向右搜索
        dfs(board, i-1, j, word, p+1);  // 向上搜索
        dfs(board, i, j-1, word, p+1);  // 向左搜索
        // 恢复字符(回溯)
        board[i][j] = (char)(-board[i][j]);//再次取负来恢复
    }
}
public class LC79 {
    public static void main(String[] args) {
        solution79 solution79=new solution79();
        char[][] board = {
                {'A', 'B', 'C', 'E'},
                {'S', 'F', 'C', 'S'},
                {'A', 'D', 'E', 'E'}
        };
        String word = "ABCCED";
        System.out.println(solution79.exist(board,word));

    }
}

LC93

package com.wang.leetcode.DFS;

import java.util.LinkedList;
import java.util.List;

//复原 IP 地址
//有效 IP 地址 正好由四个整数(每个整数位于 0 到 255 之间组成,且不能含有前导 0),整数之间用 '.' 分隔
//给定一个只包含数字的字符串 s ,用以表示一个 IP 地址,返回所有可能的有效 IP 地址,这些地址可以通过在 s 中插入 '.' 来形成。你 不能 重新排序或删除 s 中的任何数字。你可以按 任何 顺序返回答案。
class solution93{
    private List<String> res = new ArrayList<>();//结果
    private LinkedList<String> track = new LinkedList<>();//路径

    public List<String> restoreIpAddresses(String s){
        backtrack(s, 0);
        return res;
    }

    // 回溯算法框架
   private void backtrack(String s, int start){
        //终止条件:已遍历完字符串且得到4个IP段
        if (start==s.length()&&track.size()==4){
            res.add(String.join(".",track));
            return;
        }

        for (int i = start; i < s.length(); i++) {
            //不合法就跳过
            if (!isValid(s, start, i)){
                continue;
            }
            //大于四端就停止循环
            if (track.size() >= 4) {
                // 已经分解成 4 部分了,不能再分解了
                break;
            }
            // 做选择:将合法IP段加入路径
            //s.substring(start, i+1) 截取字符串中从start到i的子串作为一个IP段
            track.addLast(s.substring(start,i+1));

            // 递归进入下一层
            //i+1表示从下一个字符开始继续分割
            backtrack(s,i+1);

            // 撤销选择
            track.removeLast();
        }
    }

    // 判断 s[start..end] 是否是一个合法的 ip 段
    private boolean isValid(String s, int start, int end){
        int length = end - start + 1;//每一段的长度
        if (length==0||length>3)return false;//长度=0或大于3,不是
        if (length==1)return true;//长度=1也是合法的
        if (s.charAt(start)=='0')return false;//开头是0,不合法,如011不合法,但是0是可以的
        if (length<=2)return true;
        if (Integer.parseInt(s.substring(start,start+length))>255)return false;//如果数字的大小大于255了,不合法
        else return true;
    }
}
public class LC93 {
    public static void main(String[] args) {
        solution93 solution93=new solution93();
        String s="25525511135";
        System.out.println(solution93.restoreIpAddresses(s));
    }
}

LC40

package com.wang.leetcode.DFS;

import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;

//组合总和 II
//给定一个候选人编号的集合 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。
//candidates 中的每个数字在每个组合中只能使用 一次 。
//与39的区别是数字只能使用一次
class solution40{
    private List<List<Integer>> res = new ArrayList<>();
    // 记录回溯的路径
    private LinkedList<Integer> track = new LinkedList<>();
    // 记录 track 中的元素之和
    private int trackSum = 0;

    public List<List<Integer>> combinationSum2(int[] candidates, int target) {
        if (candidates.length == 0) {
            return res;
        }
        // 先排序,让相同的元素靠在一起
        Arrays.sort(candidates);
        backtrack(candidates, 0, target);
        return res;
    }

    // 回溯算法主函数
    private void backtrack(int[] nums, int start, int target) {
        //结束条件:达到目标和,找到符合条件的组合
        if (trackSum == target) {
            res.add(new LinkedList<>(track));
            return;
        }
        // 结束条件:超过目标和,直接结束
        if (trackSum > target) {
            return;
        }

        // 回溯算法标准框架
        for (int i = start; i < nums.length; i++) {
            // 剪枝逻辑,值相同的树枝,只遍历第一条
            if (i > start && nums[i] == nums[i - 1]) {
                continue;
            }
            // 做选择
            track.add(nums[i]);
            trackSum += nums[i];
            // 递归遍历下一层回溯树
            backtrack(nums, i + 1, target);//下一层递归从 i+1 开始,确保每个数字只被使用一次
            // 撤销选择
            track.removeLast();
            trackSum -= nums[i];
        }
    }
}
public class LC40 {
    public static void main(String[] args) {
        solution40 solution40=new solution40();
        int[] candidates={10,1,2,7,6,1,5};
        int t=8;
        System.out.println(solution40.combinationSum2(candidates,t));
    }
}

LC51

package com.wang.leetcode.DFS;

import java.util.ArrayList;
import java.util.List;

//N 皇后(困难)
//按照国际象棋的规则,皇后可以攻击与之处在同一行或同一列或同一斜线上的棋子
//n 皇后问题 研究的是如何将 n 个皇后放置在 n×n 的棋盘上,并且使皇后彼此之间不能相互攻击。
//给你一个整数 n ,返回所有不同的 n 皇后问题 的解决方案
//每一种解法包含一个不同的 n 皇后问题 的棋子放置方案,该方案中 'Q' 和 '.' 分别代表了皇后和空位。
//N 皇后问题就是一个决策问题:对于每一行,我应该选择在哪一列放置皇后呢?
//是回溯算法题目
//时间复杂度最坏情况下为 O(N!)
class solution51 {
    //存放结果
    //关于ArrayList和LinkedList应该使用哪个:
    //频繁头部/尾部插入/删除(如记录路径的track),就使用LinkedList
    //频繁添加(不在乎头和尾),最终需要返回结果(如存放结果的res),使用ArrayList
    private List<List<String>> res = new ArrayList<>();
    //添加private之后,可以保证在下面public class LC51 中使用不到它们,保证安全。
    //因为我们只想让它们在class solution51中使用
    //在实际工作中要使用private,为了保证安全

    // 输入棋盘边长 n,返回所有合法的放置
    public List<List<String>> solveNQueens(int n) {
        // '.' 表示空,'Q' 表示皇后,初始化空棋盘。
        List<String> board = new ArrayList<>();
        for (int i = 0; i < n; i++) {
            board.add(".".repeat(n));//添加".",重复n次。最终生成的是n*n的正方形,每一块上是.
        }
        backtrack(board, 0);//递归调用,从第0行开始尝试放置皇后
        return res;
    }

    //回溯函数
    //board:表示当前棋盘状态,每个字符串代表一行
    //row:表示当前正在处理的行号(从 0 开始)
    private void backtrack(List<String> board, int row) {
        //终止条件:行号到最后一个,说明该行已经处理完完毕,就加入结果中
        if (row == board.size()) {
            res.add(new ArrayList<>(board));
            return;
        }

        //n表示棋盘的列数,决定当前行有多少列需要遍历尝试放置皇后
        int n = board.get(row).length();
        for (int i = 0; i < n; i++) {
            if (!isValid(board, row, i)) {//调用isValid方法检查在(row, i)位置放置皇后是否合法,不合法就跳过
                continue;
            }
            char[] newRow = board.get(row).toCharArray();//将当前行转换为字符数组,便于修改特定位置的字符
            newRow[i] = 'Q';//在当前位置放置皇后
            board.set(row, new String(newRow));//将修改后的行重新设置回棋盘

            // 进入下一行决策
            backtrack(board, row + 1);
            // 撤销选择
            newRow[i] = '.';//将当前位置的皇后移除,恢复为空位
            board.set(row, new String(newRow));//更新棋盘状态(记得更新棋盘状态)
        }
    }

    private boolean isValid(List<String> board, int row, int col) {
        int n = board.size();

        //isValid()方法只需要检查当前位置的上方、左上和右上三个方向
        //因为回溯算法是从上往下逐行放置皇后的
        //不需要考虑下面的

        // 检查列是否有皇后互相冲突
        for (int i = 0; i < row; i++) {
            if (board.get(i).charAt(col) == 'Q')
                return false;
        }

        // 检查右上方是否有皇后互相冲突
        for (int i = row - 1, j = col + 1; i >= 0 && j < n; i--, j++) {
            //从当前位置的上一行(row-1)和右边一列(col+1)开始
            //同时向右上移动(i--, j++)
            //直到超出棋盘边界(i < 0 或 j ≥ n)
            if (board.get(i).charAt(j) == 'Q')
                return false;
        }

        // 检查左上方是否有皇后互相冲突
        for (int i = row - 1, j = col - 1; i >= 0 && j >= 0; i--, j--) {
            //从当前位置的上一行(row-1)和左边一列(col-1)开始
            //同时向左上移动(i--, j--)
            //直到超出棋盘边界(i < 0 或 j < 0)
            if (board.get(i).charAt(j) == 'Q')
                return false;
        }

        return true;
    }
}
//跟93有点像。如果题目中需要判断是否合法、是否可行,就需要一个类去判断是否合法
public class LC51 {
    public static void main(String[] args) {
        solution51 solution51=new solution51();
        int n=4;
        System.out.println(solution51.solveNQueens(n));
    }
}
posted on 2025-04-20 15:19  红星star  阅读(11)  评论(0)    收藏  举报