备战秋招DAY3-今日力扣-回溯

刚刷完二叉树,里面包含了很多递归的思想。索性今天趁热打铁来看看回溯算法吧~

在做题之前呢,先回顾一下回溯的知识,具体可以参考卡哥的代码随想录,我认为讲的还是非常清晰的!

回溯法,一般可以解决如下几种问题:

  • 组合问题:N个数里面按一定规则找出k个数的集合
  • 切割问题:一个字符串按一定规则有几种切割方式
  • 子集问题:一个N个数的集合里有多少符合条件的子集
  • 排列问题:N个数按一定规则全排列,有几种排列方式
  • 棋盘问题:N皇后,解数独等等

 

了解了以上知识后,我们就开始今日份拷打吧!

1.全排列(难度等级:中等)

这道题算是基础题啦,需要考虑的点是,数组中同一个元素是不能重复选取的。那么我们怎么在回溯过程中得知,某个元素是否已经被选取过了呢?这里我们使用一个LinkedList<Integer>来维护。

class Solution {
    List<List<Integer>> res = new ArrayList<>();
    LinkedList<Integer> path = new LinkedList<>(); //存放已经操作过的元素

    public List<List<Integer>> permute(int[] nums) {
        if(nums.length==0) return res;
        backtracking(nums, path);
        return res;
    }
    public void backtracking(int[] nums, LinkedList<Integer> list){
        if(list.size() == nums.length){
            res.add(new ArrayList<>(list));
            return;
        }
        for(int i=0; i<nums.length; i++){
            //处理节点,注意选过的节点不可重复选取
            if(list.contains(nums[i])){
                continue;
            }
            list.add(nums[i]);
            backtracking(nums, list);
            //回溯,撤销处理结果
            list.removeLast();
        }
    }
}

2.子集(难度等级:中等)

这道题相当于上一道题的升级版,因为除了需要考虑同一个元素不能重复取的情况,还需要考虑解集不能包含重复子集,即[1,2]和[2,1]不能重复出现。我们这里采取的方式是通过限制开始索引来维护,具体逻辑可以参考卡哥这幅图:

class Solution {
    List<List<Integer>> res = new ArrayList<>();
    LinkedList<Integer> path = new LinkedList<>();

    public List<List<Integer>> subsets(int[] nums) {
        if(nums.length==0) return res;
        backtrackinig(nums, path, 0);
        return res;
    }

    public void backtrackinig(int[] nums, LinkedList<Integer> path, int startIndex){
    
        res.add(new ArrayList<>(path));
        
        for(int i=startIndex; i<nums.length; i++){
            if(path.contains(nums[i])){
                continue;
            }
            path.add(nums[i]);
            startIndex++;
            backtrackinig(nums, path, startIndex);
            path.removeLast();
        }
    }
}

3.电话号码的字母组合(难度等级:中等)

其实这道题和上一道很类似,都是属于组合问题,只不过这里需要加上返回条件。本题难点其实在于怎么把数字和英文字母的关系表达明确。

class Solution {
    List<String> res = new ArrayList<>();
    StringBuilder sb = new StringBuilder();
    public List<String> letterCombinations(String digits) {
        if (digits == null || digits.length() == 0) {
            return res;
        }
        String[] numString = {"", "", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz"};
        backtracking(numString, digits, 0);
        return res;

    }

    public void backtracking(String[] numString, String digits, int index){
        if(index==digits.length()){
            res.add(sb.toString());
            return;
        }
        //取当前操作数字
        int num = digits.charAt(index)-'0';
        String eng = numString[num];

        for(int i=0; i<eng.length(); i++){
            //操作当前节点
            sb.append(eng.charAt(i));
            backtracking(numString, digits, index+1);
            sb.deleteCharAt(sb.length()-1); //回溯时删去末尾元素
        }
    }
}

4. 组合总和(难度等级:中等)

这道题本质还是组合问题,但需要注意的是同一个元素是可以重复出现的!即之前我们在for循环里处理当前元素是否已经被获取过的逻辑,在这道题里不需要了。同时,我们还要思考是否有剪枝策略呢?既然题目里提到了元素都是大于等于2的(没有负值出现),那么是不是只要当前组合的和大于目标值,我们就可以直接return了呀~

class Solution {
    List<List<Integer>> res = new ArrayList<>();
    LinkedList<Integer> path = new LinkedList<>();

