代码随想录|回溯|leetcode131,93,78,90,491,46,47,51,37

分割回文串

lass Solution {
    List<List<String>> result = new ArrayList<>();
    Deque<String> deque = new LinkedList<>();
    public List<List<String>> partition(String s) {
        backtracking(s,0);
        return result;

    }

    public void backtracking(String s, int startIndex){
        if(startIndex == s.length()){
            result.add(new ArrayList(deque));
            return;
        }

        for(int i = startIndex; i < s.length(); i++){
            if(isPalindrome(s, startIndex, i)){ //找出子串的区间范围,并判断是否为回文子串
                String str = s.substring(startIndex, i + 1);//注意左闭右开区间
                deque.addLast(str);
            }else{
                continue;
            }
            backtracking(s, i+1);
            deque.removeLast();
        }
    }

    public boolean isPalindrome(String s, int startIndex, int end){
        for(int i = startIndex, j = end; i < j; i++, j--){
            if(s.charAt(i) != s.charAt(j)){
                return false;
            }
        }
        return true;
    }
}

思路:

难点总结:

  • 切割问题可以抽象为组合问题
  • 如何模拟那些切割线 startindex就相当于分割线
  • 切割问题中递归如何终止 当startindex移动到字符串最后的时候就证明分割完毕,递归终止
  • 在递归循环中如何截取子串 子串的区间其实是【startindex,i】利用substring把子串分割出来
  • 如何判断回文 利用双指针法,从两侧同时开始遍历,出现不相等的字符返回false,否则返回true

 复原ip地址

class Solution {
    List<String> result = new ArrayList<>();
    public List<String> restoreIpAddresses(String s) {
        if(s.length() > 12) return result;
        backtrack(s, 0 ,0);
        return result;
    }

    public void backtrack(String s, int startIndex, int pointNum){
        if(pointNum == 3){//决定了树深,也是终止条件
            if(isValid(s, startIndex, s.length() - 1)){ //左闭右闭区间,判断最后一段是都合法
                result.add(s);
            }
            return;
        }

        for(int i = startIndex; i < s.length(); i++){
            if(isValid(s, startIndex, i)){ //【startindex,i】代表切割的子串
                s = s.substring(0, i+1) + "." + s.substring(i+1); //左闭右开区间
                pointNum++;
                backtrack(s, i+2, pointNum);
                pointNum--;
                s = s.substring(0,i+1) + s.substring(i+2);
            }else{
                break;
            }
        }
    }

    public Boolean isValid(String s, int start, int end){
        if(start > end){
            return false;
        }
        if(s.charAt(start) == '0' && start != end){
            return false;
        }

        int num = 0;
        for(int i = start; i <= end; i++){
            if(s.charAt(i) > '9' || s.charAt(i) < '0'){
                return false;
            }
            num = num * 10 + (s.charAt(i) - '0');
            if(num > 255){
                return false;
            }

        }
        return true;
    }
}

思路: 

难点总结:

1. 如何判断子串合法:实现了一个isvalid方法,需要特别注意的是num = num * 10 + (s.charAt(i) - '0');代码的实现过程是

  • 首先,num为0。我们首先看到字符'2',所以新的num值为0*10 + 2 = 2
  • 接下来,我们看到字符'4',所以新的num值为2*10 + 4 = 24
  • 最后,我们看到字符'8',所以新的num值为24*10 + 8 = 248

2. 递归什么时候终止:当逗点数量等于3时,就是递归的终止条件,他控制了树深。并且需要注意的是,需要检查最后一段是否是合法ip字符串

3. 新字符串中的逗点如何加入和处理:【startindex,i】代表切割的子字符串,当插入逗点时,运用substring方法,注意是左闭右开区间,所以s.substring(0, i+1) + "." + s.substring(i+1)

子集问题

class Solution {
    List<List<Integer>> result = new ArrayList<>();
    LinkedList<Integer> path = new LinkedList<>();
    public List<List<Integer>> subsets(int[] nums) {
        backtracking(nums, 0);
        return result;

    }

    private void backtracking(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]);
            backtracking(nums, i+1);
            path.removeLast();
        }

    }
}

思路:

终止条件:这道题的终止条件可以不加,因为下面的循环也可以控制递归结束,当startindex已经超过nums.length的范围,循环自己就结束了。

注意:为什么收集结果result.add(new ArrayList<>(path));在终止条件上面,因为如果在终止条件里面的话,会将最后一个结果子集漏掉。

子集II

class Solution {
   List<List<Integer>> result = new ArrayList<>();// 存放符合条件结果的集合
   LinkedList<Integer> path = new LinkedList<>();// 用来存放符合条件结果
   boolean[] used;
    public List<List<Integer>> subsetsWithDup(int[] nums) {
        if (nums.length == 0){
            result.add(path);
            return result;
        }
        Arrays.sort(nums);
        used = new boolean[nums.length];
        subsetsWithDupHelper(nums, 0);
        return result;
    }
    
    private void subsetsWithDupHelper(int[] nums, int startIndex){
        result.add(new ArrayList<>(path));
        if (startIndex >= nums.length){
            return;
        }
        for (int i = startIndex; i < nums.length; i++){
            if (i > 0 && nums[i] == nums[i - 1] && !used[i - 1]){
                continue;
            }
            path.add(nums[i]);
            used[i] = true;
            subsetsWithDupHelper(nums, i + 1);
            path.removeLast();
            used[i] = false;
        }
    }
}

思路:

这道题是组合2和子集题目的结合,主要关注于回溯的去重逻辑,分为树层去重和树根去重

 

 

 递增子序列

