开始学算法 x+3天 ===>总结战利品篇(回溯算法入门+N皇后+解数独)

跟着carl哥的第X+3天

x天是因为在床上躺了两周,从现在开始学习!!!
缺的会开始补回来的,我向天发誓
还没看N皇后跟数独(比较难),而且刚好是早上,所以看一下总结篇
半路开香槟了属于是

回溯算法基础模板

void backtracking(参数){

	//终止条件一般是当一个元素符合条件以后,将该元素存入结果集 然后返回到上一层再去向下遍历其他元素
    if(终止条件){
        存放结果;
        return;
    }
    
    //for循环就是用来遍历数组的每一个元素(横向遍历)
    for(选择:本层集合中元素(树中结点孩子的数量就是集合的大小)){
        处理节点;

		//递归向下遍历 在终止条件中判断是否符合条件
        backtracking(路径,选择列表); //递归

		//递归之后返回上一层后,再进行回溯处理,把原有下一层的元素去除 重新遍历该层中的其他元素
		//这便是回溯法
        回溯,撤销处理结果
    }
}

回溯思路(回溯三部曲):

  • 第一步:确定好递归后返回的参数(一般采用全局变量,不需要在回溯方法中传太多参数)

  • 第二步:就是确定好回溯法的终止条件 (在这道题是存储单一答案的数组大小 == k值)

  • 第三步:单层搜索的过程 (每一层我们应该怎么写 只看一层就不会乱 )

    其实回溯法就是暴力搜索,然后运用递归实现n个for循环。横向遍历是数组宽度,纵向遍历是递归。

总结回溯算法

  • 组合问题:N个数里面按一定规则找出k个数的集合
  • 切割问题:一个字符串按一定规则进行字符串的切割共有几种方式
  • 子集问题:一个N个数的集合里有多少符合条件的子集
  • 排列问题:N个数按一定规则进行全排列

每一个问题其实只要画出来树形图,基本就能清楚个大概了 ,所以建议都画画图。



组合问题

leetcode77.组合

给定两个整数 n 和 k,返回范围 [1, n] 中所有可能的 k 个数的组合。
你可以按 任何顺序 返回答案。

  • 输入:n = 4, k = 2
  • 输出:[[2,4],[3,4],[2,3],[1,2],[1,3],[1,4],]

这是我们熟悉回溯模板的第一步,现在回头看来只需要注意终止条件,直接套用模板就可以了
for 循环作为横向便利,横向遍历( backTrack )的每一个元素中都要进行深序遍历(递归)
当深序遍历到符合题目条件( if (单个结果的数组大小 = = k) ) 直接return到上一层,然后回溯(把刚刚在这一层添加的元素去掉,添加的值减掉) 然后继续遍历for循环的下一个元素,继续判断,当这一层遍历完以后,继续return,直到最上面一层的原数组遍历完成

那我们怎么知道要遍历下一层的哪一个元素呢? 我们怎么避免结果集中有重复元素呢?
这个时候就要用到 startIndex,传参时加上它可以让你遍历到上一层for循环中遍历到元素的下一个,让程序可以继续横向便利和深序遍历而不重复!

额外收获:因为回溯属于暴力遍历,所以如果能够剪枝,会让时间复杂度减少许多,从这道题看可以剪枝的地方我们就在递归中每一层的for循环所选择的起始位置,因为如果for循环选择的起始位置之后的元素个数 已经不足 我们需要的元素个数了,那么就没有必要搜索了。
k - path.size()(还需要的元素个数)<= n - i + 1(剩余的总元素)


leetcode216.组合总和III

找出所有相加之和为 n 的 k 个数的组合,且满足下列条件:

  • 只使用数字1到9
  • 每个数字 最多使用一次

返回 所有可能的有效组合的列表 。该列表不能包含相同的组合两次,组合可以以任何顺序返回。

  • 输入: k = 3, n = 9
  • 输出: [[1,2,6], [1,3,5], [2,3,4]]
  • 解释:
    1 + 2 + 6 = 9
    1 + 3 + 5 = 9
    2 + 3 + 4 = 9
    没有其他符合的组合了。

这道题跟上一道题本质没有区别,哈哈哈因为都是组合问题,只是加了一个sum变量,然后终止条件上加了一个判断就是 sum = = n


