//关于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));
}
}