leetcode-23.03.22-回溯

回溯:寻找所有的可行解,如果发现一个解不可行就舍弃。

  • 回溯与递归都要注意剪枝,避免重复计算

  • 回溯的问题,一定要把整个树图画出来,然后再去考虑如何缩小问题,还要注意恢复状态

void backtrack(已经做的选择, 选择列表):
    if 当前达到了结束位置:
        result.push(最终的选择序列)
    for 每个选择 in 选择列表:
        做选择 //可能这里需要判断选择是否可行
        backtrack(新的已选择, 新的选择列表) //这里的已选择和选择列表都要更新
        撤销选择

//在此基础上,如果部分节点已经无解,则将它能够推理出的无解的树枝均剪掉

一、回溯:排列、组合、子集问题 

17. 电话号码的字母组合

给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。答案可以按 任意顺序 返回。

给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。

示例 1:

输入:digits = "23"
输出:["ad","ae","af","bd","be","bf","cd","ce","cf"]
class Solution {
    public List<String> letterCombinations(String digits) {
        List<String> combinations = new ArrayList();
        if(digits.equals("")) return combinations;
        Map<Character, String> keyboard = new HashMap(){{
            put('2', "abc");
            put('3', "def");
            put('4', "ghi");
            put('5', "jkl");
            put('6', "mno");
            put('7', "pqrs");
            put('8', "tuv");
            put('9', "wxyz");
        }};
        backtrace(combinations,keyboard,digits,0,new StringBuffer());
        return combinations;
    }
    
    private void backtrace(List<String> combinations, Map<Character,String> keyboard, String digits,int index, StringBuffer combination){
        if(index == digits.length()) combinations.add(combination.toString());
        else {
            char digit = digits.charAt(index);
            String letters = keyboard.get(digit);
            for(int i=0;i<letters.length();++i){
                combination.append(letters.charAt(i));
                backtrace(combinations,keyboard,digits,index+1,combination);
                combination.delete(index,index+1);
            }
        }
    }
}

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]]
class Solution {
    public List<List<Integer>> permute(int[] nums) {
        int len = nums.length;
        boolean[] used = new boolean[len];
        List<List<Integer>> res = new ArrayList();
        int[] tmp = new int[len];
        backtrace(res,nums,tmp,used,0);
        return res;
    }

    private void backtrace(List<List<Integer>> res,int[] nums, int[] tmp,boolean[] used,int index){
        if(index == nums.length){
            List<Integer> arr = new ArrayList();
            for(int i : tmp) arr.add(i);
            res.add(arr);
            return;
        }
        for(int i=0;i<nums.length;++i){
            if(!used[i]){
                used[i] = true;
                tmp[index] = nums[i];
                backtrace(res,nums,tmp,used,index+1);
                used[i] = false;
            }
        }
    }
}

 思考:我们可不可以移除这个标记数组,因为这个标记数组会增加算法的空间复杂度。考虑将存储候选数字的列表划分为左右两部分,左边是选中的数字,右边是还没有被选的数字,那么我们每次只需要考虑在右侧的列表中选哪个,选中后递归,递归结束后再撤销本次选中操作。

class Solution {
    public List<List<Integer>> permute(int[] nums) {
        int len = nums.length;
        List<List<Integer>> res = new ArrayList();
        List<Integer> arr = new ArrayList();
        for(int i : nums) arr.add(i);
        backtrace(res,arr,len,0);
        return res;
    }

    private void backtrace(List<List<Integer>> res, List<Integer> arr, int len, int toFill){
        if(toFill == len){
            res.add(new ArrayList(arr));
            return;
        }
        for(int i=toFill;i<len;++i){
            Collections.swap(arr, toFill, i);
            backtrace(res,arr,len,toFill+1);
            Collections.swap(arr, toFill, i);
        }
    }
}

与动态规划的区别


共同点: 用于求解多阶段决策问题。多阶段决策问题即:

1. 求解一个问题分为很多步骤(阶段);
2. 每一个步骤(阶段)可以有多种选择。

不同点:


1. 动态规划只需要求我们评估最优解是多少,最优解对应的具体解是什么并不要求。因此很适合应用于评估一个方案的效果;
2. 回溯算法可以搜索得到所有的方案(当然包括最优解),但是本质上它是一种遍历算法,时间复杂度很高

47. 全排列 II

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

示例 1:

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