    public List<List<Integer>> combinationSum(int[] candidates, int target) {
        backtracking(candidates, target, path, 0, 0);
        return res;
    }
    public void backtracking(int[] candidates, int target, LinkedList<Integer> path, int sum, int startIndex){
        if(sum == target){
            res.add(new ArrayList<>(path));
            return;
        }
        if(sum > target){
            return;
        }
        for(int i=startIndex; i<candidates.length; i++){
            //处理当前节点
            sum += candidates[i];
            path.add(candidates[i]);
            backtracking(candidates, target, path, sum, i);
            sum -= candidates[i];
            path.removeLast();
        }
    }
}

5.括号生成(难度等级:中等)

这道题初看其实很难看出回溯的影子,但确实又属于排列问题。只不过,这里排列的元素不是数字,而是左括号和右括号。此外,这里的排列是有特定规则的,即满足题目所述的条件。那么我们怎么将所谓的规则用代码体现呢?这里可以用当前已遍历元素种左括号和右括号的数量来限制。

class Solution {
    List<String> res = new ArrayList<>();
    public List<String> generateParenthesis(int n) {
        backtracking(new StringBuilder(), 0, 0, n);
        return res;
    }
    public void backtracking(StringBuilder cur, int open, int close, int n){
        if(cur.length()==n*2){
            res.add(cur.toString());
            return;
        }
        //若左括号数满足规则
        if(open < n){
            cur.append('(');
            backtracking(cur, open+1, close, n);
            cur.deleteCharAt(cur.length()-1);
        }
        //若右括号数满足规则
        if(close < open){
            cur.append(')');
            backtracking(cur, open, close+1, n);
            cur.deleteCharAt(cur.length()-1);
        }
    }
}

 6.单词搜索(难度等级:中等)

其实这道题也属于排列问题,但难点在于:1)二维数组的元素如何遍历;2) 如何表示二维数组的元素是否被使用过。二维数组遍历我们一般通过构造一个direction数组来实现:int[][] directions = {{0, 1}, {0, -1}, {1, 0}, {-1, 0}}分别表示4个行径方向。至于元素是否被使用过,通过一个boolean类型的二维数组来体现。另外,需要注意的是起点并不一定是(0,0),所以我们需要遍历所有情况,

class Solution {
    public boolean exist(char[][] board, String word) {
        int h = board.length, w = board[0].length;
        boolean[][] visited = new boolean[h][w]; //判断某个元素是否已被使用过
        for (int i = 0; i < h; i++) {
            for (int j = 0; j < w; j++) {
                //起点不同
                boolean flag = check(board, visited, i, j, word, 0);
                if (flag) {
                    return true;
                }
            }
        }
        return false;
    }

    public boolean check(char[][] board, boolean[][] visited, int i, int j, String s, int k) {
        if (board[i][j] != s.charAt(k)) {
            return false;
        } else if (k == s.length() - 1) {
            return true;
        }
        visited[i][j] = true;
        int[][] directions = {{0, 1}, {0, -1}, {1, 0}, {-1, 0}}; //四个前进方向
        boolean result = false;
        for (int[] dir : directions) {
            int newi = i + dir[0], newj = j + dir[1];
            if (newi >= 0 && newi < board.length && newj >= 0 && newj < board[0].length) {
                if (!visited[newi][newj]) {
                    boolean flag = check(board, visited, newi, newj, s, k + 1);
                    if (flag) {
                        result = true;
                        break;
                    }
                }
            }
        }
        visited[i][j] = false;
        return result;
    }
}

7.分割回文串(难度等级:中等)

这道题也是最近大厂的常考题了~其实问题的逻辑无非在于1.分割 2.判断是否为回文串。需要注意的点也就是分割的操作了,其他跟之前的逻辑都差不多~

class Solution {
    List<List<String>> res = new ArrayList<>();
    List<String> path = new LinkedList<>();
    public List<List<String>> partition(String s) {
        backtracking(s, 0);
        return res;
    }
    public void backtracking(String s, int stratIndex){
        if(stratIndex >= s.length()){
            res.add(new ArrayList(path));
            return;
        }
        for(int i=stratIndex; i<s.length(); i++){
            if(isHuiWen(s, stratIndex, i)){
                String str = s.substring(stratIndex, i+1);
                path.add(str);
                backtracking(s, i+1);
                path.removeLast();
            }else{
                continue;
            }
        }
    }
    public boolean isHuiWen(String s, int start, int end){
        for(int i=start, j=end; i<j; i++,j--){
            if(s.charAt(i)!=s.charAt(j)){
                return false;
            }
        }
        return true;
    }
}

8.N 皇后(难度等级:困难)

这道题逻辑上可以分为三步:1.判断当前位置是否会与已有位置发生攻击;2.放置皇后;3.生成固定格式的结果。难点就在于如何表示当前位置是否会与已有位置发生攻击,即同一列、斜线怎么表示。这里给出空间占用比较高,但是较好理解的答案:

class Solution {
    public List<List<String>> solveNQueens(int n) {
        List<List<String>> solutions = new ArrayList<List<String>>();
        int[] queens = new int[n];
        Arrays.fill(queens, -1);
        Set<Integer> columns = new HashSet<Integer>(); //每一列是否有元素
        Set<Integer> diagonals1 = new HashSet<Integer>(); //从左下到右上的斜线是否有元素
        Set<Integer> diagonals2 = new HashSet<Integer>(); //从左上到右下的斜线上是否有元素
        backtrack(solutions, queens, n, 0, columns, diagonals1, diagonals2);
        return solutions;
    }

    public void backtrack(List<List<String>> solutions, int[] queens, int n, int row, Set<Integer> columns, Set<Integer> diagonals1, Set<Integer> diagonals2) {
        if (row == n) {
            List<String> board = generateBoard(queens, n);
            solutions.add(board);
        } else {
            for (int i = 0; i < n; i++) {
                if (columns.contains(i)) {
                    continue;
                }
                int diagonal1 = row - i;
                if (diagonals1.contains(diagonal1)) {
                    continue;
                }
                int diagonal2 = row + i;
                if (diagonals2.contains(diagonal2)) {
                    continue;
                }
                queens[row] = i;
                columns.add(i);
                diagonals1.add(diagonal1);
                diagonals2.add(diagonal2);
                backtrack(solutions, queens, n, row + 1, columns, diagonals1, diagonals2);
                queens[row] = -1;
                columns.remove(i);
                diagonals1.remove(diagonal1);
                diagonals2.remove(diagonal2);
            }
        }
    }
    //生成结果
    public List<String> generateBoard(int[] queens, int n) {
        List<String> board = new ArrayList<String>();
        for (int i = 0; i < n; i++) {
            char[] row = new char[n];
            Arrays.fill(row, '.');
            row[queens[i]] = 'Q';
            board.add(new String(row));
        }
        return board;
    }
}

【总结】

其实对于回溯问题,如果你能把树结构画出来,基本就能解答了。回溯问题通常还需要考虑的几个因素以及对应代码我也总结了一下:

1.同一个元素不能重复取

2.子集不能重复,即[1, 2, 2]与[2,1,2]不能重复放进答案

3.进行适当的剪枝,以降低时间复杂度

  • 9
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
力扣是一个在线编程平台,提供了大量的算法题目,可以帮助程序员提高算法能力。回溯算法是一种搜索算法,它通过不断地尝试所有可能的解来求解问题。在回溯算法中,我们首先定义一个解空间,然后从解空间中搜索所有可能的解,直到找到符合要求的解为止。回溯算法通常用于求解组合问题、排列问题、子集问题等。 在 Java 中实现回溯算法,通常需要定义一个递归函数来搜索解空间。在递归函数中,我们首先判断当前状态是否符合要求,如果符合要求,则将当前状态加入到解集中;否则,我们继续搜索下一个状态。在搜索下一个状态时,我们需要对当前状态进行一些修改,然后递归调用自身来搜索下一个状态。当搜索完所有可能的状态后,我们需要回溯到上一个状态,继续搜索其他可能的状态。 以下是回溯算法的一般步骤: 1. 定义解空间:确定问题的解空间,并定义一个数据结构来表示解空间中的每个状态。 2. 确定约束条件:确定哪些状态是合法的,并定义一个函数来判断当前状态是否符合要求。 3. 确定搜索策略:确定搜索解空间的顺序,并定义一个函数来生成下一个状态。 4. 搜索解空间:使用递归函数搜索解空间,如果当前状态符合要求,则将其加入到解集中;否则,继续搜索下一个状态。 5. 回溯:当搜索完所有可能的状态后,回溯到上一个状态,继续搜索其他可能的状态。 以下是一个力扣题目的回溯算法 Java 实现示例: ``` class Solution { List<List<Integer>> res = new ArrayList<>(); List<Integer> path = new ArrayList<>(); public List<List<Integer>> subsets(int[] nums) { dfs(nums, 0); return res; } private void dfs(int[] nums, int start) { res.add(new ArrayList<>(path)); for (int i = start; i < nums.length; i++) { path.add(nums[i]); dfs(nums, i + 1); path.remove(path.size() - 1); } } } ``` 该算法用于求解给定数组的所有子集。在递归函数中,我们首先将当前状态加入到解集中,然后从当前位置开始搜索下一个状态。在搜索下一个状态时,我们将当前元素加入到路径中,并递归调用自身来搜索下一个状态。当搜索完所有可能的状态后,我们需要回溯到上一个状态,继续搜索其他可能的状态。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值