leetcode17.电话号码的字母组合

给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。答案可以按 任意顺序 返回。
给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。
请添加图片描述

  • 输入:“23”
  • 输出:[“ad”, “ae”, “af”, “bd”, “be”, “bf”, “cd”, “ce”, “cf”]

这道题相较于前两题,看本质,跟前面不同的是前面是一个数组内组合,这道题是两个不同或者两个以上的数组的组合=====》这个其实就是影响到startIndex,因为startIndex是根据上一层的startIndex来遍历接下来的同一层下个元素跟下一层的元素的,但是因为是不同数组,所以第一个数组的元素需要从0开始去遍历另外一个数组的元素!!!

我的额外收获:使用了LinkedList去删除数组(其实已经是链表了)最后一个元素的话,不需要再写
path.remove(path.size() - 1),而是直接path.removeLast(),因为链表有记录链表大小所以直接使用该方法就行,主要还是因为代码少哈哈。还有就是用用一个字符串数组的下标来表示每一个按键的值。


leetcode39.组合总和

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

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

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

这个就是在刚刚第二题的基础上 增加了元素的重复操作。那我们观察这道题,可以发现又是跟startIndex有关,因为可以元素可以重复,所以下一层的startIndex需要从本层的startIndex开始。


leetcode40.组合总和II

给定一个候选人编号的集合 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。

candidates 中的每个数字在每个组合中只能使用 一次 。
注意:解集不能包含重复的组合。

  • 输入: candidates = [10,1,2,7,6,1,5], target = 8,
  • 输出:[[1,1,6],[1,2,5],[1,7],[2,6]]

我们观察一下这道题,发现需要查重了,而且因为只是判断集合中的元素与target之间的关系,所以这道题元素之间的顺序无关,既然无关我们可以先对原数组进行排序,然后进行查重判断。怎么判断呢,因为从这道题我们可以知道,如果是在不同层不同元素但是值相同(一个结果数组中的不同下标)是可以使用的,但是在相同层(也就是不同结果中的相同下标)不能使用相同值的。
所以我们要先弄构建一个used数组,当使用一个元素之后,给used数组的该下标上的值赋值为true。当不同层但是值相同的情况下,因为used数组下标不同,并且是上一层的该下标下的used的值为true,这一层的下标下的used值为flase,我们即可以使用。 但是相同层并且值相同的情况下,上一个遍历过的元素used[i - 1]的值为false,则需要跳过,因为该层已经使用该值了。
在这里插入图片描述


切割问题

leetcode131.分割回文串

给你一个字符串 s,请你将 s 分割成一些子串,使每个子串都是 回文串 。返回 s 所有可能的分割方案。
回文串 是正着读和反着读都一样的字符串。

  • 输入:s = “aab”
  • 输出:[[“a”,“a”,“b”],[“aa”,“b”]]

这道题开始就有点区间判断那味了,就是说 你得判断一个区间内是不是回文串,那用什么来定义区间的左右两个边界呢? 刚好我们在单层判断中有一个有一个变量i,还有一个传过来的参数startIndex。因为同一层中startIndex一直是一个区间中的左边界(因为是上一层传过来的需要遍历的第一个值)而且单层遍历过程中 i 会一直 ++,所以我们让startIndex为左边界,i为右边界,然后判断是不是回文串,如果是就加到结果集,不是便continue继续 i ++。然后再进行回溯。


leetcode93.复原IP地址

这道题要注意的点是我们要怎样去写终止条件,怎么样去拼接出一个IP地址,怎么判断是不是IP地址中的一块子字符串等。
因为是IP地址,所以终止条件我们可以判断IP字符串中的 · 的个数。
拼接字符串:当判断IP地址中的子字符串符合IP地址的要求,则在该位置后面加一个 ·
判断IP地址:根据题目编写相对应代码(注意不能直接Integer.parseInt()直接强转再判断,因为00转过来是0 并且会报错)
最后这三个步骤完成后再参照上一道题的思想即可



子集问题

leetcode78.子集

给你一个整数数组 nums ,数组中的元素 互不相同 。返回该数组所有可能的子集(幂集)。
解集 不能 包含重复的子集。你可以按 任意顺序 返回解集。

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