思路:仍然沿用【46.全排列】的思路但是要在此基础上判断重复数字带来的问题,考虑使用回标记数组而非原地划分的方式进行回溯,为了实现不重复的排列,我们就要在当前要填入的位置toFill上保持一个原则:该位置只会被相同的数字填充一次。为了方便我们实现这个原则,我们可以对数组排序,这样撤销操作并进行下一次的选择时可以通过向后滑动来轻松解决。

if(i>0 && nums[i] == nums[i-1] && !used[i-1]) continue;

所以只需要在【46.全排列】的回溯过程中做选择之前添加一个条件判断即可。

78. 子集

给你一个整数数组 nums ,数组中的元素 互不相同 。返回该数组所有可能的子集(幂集)。

解集 不能 包含重复的子集。你可以按 任意顺序 返回解集。

示例 1:

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

class Solution {

    List<List<Integer>> res = new ArrayList();
    List<Integer> tmp = new ArrayList();

    public List<List<Integer>> subsets(int[] nums) {
        backtrace(nums,0);
        return res;
    }

    private void backtrace(int[] nums,int index){
        if(index == nums.length){
            res.add(new ArrayList(tmp));
            return;
        }
        tmp.add(nums[index]);
        backtrace(nums, index+1);
        tmp.remove(tmp.size()-1);
        backtrace(nums, index+1);
    }
}

90. 子集 II

给你一个整数数组 nums ,其中可能包含重复元素,请你返回该数组所有可能的子集(幂集)。

解集 不能 包含重复的子集。返回的解集中,子集可以按 任意顺序 排列。

示例 1:

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

思路:排序去重

    private void backtrace(boolean choosePre, int[] nums, int index){
        if(index == nums.length){
            res.add(new ArrayList(tmp));
            return;
        }
        backtrace(false,nums,index+1);
        if (!choosePre && index > 0 && nums[index - 1] == nums[index]) return;
        tmp.add(nums[index]);
        backtrace(true,nums,index+1);
        tmp.remove(tmp.size()-1);
    }

77. 组合

给定两个整数 n 和 k,返回范围 [1, n] 中所有可能的 k 个数的组合。

你可以按 任何顺序 返回答案。

示例 1:

输入:n = 4, k = 2
输出:
[
  [2,4],
  [3,4],
  [2,3],
  [1,2],
  [1,3],
  [1,4],
]
class Solution {

    List<List<Integer>> res = new ArrayList();
    List<Integer> tmp = new ArrayList();

    public List<List<Integer>> combine(int n, int k) {
        backtrace(n,k,1,0);
        return res;
    }

    private void backtrace(int n, int k, int index, int count){
        // 剪枝:temp 长度加上区间 [cur, n] 的长度小于 k,不可能构造出长度为 k 的 temp
        // 增加剪枝后,运行时间由17ms减小到2ms
        if (tmp.size() + (n - index + 1) < k) return;
        if(count == k) res.add(new ArrayList(tmp));
        for(int i=index;i<=n;++i){
            tmp.add(i);
            ++count;
            backtrace(n,k,i+1,count);
            tmp.remove(tmp.size() - 1);
            --count;
        }
    }
}

事实上,【子集】、【组合】相关问题的leetcode官方题解中并没有提到回溯算法,而只是说使用递归的方法,递归的函数也并没有命名为`backtrace()`而是使用了一个让人意想不到的名称:`dfs()`深度优先搜索。思考这个问题发现,回溯是不是可以看成一种对深度优先搜索算法的抽象,二者都通过“回退”的手段来实现对所有可能的“解”的遍历,dfs是为了遍历一棵树,backtrace则是使用这种通过回退来实现遍历的思想搜索可行解。

而且对于子集、组合问题的解结构,也确实是可以用二叉树模拟的,只不过我们并不需要把这个二叉树构建出来而已。

对于【子集】问题,我们可以构造出这样一个二叉树,所有从root走到叶子节点的路径全部是正确结果:

 对于【组合】问题,也按照类似的思路构造二叉树,唯一不同的是,组合问题的二叉树确定结果应该由“经过的有效的节点数量”确定,而非延展到每一个叶子节点,所以可以提前判断一些出“从此之后再不可能产生可行解”的节点舍去,从而达到剪枝(降低运行时间)的目的:

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 。
仅有这两种组合。
class Solution {

    List<List<Integer>> res = new ArrayList();
    List<Integer> tmp = new ArrayList();