class Solution {
    List<List<Integer>> result = new ArrayList<>();
    List<Integer> path = new ArrayList<>();
    public List<List<Integer>> findSubsequences(int[] nums) {
        backTracking(nums, 0);
        return result;
    }
    private void backTracking(int[] nums, int startIndex){
        if(path.size() >= 2) //子集必须要有两个元素
                result.add(new ArrayList<>(path));            
        HashSet<Integer> hs = new HashSet<>(); //set在每一树层会刷新
        for(int i = startIndex; i < nums.length; i++){
            if(!path.isEmpty() &&  nums[i] < path.get(path.size() -1 ) || hs.contains(nums[i]))
                continue;
            hs.add(nums[i]);
            path.add(nums[i]);
            backTracking(nums, i + 1);
            path.remove(path.size() - 1);
        }
    }
}

 思路:

需要注意这道题

1. 收获子集必须要至少包含两个元素

2. 当前元素nums[i]必须要比path中末尾元素要小

3. 存在重复元素需要去重,也就是每一层取的元素不能一样,所以把遍历过的元素都放在一个set里面,然后每次遍历一个元素都要在set中检查是否之前已经出现过该元素。

全排列

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;
        }
    }
}

思路:

这题是排列问题,与组合问题关键的不同是允许例如[1,2,3],[3,2,1]这样的结果存在,所以我们不需要startindex来控制循环必须是否从元素的下一个开始,而是直接从i=0开始遍历就可以。还有一点需要注意的是,结果集中不允许有重复的元素出现,例如[1,1,2],所以我们需要利用一个used数组来标记每个元素的使用情况,当遍历完一个元素时,对应的used数组应该标记为1,然后下一层遍历我们就如果当前的元素被使用过,我们就直接跳过continue,遇到没有被标记的元素我们再加入到path里

全排列II 

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;//回溯
            }
        }
    }
}

思路:

这道题和上一个题目唯一的区别是,这道题要求输出所有不重复的全排列,所以代码上只多了两行树层去重的逻辑。

n皇后

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;
    }
}

思路:

如何判断棋盘格是否合法

  1. 不能同行
  2. 不能同列
  3. 不能同斜线 (45度和135度角)左上角,右下角

为什么没有在同行进行检查呢?

因为在单层搜索的过程中,每一层递归,只会选for循环(也就是同一行)里的一个元素,所以不用去重了。

递归终止条件:当rows等于棋盘格的长度的时候,表示已经遍历到最后一行,即为终止。

解数独 

class Solution {
    // 主函数,开始解数独问题
    public void solveSudoku(char[][] board) {
        solveSudokuHelper(board);
    }

    // 递归解数独辅助函数
    private boolean solveSudokuHelper(char[][] board){
        // 两层for循环用于遍历棋盘上的每一个格子
        for (int i = 0; i < 9; i++){ // 遍历行
            for (int j = 0; j < 9; j++){ // 遍历列
                // 如果当前格子已经填了数字(不是'.'),则跳过
                if (board[i][j] != '.'){
                    continue;
                }
                // 对当前格子尝试放入'1'到'9'
                for (char k = '1'; k <= '9'; k++){
                    // 判断数字k是否可以放到当前格子
                    if (isValidSudoku(i, j, k, board)){
                        board[i][j] = k; // 放入数字
                        // 递归尝试填下一个格子
                        if (solveSudokuHelper(board)){
                            return true; // 如果下面的填法都对,则这样填当前格子是正确的
                        }
                        board[i][j] = '.'; // 如果下面的填法不对,撤回k,尝试下一个数字
                    }
                }
                // 如果1-9都尝试过了都不行,说明前面的格子填法有问题,需要回溯
                return false;
            }
        }
        // 所有的格子都填满了,说明找到了解答
        return true;
    }

    // 检查数字val放在棋盘的(row, col)处是否是合法的
    private boolean isValidSudoku(int row, int col, char val, char[][] board){
        // 检查行是否有重复
        for (int i = 0; i < 9; i++){
            if (board[row][i] == val){
                return false;
            }
        }
        // 检查列是否有重复
        for (int j = 0; j < 9; j++){
            if (board[j][col] == val){
                return false;
            }
        }
        // 检查3x3的方格是否有重复
        int startRow = (row / 3) * 3;
        int startCol = (col / 3) * 3;
        for (int i = startRow; i < startRow + 3; i++){
            for (int j = startCol; j < startCol + 3; j++){
                if (board[i][j] == val){
                    return false;
                }
            }
        }
        return true; // 在行、列和3x3方格都没有重复,所以是合法的
    }
}
  1. 用行号(row)除以3来确定这个位置在哪一个大的行区间(0-2、3-5、或6-8)。
  2. 用列号(col)除以3来确定这个位置在哪一个大的列区间(0-2、3-5、或6-8)。

代码中的(row / 3) * 3(col / 3) * 3实际上就是在做上述的操作,以便确定3x3九宫格的起始位置。

startRow为例:

  • 如果row是0、1、或2,那么row / 3的结果是0,乘以3之后还是0,表示这个3x3九宫格的起始行是0。
  • 如果row是3、4、或5,那么row / 3的结果是1,乘以3之后变成3,表示这个3x3九宫格的起始行是3。
  • 如果row是6、7、或8,那么row / 3的结果是2,乘以3之后变成6,表示这个3x3九宫格的起始行是6。

当我们可以一直从1-9的数字中找到一个数填棋盘格时,我们就一直递归返回true,当我们最终把棋盘格填满时, 我们就退出了循环,所以返回true

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值