回溯算法是一种比较有用的算法工具.能够帮助我们解决三种问题:组合
,子集
,棋盘
.
也许业务中有很多种不同的问题,基本都可以归类为这三种问题.
回溯问题可以理解为是一个回溯树,如果有一些问题没有解题思路,可以画出回溯树来帮助我们.
当在每一个节点的时候,都有两个选项:选择区间(已选择元素)+待选择区间.对于待选择区间中的每个元素都有两个选择,选入或者不选入.然后进入下一个步骤的选择情况,这是一个递归.
结束条件:在回溯的开始的地方都会有结束条件.当遇到结束条件的时候就结束.
去重:对于结果有两种去重,第一种是:是将所有的结果集都枚举出来,然后使用set进行去重.第二种是:使用排序,然后查看重复回溯.
剪枝:未来加快执行速度,将其中一些不可能执行的路径提前返回,加快执行熟读.
回溯法的标准写法:
backtrack(结束条件,选择区间,临时结果,最终结果){ if(当前流程是否结束(结束条件,临时结果)){ 将临时结果添加到最终结果,并结束流程. } for(ele in 选择区间){ 1.标记ele元素已经被选入临时结果 2.临时结果 + ele backtrack(结束条件,选择区间-ele,临时结果 + ele,结果); 4.临时结果 - ele 6.标记 ele元素没有被选入临时结果 } }
排列问题在思考的时候,因为需要选择n个元素.从[0,n-1]区间中位置上选择没有被选过的元素添加到临时结果的当前位置上.
结束条件是 当临时结果长度等于 n的时候表示结束.
适用 hits[i]==1 表示第i个元素被选中了.
去重:先对所有元素进行排序,然后判断当前元素和前一个元素相等,且前一个元素没有被使用.这个时候表示当前的分支的树在前面一个元素的各种选项中已经出现过了.现在是回溯回来处理当前选项.
public List<List<Integer>> permute(int[] nums) { /** * 这个是全排列 */ List<List<Integer>> result = new ArrayList<>(); Stack<Integer> stack = new Stack<>(); int[] hints = new int[nums.length]; backtrack(nums, hints, stack, result); return result; } /** * 其中 nums + hints表示待选项 * stack表示临时结果 * hits[i]=0 表示未选项 * 结束条件是 stack.length() == nums.length的时候表示结束. */ private void backtrack(int[] nums, int[] hints, Stack<Integer> stack, List<List<Integer>> result) { if (stack.size() == nums.length) { result.add(new ArrayList<>(stack)); return; } /** * 这里是n叉树.表示当前可以选择的元素个数. * 和已经选过的元素 */ for (int i = 0; i < nums.length; i++) { if (hints[i] == 1) { continue; } //表示选择当前的元素为 stack的顶部位置 hints[i] = 1; stack.push(nums[i]); backtrack(nums, hints, stack, result); //这里表示nums[i]不在stack的当前堆顶元素.下一次会继续进行处理的. hints[i] = 0; stack.pop(); } }
典型全排列中去重的实现
public List<List<Integer>> permuteUnique(int[] nums) { /** * 给定一个元素,同时处理全排列.同时去从 */ List<List<Integer>> result = new ArrayList<>(); Stack<Integer> tempResult = new Stack<Integer>(); Arrays.sort(nums); int[] hists = new int[nums.length]; backtrack(nums, hists, tempResult, result); return result; } /** * 全排列的去重 * https://leetcode.cn/problems/7p8L0Z/ * @param nums * @param hists * @param tempResult * @param result */ private void backtrack(int[] nums, int[] hists, Stack<Integer> tempResult, List<List<Integer>> result) { if (tempResult.size() == nums.length) { result.add(new ArrayList<>(tempResult)); return; } for (int i = 0; i < nums.length; i++) { //如果两个元素相等,就跳过. if (hists[i] == 1) { continue; } //这里的去从是否有道理.这里是去从的实现. if (i > 0 && nums[i] == nums[i - 1] && hists[i - 1] == 1) { continue; } tempResult.push(nums[i]); hists[i] = 1; backtrack(nums, hists, tempResult, result); hists[i] = 0; tempResult.pop(); } }
子集是子从所有的元素中选择 [0,n-1]个元素到零时结合中来.
其中需要去从.去重的方法是: 先排序,然后判断 待选结合中 第二个元素开始是否和第一个元素相等,如果相等就是重复的.因为前一个元素和当前元素相等:这个时候前一个元素的待选区间比当前元素的待选区间打,后一个元素是前一个元素的子集,是重复的.需要被剪枝.
public List<List<Integer>> subsetsWithDup(int[] nums) { List<List<Integer>> result = new ArrayList<>(); Stack<Integer> path = new Stack<Integer>(); Arrays.sort(nums); //这里是从第0层开始 backtrack(nums, 0, path, result); return result; } /** * * * @param nums * @param startRank [startRank,nums.length)表示待选区间. * @param path 零时结果 * @param result 最终结果 */ private void backtrack(int[] nums, int startRank, Stack<Integer> path, List<List<Integer>> result) { result.add(new ArrayList<>(path)); /** * nums[startRank,nums.length)表示 可选的数值范围. */ for (int i = startRank; i < nums.length; i++) { //当前的path相同,同一个层次的上一个元素选中了,那么如果当前无论选中不选中, //其中剩余的可选项都是上一个的子集.所以这个是重复的. if (i > startRank && nums[i] == nums[i - 1]) { continue; } //当前元素添加进去了. path.push(nums[i]); backtrack(nums, i + 1, path, result); //表示当前元素被添加到path中去. path.pop(); } }
在走到某一步的时候,判断当前这个步骤是否满足某个条件,如果不满足就终止当前流程.如果满足就从当前可选项中选择一个继续下去.
/** *给定一个 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 示例 2: *输入:board = [["A","B","C","E"],["S","F","C","S"],["A","D","E","E"]], word = "SEE" *输出:true *示例 3: * * * 输入:board = [["A","B","C","E"],["S","F","C","S"],["A","D","E","E"]], word = "ABCB" * 输出:false * * * 提示: * * m == board.length * n = board[i].length * 1 <= m, n <= 6 * 1 <= word.length <= 15 * board 和 word 仅由大小写英文字母组成 */ public boolean exist(char[][] board, String word) { if (board == null) { return false; } int row = board.length; int column = board[0].length; int[][] hits = new int[row][column]; for (int i = 0; i < row; i++) { for (int j = 0; j < column; j++) { if (backtrack(board, i, j, hits, word, 0)) { return true; } } } return false; } /** * 从棋盘上的任意一个坐标开始,和word.charAt(rank)进行比较.如果不相等就结束,如果相等就继续: * 选项有四个:上下左右.如果左边超过棋盘结束,如果来过了,也返回为负.然后向四个方向探索,如果有一个能探索通过就返回结果. * 其中 hits[rowRank][columnRank]表示是否已经访问过了. * @param board * @param rowRank * @param columnRank * @param hits * @param word * @param rank * @return */ private boolean backtrack(char[][] board, int rowRank, int columnRank, int[][] hits, String word, int rank) { if (rank >= word.length()) { return true; } if (rowRank >= board.length || columnRank >= board[0].length || rowRank < 0 || columnRank < 0 || hits[rowRank][columnRank] == 1) { return false; } if (board[rowRank][columnRank] != word.charAt(rank)) { return false; } else { hits[rowRank][columnRank] = 1; boolean result = backtrack(board, rowRank - 1, columnRank, hits, word, rank + 1) || backtrack(board, rowRank + 1, columnRank, hits, word, rank + 1) || backtrack(board, rowRank, columnRank + 1, hits, word, rank + 1) || backtrack(board, rowRank, columnRank - 1, hits, word, rank + 1); hits[rowRank][columnRank] = 0; return result; } }