    public List<List<Integer>> combinationSum(int[] candidates, int target) {
        backtrace(candidates,target,0,0);
        return res;
    }

    private void backtrace(int[] candidates, int target, int begin, int sum){
        if(sum > target) return;
        if(sum == target) {
            res.add(new ArrayList(tmp));
            return;
        }
        for(int i=begin;i<candidates.length;++i){
            tmp.add(candidates[i]);
            sum += candidates[i];
            backtrace(candidates,target,i,sum);
            sum -= candidates[i];
            tmp.remove(tmp.size()-1);
        }
    }
}

 或者沿用【子集】【组合】中的“选或者不被选”的想法:

class Solution {
    public List<List<Integer>> combinationSum(int[] candidates, int target) {
        List<List<Integer>> ans = new ArrayList<List<Integer>>();
        List<Integer> combine = new ArrayList<Integer>();
        dfs(candidates, target, ans, combine, 0);
        return ans;
    }

    public void dfs(int[] candidates, int target, List<List<Integer>> ans, List<Integer> combine, int idx) {
        if (idx == candidates.length) {
            return;
        }
        if (target == 0) {
            ans.add(new ArrayList<Integer>(combine));
            return;
        }
        // 直接跳过
        dfs(candidates, target, ans, combine, idx + 1);
        // 选择当前数
        if (target - candidates[idx] >= 0) {
            combine.add(candidates[idx]);
            dfs(candidates, target - candidates[idx], ans, combine, idx);
            combine.remove(combine.size() - 1);
        }
    }
}

40. 组合总和 II

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

candidates 中的每个数字在每个组合中只能使用 一次 。

注意:解集不能包含重复的组合。 

示例 1:

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

思路:排序去重

93. 复原 IP 地址

有效 IP 地址 正好由四个整数(每个整数位于 0 到 255 之间组成,且不能含有前导 0),整数之间用 '.' 分隔。

  • 例如:"0.1.2.201" 和 "192.168.1.1" 是 有效 IP 地址,但是 "0.011.255.245""192.168.1.312" 和 "192.168@1.1" 是 无效 IP 地址。

给定一个只包含数字的字符串 s ,用以表示一个 IP 地址,返回所有可能的有效 IP 地址,这些地址可以通过在 s 中插入 '.' 来形成。你 不能 重新排序或删除 s 中的任何数字。你可以按 任何 顺序返回答案。

示例 1:

输入:s = "25525511135"
输出:["255.255.11.135","255.255.111.35"]

 二、回溯:Flood Fill

733. 图像渲染

有一幅以 m x n 的二维整数数组表示的图画 image ,其中 image[i][j] 表示该图画的像素值大小。

你也被给予三个整数 sr ,  sc 和 newColor 。你应该从像素 image[sr][sc] 开始对图像进行 上色填充 。

为了完成 上色工作 ,从初始像素开始,记录初始坐标的 上下左右四个方向上 像素值与初始坐标相同的相连像素点,接着再记录这四个方向上符合条件的像素点与他们对应 四个方向上 像素值与初始坐标相同的相连像素点,……,重复该过程。将所有有记录的像素点的颜色值改为 newColor 。

最后返回 经过上色渲染后的图像 

示例 1:

输入: image = [[1,1,1],[1,1,0],[1,0,1]],sr = 1, sc = 1, newColor = 2
输出: [[2,2,2],[2,2,0],[2,0,1]]
解析: 在图像的正中间,(坐标(sr,sc)=(1,1)),在路径上所有符合条件的像素点的颜色都被更改成2。
注意,右下角的像素没有更改为2,因为它不是在上下左右四个方向上与初始点相连的像素点。
class Solution {

    public int[][] floodFill(int[][] image, int sr, int sc, int color) {
        if(color == image[sr][sc]) return image;
        backtrace(image,sr,sc,color,image[sr][sc]);
        return image;
    }

    private void backtrace(int[][] image, int sr, int sc, int color, int srcColor){
        // 边界检查
        if(sr<0||sc<0||sr>=image.length||sc>=image[0].length) return;
        // 剪枝
        if(image[sr][sc] != srcColor) return;
        image[sr][sc] = color;
        // 检查上下左右
        backtrace(image,sr-1,sc,color,srcColor);
        backtrace(image, sr+1,sc,color,srcColor);
        backtrace(image,sr,sc-1,color,srcColor);
        backtrace(image,sr,sc+1,color,srcColor);
    }
}