这个好像没有什么限制,并且直接套用模板直接输出每一个节点即可。


leetcode90.子集II

给你一个整数数组 nums ,其中可能包含重复元素,请你返回该数组所有可能的子集(幂集)。
解集 不能 包含重复的子集。返回的解集中,子集可以按 任意顺序 排列。

  • 输入:nums = [1,2,2]
  • 输出:[[],[1],[1,2],[1,2,2],[2],[2,2]]

因为只需返回数组里的子集,并且可以任意顺序排列,所以直接套用上面 leetcode40.组合总和II 中的查重思想,又因为还是没啥终止条件,所以直接添加所有节点即可。


leetcode491.递增子序列

给你一个整数数组 nums ,找出并返回所有该数组中不同的递增子序列,递增子序列中 至少有两个元素 。你可以按 任意顺序 返回答案。
数组中可能含有重复元素,如出现两个整数相等,也可以视作递增序列的一种特殊情况。

实例1:

  • 输入:nums = [4,6,7,7]
  • 输出:[[4,6],[4,6,7],[4,6,7,7],[4,7],[4,7,7],[6,7],[6,7,7],[7,7]]

实例2:

  • 输入:nums = [4,4,3,2,1]
  • 输出:[[4,4]]

这道题乍一看还以为跟上一道题一样,直接套用上面查重的思想,只是终止条件修改一下即可。
但是它不能够排序,所以不能套用上面那个公式直接判断与前一个的关系。
所以就要想一下该怎么查重:

  • used数组判断元素是否在同一层有用过—>用map记录用过的元素(map里的元素不能重复)
  • 递增子序列:运用刚刚区间那两道题的思想,判断结果数组中的右边界元素与剩下元素(区间中的)即将添加进来元素的大小。

考虑这两点就能做出来了。



排列问题

全排列II

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

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

这道题不能使用startIndex,因为我们每一次遍历要从数组的第一个元素开始,然后通过used的数组来存储已经使用过的数字,然后进行回溯遍历即可。 终止条件可以看出是当结果数组 == 原数组大小。


全排列II

给定一个可包含重复数字的序列 nums ,按任意顺序 返回所有不重复的全排列。

  • 输入:nums = [1,1,2]
  • 输出:[[1,1,2],[1,2,1],[2,1,1]]

因为是包含重复数字,并且可以按照任意顺序返回,所以是前面查重的思想 + 上一道题的遍历方法。



leetcode51. N 皇后

n 皇后问题 研究的是如何将 n 个皇后放置在 n×n 的棋盘上,并且使皇后彼此之间不能相互攻击。
给你一个整数 n ,返回所有不同的 n 皇后问题 的解决方案。
每一种解法包含一个不同的 n 皇后问题 的棋子放置方案,该方案中 ‘Q’ 和 ‘.’ 分别代表了皇后和空位。

在这里插入图片描述

  • 输入:n = 4
  • 输出:[[“.Q…”,“…Q”,“Q…”,“…Q.”][“…Q.”,“Q…”,“…Q”,“.Q…”]]
  • 解释:如上图所示,4 皇后问题存在两个不同的解法。

这道题跟前面差不多,唯一的不同就是for循环中每次遍历一层,在这一层中的每个元素检查是否符合规则,如果符合规则则继续往下面遍历,知道成功或者是失败并进行回溯。
判定元素是否符合,需要判断对角(45° 135°) 以及所在列。
终止条件是当遍历完最后一列后,即返回一整个二维数组即可。


答案:

public class Test01 {

    static BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
    static BufferedWriter out = new BufferedWriter(new OutputStreamWriter(System.out));
    static List<List<String>> res = new ArrayList<>();

    public static void main(String[] args) throws IOException {

        int n = Integer.parseInt(in.readLine());
        List<List<String>> result = solveNQueens(n);

        out.flush();
        out.close();
    }

    private static List<List<String>> solveNQueens(int n) {

        if (n == 2 || n ==3) {
            return res;
        }

        //初始化棋盘
        char[][] chessboard = new char[n][n];
        for (char[] c : chessboard) {
            Arrays.fill(c, '.');
        }

        backtrack(chessboard, n, 0);

        return res;
    }

