LeetCode刷题笔记--回溯


算法介绍

回溯算法(Backtracking)是对树形或者图形结构执行一次深度优先遍历,实际上类似枚举的搜索尝试过程,在遍历的过程中寻找问题的解。

基本思想是:从问题的某一种状态(初始状态)出发,搜索从这种状态出发所能达到的所有“状态”,当一条路走到“尽头”的时候(不能再前进),再后退一步或若干步,从另一种可能“状态”出发,继续搜索,直到所有的“路径”(状态)都试探过。

深度优先遍历有个特点:当发现已不满足求解条件时,就返回,尝试别的路径。此时对象类型变量就需要重置成为和之前一样,称为状态重置

实际上,回溯算法就是暴力搜索算法。

练习题目

1. 39.组合总和

题目:给你一个 无重复元素 的整数数组 candidates 和一个目标整数 target ,找出 candidates 中可以使数字和为目标数 target 的 所有 不同组合 ,并以列表形式返回。你可以按 任意顺序 返回这些组合。

candidates 中的 同一个 数字可以 无限制重复被选取 。如果至少一个数字的被选数量不同,则两种组合是不同的。

对于给定的输入,保证和为 target 的不同组合数少于 150 个。

示例 1:
输入:candidates = [2,3,6,7], target = 7
输出:[[2,2,3],[7]]
解释:
2 和 3 可以形成一组候选,2 + 2 + 3 = 7 。注意 2 可以使用多次。
7 也是一个候选, 7 = 7 。
仅有这两种组合。

示例 2:
输入: candidates = [2,3,5], target = 8
输出: [[2,2,2,2],[2,3,3],[3,5]]

示例 3:
输入: candidates = [2], target = 1
输出: []

求解:
本题是经典回溯例题,需注意的点如下:
①递归终止条件;
②子问题起始遍历索引;
③结果集添加需根据path队列重新构造一个对象;

本题递归终止条件有两个,分别是数字和大于目标值与数字和等于目标值;子问题起始遍历索引是从start开始,因为其组合元素可以重复;

代码如下:

class Solution {
    //返回值列表
    List<List<Integer>> res=new ArrayList<>();
    //记录队列
    Deque<Integer> path=new ArrayDeque<>();
    public List<List<Integer>> combinationSum(int[] candidates, int target) {
        backTracking(candidates,target,0,0);
        return res;
    }

    public void backTracking(int[] arr,int target,int start,int sum){
        //数字和大于目标值,不符合返回
        if(sum>target){
            return;
        }
        //数字和等于目标值,添加到返回值列表
        if(sum==target){
            //!!!重要
            //需重新生成一个列表,原因是path是引用的地址
            //若直接add(path)则res内的所有列表为空,
            //因为是该步未实际的将path添加,
            //而是等最后的removeLast将path清空后,
            //通过path地址将清空后的path添加进去
            res.add(new ArrayList<>(path));
            return;
        }
        //从start出发开始遍历
        for(int i=start;i<arr.length;i++){
            //将元素添加到队列末尾
            path.addLast(arr[i]);
            //通过sum记录数字和,递归遍历
            backTracking(arr,target,i,sum+arr[i]);
            //进行回溯,将队列末尾元素删除
            path.removeLast();
        }
    }
}

2. 46.全排列

题目: 给定一个不含重复数字的数组 nums ,返回其所有可能的全排列 。你可以按任意顺序返回答案。

示例 1:
输入:nums = [1,2,3]
输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]

示例 2:
输入:nums = [0,1]
输出:[[0,1],[1,0]]

示例 3:
输入:nums = [1]
输出:[[1]]

求解:
本题思路较为简单,是较为常规的回溯算法思路,需要注意的主要有两点:
①回溯过程中任何元素都可作为全排列的头元素且无序,故从0开始遍历;
②增加的一个记录状态数组:记录当前数据有无被遍历,来对遍历条件进行筛选;

代码如下:

class Solution {
    List<List <Integer>> res=new ArrayList<>();
    Deque<Integer> path=new ArrayDeque<>();
    public List<List<Integer>> permute(int[] nums) {
        //记录状态数组:记录当前数据有无被遍历
        //初始化数组全为0,若已遍历修改状态为1
        int [] flag=new int[nums.length];
        dfs(nums,flag);
        return res;
    }

    public void dfs(int[] arr,int[] flag){
        if(path.size() == arr.length){
            res.add(new ArrayList<>(path));
            return;
        }
        //!!!
        //任何元素都可作为全排列的头元素,故从0开始遍历
        for(int i=0;i<arr.length;i++){
            //若未被遍历,则可以添加
            if(flag[i]==0){
                flag[i]=1;
                path.addLast(arr[i]);
                dfs(arr,flag);
                //回溯,状态复原
                flag[i]=0;
                path.removeLast();
            }
        }
    }
}