200. 岛屿数量

给你一个由 '1'(陆地)和 '0'(水)组成的的二维网格,请你计算网格中岛屿的数量。

岛屿总是被水包围,并且每座岛屿只能由水平方向和/或竖直方向上相邻的陆地连接形成。

此外,你可以假设该网格的四条边均被水包围。

示例 1:

输入:grid = [
  ["1","1","1","1","0"],
  ["1","1","0","1","0"],
  ["1","1","0","0","0"],
  ["0","0","0","0","0"]
]
输出:1
class Solution {
    public int numIslands(char[][] grid) {
        boolean[][] visited = new boolean[grid.length][grid[0].length];
        int islandNum = 0;
        for(int i=0;i<grid.length;++i){
            for(int j=0;j<grid[0].length;++j){
                if(grid[i][j] == '1'){
                    ++islandNum;
                    backtrace(grid,i,j);
                }
            }
        }
        return islandNum;
    }

    private void backtrace(char[][] grid, int i, int j){
        if(i<0||j<0||i>=grid.length||j>=grid[0].length) return;
        if(grid[i][j] == '0') return;
        grid[i][j] = '0';
        backtrace(grid,i-1,j);
        backtrace(grid,i+1,j);
        backtrace(grid,i,j-1);
        backtrace(grid,i,j+1);
    }
}

 实际上,这并不应该称之为回溯,而更应该使用【深度优先遍历】来描述这个算法,因为“岛屿”实际上是抽象数据结构【网络】的一个具现,我们可以使用DFS来实现【树】的遍历,这里实际上是对【DFS向网络数据结构的扩展】,相关的岛屿问题有:【200.岛屿数量】、【463.岛屿周长】、【695.岛屿最大面积】、【827.最大人工岛】,都可以使用深度优先搜索实现。

130. 被围绕的区域

给你一个 m x n 的矩阵 board ,由若干字符 'X' 和 'O' ,找到所有被 'X' 围绕的区域,并将这些区域里所有的 'O' 用 'X' 填充。

示例 1:

输入:board = [["X","X","X","X"],["X","O","O","X"],["X","X","O","X"],["X","O","X","X"]]
输出:[["X","X","X","X"],["X","X","X","X"],["X","X","X","X"],["X","O","X","X"]]
解释:被围绕的区间不会存在于边界上,换句话说,任何边界上的 'O'都不会被填充为 'X'。 任何不在边界上,或不与边界上的 'O' 相连的 'O' 最终都会被填充为 'X'。如果两个元素在水平或垂直方向相邻,则称它们是“相连”的。 
class Solution {
    public void solve(char[][] board) {
        boolean[][] reserve = new boolean[board.length][board[0].length];
        // 处理四个边界
        for(int i=0;i<board[0].length;++i) {
            if(board[0][i]=='O') touch(board,0,i,reserve);
            if(board[board.length-1][i]=='O') touch(board,board.length-1,i,reserve);
        } 
        for(int i=0;i<board.length;++i) {
            if(board[i][0]=='O') touch(board,i,0,reserve);
            if(board[i][board[0].length-1]=='O') touch(board,i,board[0].length-1,reserve);
        }
        // 吞并
        for(int i=1;i<board.length-1;++i){
            for(int j=1;j<board[0].length-1;++j){
                if(!reserve[i][j]) board[i][j] = 'X';
            }
        }
    }

    private void touch(char[][] board,int i,int j,boolean[][] reserve){
        if(i<0||j<0||i>board.length-1||j>board[0].length-1) return;
        if(board[i][j] == 'X' || reserve[i][j] == true) return;
        reserve[i][j] = true;
        touch(board,i-1,j,reserve);
        touch(board,i+1,j,reserve);
        touch(board,i,j-1,reserve);
        touch(board,i,j+1,reserve);
    }
}

79. 单词搜索

给定一个 m x n 二维字符网格 board 和一个字符串单词 word 。如果 word 存在于网格中,返回 true ;否则,返回 false 。

单词必须按照字母顺序,通过相邻的单元格内的字母构成,其中“相邻”单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母不允许被重复使用。

示例 1:

输入:board = [["A","B","C","E"],["S","F","C","S"],["A","D","E","E"]], word = "ABCCED"
输出:true
class Solution {

    private boolean wordExist = false;