    private static void backtrack(char[][] chessboard, int n, int row) {

        if (row == n) {
            res.add(arrayToList(chessboard));
            return;
        }

        for (int col = 0; col < n; col ++) {

            if (isValid(chessboard, row, col, n)) {
                chessboard[row][col] = 'Q';
                backtrack(chessboard, n, row + 1);
                chessboard[row][col] = '.';
            }
        }
    }

    private static boolean isValid(char[][] chessboard, int row, int col, int n) {

        //检查坐标点所在的那一列
        for (int i = 0; i < row; i ++) {

            if (chessboard[i][col] == 'Q') {
                return false;
            }
        }

        //检查对角
        for (int i = row - 1, j = col - 1; i > -1 && j > -1; i --, j --) {

            if (chessboard[i][j] == 'Q') {
                return false;
            }
        }
        for (int i = row - 1, j = col + 1; i > -1 && j < n; i --, j ++) {

            if (chessboard[i][j] == 'Q') {
                return false;
            }
        }

        return true;
    }

    private static List<String> arrayToList(char[][] chessboard) {
        List<String> path = new ArrayList<>();
        for (char[] chars : chessboard) {
            path.add(String.copyValueOf(chars));
        }
        return path;
    }
}


leetcode37. 解数独

编写一个程序,通过填充空格来解决数独问题。

一个数独的解法需遵循如下规则: 数字 1-9 在每一行只能出现一次。 数字 1-9 在每一列只能出现一次。 数字 1-9 在每一个以粗实线分隔的 3x3 宫内只能出现一次。 空白格用 ‘.’ 表示。

在这里插入图片描述
在这里插入图片描述

答案被标成红色。

  • 给定的数独序列只包含数字 1-9 和字符 ‘.’ 。
  • 你可以假设给定的数独只有唯一解。
  • 给定数独永远是 9x9 形式的。

这道题跟前面有很大的不同,就是相对于之前的模板中的单层for循环处理逻辑,因为这道题需要对二维数组中的每一个空位进行遍历,所以我们单层逻辑要转换成双层for循环,可以仔细想想,进入递归以后就是对接下来的数进行判定,因为是二维数组,所以就用双层for。
然后判定元素条件跟上面的N皇后有一点相似,判断元素所在的列跟行,还有所在的九宫格。
判断终止条件不需要写,因为遍历到最后如果没有返回false的话,就返回true 即解数独成功。


答案:

public class Test02 {

    static BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
    static BufferedWriter out = new BufferedWriter(new OutputStreamWriter(System.out));
    static List<List<String>> res = new ArrayList<>();

    public static void main(String[] args) {


    }

    public void solveSudoku(char[][] board) {
        boolean flag = backtrack(board);
    }

    private boolean backtrack(char[][] board) {
        for (int i = 0; i < board.length; i ++) {
            for (int j = 0; j < board[0].length; j ++) {
                if (board[i][j] == '.') {
                    for (char k = '1'; k <= '9'; k ++) {
                        if (isValid(board, i, j, k)) {
                            board[i][j] = k;
                            if (backtrack(board)) {
                                return true;
                            }
                            board[i][j] = '.';
                        }
                    }
                    return false;
                }
            }
        }
        return true;
    }

    // 判断该元素所在的九宫格跟列跟行是否会重复
    private boolean isValid(char[][] board, int row, int col, char k) {

       // 行
       for (int i = 0; i < board.length; i ++) {

           if (board[row][i] == k) {
               return false;
           }
       }

       // 列
       for (int j = 0; j < board[0].length; j ++) {

           if (board[j][col] == k) {
               return false;
           }
       }

       int x = (row / 3) * 3;
       int y = (col / 3) * 3;
       //九宫格
       for (int i = x; i < x + 3; i ++) {

           for (int j = y; j < y + 3; j ++) {

               if (board[i][j] == k) {
                   return false;
               }
           }
       }

       return true;
    }
}

自我总结

回溯终于入门了!!

单层循环中的 i 与 startIndex递归时,不能再传错参了。。
leetcode交题不能用static 不然会报错。

大概就是这样,大家懂了吗
每一道题都在我往期的文章中有详细题解,有哪里还有模糊的可以看看
有什么不懂的评论区评论或者私信我吧!!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值