回溯算法简介
回溯法采用试错 的思想,它尝试分步的去解决一个问题。在分步解决问题的过程中,当它通过尝试发现现有的分步答案不能得到有效的正确的解答的时候,它将取消上一步甚至是上几步的计算,再通过其它的可能的分步解答再次尝试寻找问题的答案。回溯法通常用最简单的递归方法来实现,在反复重复上述的步骤后可能出现两种情况:
找到一个可能存在的正确的答案;
在尝试了所有可能的分步方法后宣告该问题没有答案。
回溯算法相关的典型题目:https://leetcode-cn.com/problems/permutations/solution/hui-su-suan-fa-python-dai-ma-java-dai-ma-by-liweiw/
题型一:排列、组合、子集相关问题
提示:这部分练习可以帮助我们熟悉「回溯算法」的一些概念和通用的解题思路。解题的步骤是:先画图,再编码。去思考可以剪枝的条件, 为什么有的时候用 used 数组,有的时候设置搜索起点 begin 变量,理解状态变量设计的想法。
题型二:Flood Fill
题型三:字符串中的回溯问题
题型四:游戏问题
排列系列问题
打印一个字符串的全部子序列
打印一个字符串的全部子序列,包括空字符串。
思路:
递归过程中在修改 char[] 数组当前位置的值为空 ''
,代表不考虑当前字符的情况,修改后进入子问题,得到不考虑当前位置字符时的结果;然后该子问题的方法栈调用完毕后,再将原始值赋值回去,再调用一次子问题,代表考虑当前字符的情况,又可以得到一种结果
递归到底时,再保存当前 char[] 中的内容到结果列表里。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 public static List<String> printAllSubsquences (String str) { if (str == null ) { return null ; } char [] cstr = str.toCharArray(); List<String> res = new ArrayList<>(); process(0 , cstr, res); return res; } public static void process (int i, char [] cstr, List<String> res) { if (i == cstr.length) { res.add(new String(cstr)); return ; } char tmp = cstr[i]; cstr[i] = 0 ; process(i + 1 , cstr, res); cstr[i] = tmp; process(i + 1 , cstr, res); } public static void main (String[] args) { String test = "abc" ; System.out.println(printAllSubsquences(test)); }
打印一个字符串的全部排列
相似题目:【全排列】打印一个数组的全部排列
打印一个字符串的全部排列,要求不要出现重复的排列 。
思路:本题使用交换字符 的技巧减少占用的空间,并设置 visited[]
数组避免重复的排列 (可能某些字符会重复出现在数组中)
代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 public static List<String> printAllPermutations (String str) { if (str == null ) { return null ; } char [] cstr = str.toCharArray(); List<String> res = new ArrayList<>(); process(0 , cstr, res); return res; } public static void process (int i, char [] cstr, List<String> res) { if (i == cstr.length) { res.add(new String(cstr)); return ; } boolean [] visited = new boolean [26 ]; for (int j = i; j < cstr.length; j++) { if (visited[cstr[j] - 'a' ] == true ) { continue ; } visited[cstr[j] - 'a' ] = true ; swap(cstr, i, j); process(i + 1 , cstr, res); swap(cstr, i, j); } } private static void swap (char [] cstr, int i, int j) { char tmp = cstr[i]; cstr[i] = cstr[j]; cstr[j] = tmp; } public static void main (String[] args) { String test = "abc" ; System.out.println(printAllPermutations(test)); }
46. 全排列
46. 全排列 :给定一个不含重复数字的数组 nums
,返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。
本问题和上一个问题非常相似。区别在于数组不重复,无需设置 visited 数组判断每个字符是否已经被交换过
代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 public List<List<Integer>> permute(int [] nums) { List<List<Integer>> res = new ArrayList<>(); permuteHelper(0 , nums, res); return res; } public void permuteHelper (int i, int [] nums, List<List<Integer>> res) { if (i == nums.length) { List<Integer> list = new ArrayList<>(); for (int k = 0 ; k < nums.length; k++) { list.add(nums[k]); } res.add(list); return ; } for (int j = i; j < nums.length; j++) { swap(nums, i, j); permuteHelper(i + 1 , nums, res); swap(nums, i, j); } } public void swap (int [] nums, int i, int j) { int tmp = nums[i]; nums[i] = nums[j]; nums[j] = tmp; }
47. 全排列 II
47. 全排列 II :给定一个可包含重复数字的序列 nums
,按任意顺序 返回所有不重复的全排列。
该问题和前面的打印一个字符串的全部排列非常相似,都需要使用 visited[] 数组判断是否重复
代码:
78. 子集
78. 子集 :给你一个整数数组 nums
,数组中的元素 互不相同 。返回该数组所有可能的子集(幂集)。解集 不能 包含重复的子集。你可以按 任意顺序 返回解集。
思路:当前位置的数字可以选择添加到子集,也可以选择不添加到子集。遍历所有情况即可得到所有子集。
代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 List<List<Integer>> res; public List<List<Integer>> subsets(int [] nums) { res = new ArrayList<>(); Set<Integer> set = new HashSet<>(); recur(nums, 0 , set); return res; } public void recur (int [] nums, int i, Set<Integer> set) { if (i == nums.length) { res.add(new ArrayList<>(set)); return ; } recur(nums, i + 1 , set); set.add(nums[i]); recur(nums, i + 1 , set); set.remove(nums[i]); }
90. 子集 II
90. 子集 II :给你一个整数数组 nums
,其中可能包含重复元素,请你返回该数组所有可能的子集(幂集)。解集 不能 包含重复的子集。返回的解集中,子集可以按 任意顺序 排列。
与上一题的区别在于有重复元素。
策略 :先排序 。如果当前值等于前一个值。那么判断前一个值是否被选中
如果被选中,则当前值可以被选中,也可以不被选中
如果没被选中,则当前值不能被选中(因为这种情况会和第一种中的某一个情况重复,例如前选中,后没选中)
代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 public List<List<Integer>> ans;public List<List<Integer>> subsetsWithDup(int [] nums) { if (nums == null || nums.length == 0 ) { return new ArrayList<>(); } Arrays.sort(nums); ans = new ArrayList<>(); List<Integer> selected = new ArrayList<>(); recur(nums, 0 , false , selected); return ans; } public void recur (int [] nums, int i, boolean preIsSelected, List<Integer> selected) { if (i == nums.length) { ans.add(new ArrayList<>(selected)); return ; } if (i > 0 && nums[i - 1 ] == nums[i] ) { if (preIsSelected) { selected.add(nums[i]); recur(nums, i + 1 , true , selected); selected.remove(selected.size() - 1 ); recur(nums, i + 1 , false , selected); } else { recur(nums, i + 1 , false , selected); } } else { selected.add(nums[i]); recur(nums, i + 1 , true , selected); selected.remove(selected.size() - 1 ); recur(nums, i + 1 , false , selected); } }
39. 组合总数
39. 组合总和 :给你一个 无重复元素 的整数数组 candidates
和一个目标整数 target
,找出 candidates
中可以使数字和为目标数 target
的 所有 不同组合 ,并以列表形式返回。你可以按 任意顺序 返回这些组合。
candidates
中的 同一个 数字可以 无限制重复被选取 。如果至少一个数字的被选数量不同,则两种组合是不同的。 对于给定的输入,保证和为 target
的不同组合数少于 150
个。
代码:
回溯算法常见题目
汉诺塔问题
在经典汉诺塔问题中,有 3 根柱子及 N 个不同大小的穿孔圆盘,盘子可以滑入任意一根柱子。一开始,所有盘子自上而下按升序依次套在第一根柱子上(即每一个盘子只能放在更大的盘子上面)。移动圆盘时受到以下限制:
每次只能移动一个盘子
盘子只能从柱子顶端滑出移到下一根柱子
盘子只能叠在比它大的盘子上
请编写程序,用栈将所有盘子从第一根柱子移到最后一根柱子。你需要原地修改栈。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public void hanota (List<Integer> A, List<Integer> B, List<Integer> C) { hanotaHelper(A.size(), A, C, B); } public void hanotaHelper (int i, List<Integer> start, List<Integer> end, List<Integer> other) { if (i == 1 ) { end.add(0 , start.remove(0 )); return ; } hanotaHelper(i-1 , start, other, end); end.add(0 , start.remove(0 )); hanotaHelper(i-1 , other, end, start); }
栈的逆序
给你一个栈,请你逆序这个栈,不能申请额外的数据结构,只能使用递归函数。如何实现?
思路:
定义方法 reverse()
,将递归地调用该方法将整个栈中的元素进行反转,步骤:
先调用自定义的方法 getAndRemoveLastElement()
抽取出栈底元素
接着递归调用 reverse()
方法栈中剩余元素进行反转
反转完毕后,再将刚才抽取出的栈底元素放到栈顶,即完成了整体的逆序
该过程利用了系统栈将每一层方法栈中抽取的栈底元素保存起来,将剩余元素逆序后再回到当前方法栈,取回刚才保存的栈底元素放到栈顶。
代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 public static void reverse (Stack<Integer> stack) { if (stack.isEmpty()) { return ; } Integer tmp = getAndRemoveLastElement(stack); reverse(stack); stack.push(tmp); } public static Integer getAndRemoveLastElement (Stack<Integer> stack) { Integer cur = stack.pop(); if (stack.isEmpty()) { return cur; } else { Integer last = getAndRemoveLastElement(stack); stack.push(cur); return last; } } public static void main (String[] args) { Stack<Integer> test = new Stack<Integer>(); test.push(1 ); test.push(2 ); test.push(3 ); test.push(4 ); test.push(5 ); reverse(test); while (!test.isEmpty()) { System.out.println(test.pop()); } }
N 皇后
n 皇后问题 研究的是如何将 n 个皇后放置在 n×n 的棋盘上,并且使皇后彼此之间不能相互攻击。
给你一个整数 n ,返回所有不同的 n 皇后问题 的解决方案。每一种解法包含一个不同的 n 皇后问题 的棋子放置方案,该方案中 ‘Q’ 和 ‘.’ 分别代表了皇后和空位。
代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 class Solution { public List<List<String>> solveNQueens(int n) { int [] record = new int [n]; process(0 , record, n); return res; } List<List<String>> res = new ArrayList<>(); public void process (int i, int [] record, int n) { if (i == n) { res.add(generateBoard(n, record)); return ; } for (int j = 0 ; j < n; j++) { if (isValid(record, i, j)) { record[i] = j; process(i + 1 , record, n); } } } public boolean isValid (int [] record, int i, int j) { for (int k = 0 ; k < i; k++) { if (record[k] == j || Math.abs(record[k] - j) == Math.abs(i - k)) { return false ; } } return true ; } public List<String> generateBoard (int n, int [] record) { List<String> list = new ArrayList<>(); for (int row = 0 ; row < n; row++) { char [] board = new char [n]; Arrays.fill(board, '.' ); board[record[row]] = 'Q' ; list.add(new String(board)); } return list; } }