    public boolean exist(char[][] board, String word) {
        boolean[][] visited = new boolean[board.length][board[0].length];
        for(int i=0;i<board.length;++i){
            for(int j=0;j<board[0].length;++j){
                dfs(board,i,j,word,0,visited);
            }
        }
        return wordExist;
    }

    private void dfs(char[][] board, int i, int j, String word, int idx,boolean[][] visited){
        if(idx == word.length()){
            wordExist = true;
            return;
        }
        if(i<0||j<0||i>=board.length||j>=board[0].length) return;
        if(board[i][j] != word.charAt(idx) || visited[i][j]) return;
        visited[i][j] = true;
        dfs(board,i-1,j,word,idx+1,visited);
        dfs(board,i+1,j,word,idx+1,visited);
        dfs(board,i,j-1,word,idx+1,visited);
        dfs(board,i,j+1,word,idx+1,visited);
        visited[i][j] = false;
    }
}

### 回答1: 好的,我来用中文回复这个链接:https://leetcode-cn.com/tag/dynamic-programming/ 这个链接是 LeetCode 上关于动态规划的题目集合。动态规划是一种常用的算法思想,可以用来解决很多实际问题,比如最长公共子序列、背包问题、最短路径等等。在 LeetCode 上,动态规划也是一个非常重要的题型,很多题目都需要用到动态规划的思想来解决。 这个链接里包含了很多关于动态规划的题目,按照难度从简单到困难排列。每个题目都有详细的题目描述、输入输出样例、题目解析和代码实现等内容,非常适合想要学习动态规划算法的人来练习和提高自己的能力。 总之,这个链接是一个非常好的学习动态规划算法的资源,建议大家多多利用。 ### 回答2: 动态规划是一种算法思想,通常用于优化具有重叠子问题和最优子结构性质的问题。由于其成熟的数学理论和强大的实用效果,动态规划在计算机科学、数学、经济学、管理学等领域均有重要应用。 在计算机科学领域,动态规划常用于解决最优化问题,如背包问题、图像处理、语音识别、自然语言处理等。同时,在计算机网络和分布式系统中,动态规划也广泛应用于各种优化算法中,如链路优化、路由算法、网络流量控制等。 对于算法领域的程序员而言,动态规划是一种必要的技能和知识点。在LeetCode这样的程序员平台上,题目分类和标签设置十分细致和方便,方便程序员查找并深入学习不同类型的算法LeetCode的动态规划标签下的题目涵盖了各种难度级别和场景的问题。从简单的斐波那契数列、迷宫问题到可以用于实际应用的背包问题、最长公共子序列等,难度不断递进且话题丰富,有助于开发人员掌握动态规划的实际应用技能和抽象思维模式。 因此,深入LeetCode动态规划分类下的题目学习和练习,对于程序员的职业发展和技能提升有着重要的意义。 ### 回答3: 动态规划是一种常见的算法思想,它通过将问题拆分成子问题的方式进行求解。在LeetCode中,动态规划标签涵盖了众多经典和优美的算法问题,例如斐波那契数列、矩阵链乘法、背包问题等。 动态规划的核心思想是“记忆化搜索”,即将中间状态保存下来,避免重复计算。通常情况下,我们会使用一张二维表来记录状态转移过程中的中间值,例如动态规划求解斐波那契数列问题时,就可以定义一个二维数组f[i][j],代表第i项斐波那契数列中,第j个元素的值。 在LeetCode中,动态规划标签下有众多难度不同的问题。例如,经典的“爬楼梯”问题,要求我们计算到n级楼梯的方案数。这个问题的解法非常简单,只需要维护一个长度为n的数组,记录到达每一级楼梯的方案数即可。类似的问题还有“零钱兑换”、“乘积最大子数组”、“通配符匹配”等,它们都采用了类似的动态规划思想,通过拆分问题、保存中间状态来求解问题。 需要注意的是,动态规划算法并不是万能的,它虽然可以处理众多经典问题,但在某些场景下并不适用。例如,某些问题的状态转移过程比较复杂,或者状态转移方程中存在多个参数,这些情况下使用动态规划算法可能会变得比较麻烦。此外,动态规划算法也存在一些常见误区,例如错用贪心思想、未考虑边界情况等。 总之,掌握动态规划算法对于LeetCode的学习和解题都非常重要。除了刷题以外,我们还可以通过阅读经典的动态规划书籍,例如《算法竞赛进阶指南》、《算法与数据结构基础》等,来深入理解这种算法思想。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值