3. 51.N皇后

题目: 按照国际象棋的规则,皇后可以攻击与之处在同一行或同一列或同一斜线上的棋子。
n 皇后问题研究的是如何将 n 个皇后放置在 n×n 的棋盘上,并且使皇后彼此之间不能相互攻击。
给你一个整数 n ,返回所有不同的 n 皇后问题 的解决方案。
每一种解法包含一个不同的 n 皇后问题 的棋子放置方案,该方案中 ‘Q’ 和 ‘.’ 分别代表了皇后和空位。

示例 1:
输入:n = 4
输出:[[“.Q…”,“…Q”,“Q…”,“…Q.”],[“…Q.”,“Q…”,“…Q”,“.Q…”]]
两种棋盘摆放方案

解释:如上图所示,4 皇后问题存在两个不同的解法。

示例 2:
输入:n = 1
输出:[[“Q”]]
求解:
本题可以通过暴力枚举将 N 个皇后放置在 N×N 的棋盘上的所有可能的情况,并对每一种情况判断是否满足皇后彼此之间不相互攻击。但此暴力解法的时间复杂度非常高,需加以约束条件进行优化剪枝。
本题关键是个皇后必须位于不同行和不同列,因此将 N 个皇后放置在 N×N 的棋盘上,一定是每一行有且仅有一个皇后每一列有且仅有一个皇后且任何两个皇后都不能在同一条斜线上(主对角线与副对角线两个方向)。
使用三个集合column、dia1、dia2 分别记录每一列以及两个方向的每条斜线上是否有皇后。
行方向使用索引start记录,每次递归进行start+1,故不需判断行方向;
列方向使用column记录,从0到n-1,每次需判断是否相等;
主对角线方向用dia1记录,利用当前点行号-列号进行判断,如(0,1)、(1,2)点结果都为-1;
副对角线方向用dia2记录,利用当前点行号+列号进行判断,如(0,1)、(1,0)点结果都为1;

每次放置皇后时,对于每个位置判断其是否在三个集合中,如果三个集合都不包含当前位置,则当前位置是可以放置皇后的位置。

代码如下:

class Solution {
    //结果列表
    List<List<String>> res = new ArrayList<>();
    //列信息列表
    List<Integer> column=new ArrayList<>();
    //反斜杆对角线
    List<Integer> dia1=new ArrayList<>();
    //斜杆对角线
    List<Integer> dia2=new ArrayList<>();
    public List<List<String>> solveNQueens(int n) {
        //存储每行的Queen位置
        int[] arr=new int[n];
        //填充-1,避免0位置冲突
        Arrays.fill(arr,-1);
        dfs(arr,0);
        return res;

    }

    public void dfs(int[] arr,int start){
        //行与n相等,结束遍历
        if(start == arr.length){
            //添加棋盘方案
            res.add(generate(arr));
            return;
        }
        //列
        for(int i=0;i<arr.length;i++){
            //判断列
            if(column.contains(i)){
                continue;
            }
            //判断反斜杆对角线:行-列相等
            int dia11=start-i;
            if(dia1.contains(dia11)){
                continue;
            }
            //判断斜杆对角线:行+列相等
            int dia22=start+i;
            if(dia2.contains(dia22)){
                continue;
            }
            arr[start]=i;
            column.add(i);
            dia1.add(dia11);
            dia2.add(dia22);
            
            //start+1表示行,故行永不相等,无需判断行
            dfs(arr,start+1);

            //回溯,进行状态复原
            arr[start]=-1;
            column.remove(column.size()-1);
            dia1.remove(dia1.size()-1);
            dia2.remove(dia2.size()-1);
        }
    }


    //根据arr生成string列表
    public List<String> generate(int[] arr){
        List<String> lis=new ArrayList<>();
        for(int i=0;i<arr.length;i++){
            //利用char数组和new String生成字符串
            char[] row = new char[arr.length];
            Arrays.fill(row, '.');
            row[arr[i]] = 'Q';
            lis.add(new String(row));
        }
        return lis;
    }
}

总结

以上就是今天要讲的内容,本文仅仅简单介绍了回溯算法及相关案例,其中结束递归条件与状态重置尤为重要,其余情况则需根据题目条件具体分析,如子问题起始遍历条件,子问题筛选等。

参考《力扣 回溯》
https://leetcode.cn/tag/backtracking/problemset/

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值