目录
7.1 电话号码的字母组合(中等):回溯
7.2 解数独(困难):回溯算法
7.3 组合总和(中等):回溯算法
7.4 组合总和Ⅱ(中等):回溯算法
7.5 全排列Ⅱ(中等):回溯算法
7.6 组合(中等):
7.7 子集(中等):回溯算法
7.8 回溯算法总结!!!
题目:给定一个仅包含数字 2-9
的字符串,返回所有它能表示的字母组合。答案可以按 任意顺序 返回。
给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。
思想:使用一个哈希表存储每个数字对应的所有可能字母,然后进行回溯操作;
回溯是维护一个字符串,表示已有的字母排列,注意此时应该遍历完电话号码的所有数字
每次取电话号码的一位数字,从哈希表中获取该数字对应的所有可能的字母,并将一个字母插入到已有的字母排列后面
继续处理电话号码的后一位数字,直到处理完电话号码中的所有数字
进行回退操作,遍历其他的字母排列
总结:先找到每次能够跳跃到的边界值以及在这个边界范围内能够跳到的最远位置,将能跳到的最远位置作为下一次的边界
代码:
class Solution {
public List letterCombinations(String digits) {
//创建一个存储结果的数组
List combinations = new ArrayList<>();
//若digits为空直接返回
if(digits.length() == 0){
return combinations;
}
//将电话号码和字母的对应关系存入哈希表中
Map map = 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");
}};
//使用回溯算法寻找所有的可能序列: 传入一个new StringBuffer()对象,用来存储每一次的字符可能结果
backtrack(combinations, map, digits, 0, new StringBuffer());
return combinations;
}
//使用回溯算法
public void backtrack(List combinations, Map map, String digits, int index, StringBuffer combination){
//若遍历完String的所有元素,则返回此时的排列顺序
if(index == digits.length()){
combinations.add(combination.toString());
}else{
//取出digits的元素, 并根据该元素取出存入map中的值
char c = digits.charAt(index);
String letters = map.get(c);
int len = letters.length();
for(int i = 0; i < len; i++){
combination.append(letters.charAt(i));
backtrack(combinations, map, digits, index + 1, combination);
combination.deleteCharAt(index);
}
}
}
}
题目:编写一个程序,通过填充空格来解决数独问题。
数独的解法需 遵循如下规则:
数字 1-9
在每一行只能出现一次。
数字 1-9
在每一列只能出现一次。
数字 1-9
在每一个以粗实线分隔的 3x3
宫内只能出现一次。(请参考示例图)
数独部分空格内已填入了数字,空白格用 '.'
表示。
思想:
对数组整个遍历,将数组中第[i,j]
个空白格存入一个列表中,方便后续递归
由于数独要求为:一个数组每一行、每一列、每个九宫格只能出现一次,因此我们设置三个数组,将其初始值设置为false,表示该数字在每一行、每一列、每一个九宫格未被使用过
使用line[i][x-1]、column[j][x-1]、block[i][j][x-1] = false
,表示第i
行、第j
列、第i,j
个九宫格中,x
未被出现过,可以填入空格中
每次填入一个x
之后,要将这三个值都变为true
,表示已经使用过
如果这个位置无法填入x
,则回溯至上一个空白格位置进行递归,并将原本已经变为true
的重置为False
总结:要确定下需要填写的位置、什么情况下能够填写进该位置、若填写下该位置则从这一状态开始能够进行到最终状态才算是一个解,否则就回溯到上一个状态
代码:
class Solution {
//记录三个状态信息: 表示这一行的x是否用过
private boolean[][] line = new boolean[9][9];
private boolean[][] column = new boolean[9][9];
private boolean[][][] block = new boolean[3][3][9];
//声明一个列表存储输入中的空白格: 即我们需要填入的部分
private List spaces = new ArrayList<>();
//记录一个状态变量:存储最终是否有解的状态
boolean flag = false;
public void solveSudoku(char[][] board) {
for(int i = 0; i < 9; i++){
for(int j = 0; j < 9; j++){
//找到空白格,加入例表中
if(board[i][j] == '.'){
spaces.add(new int[]{i,j});
}else{
//此时的x应该为这个位置的值;它的值在数组中存储为字符型,我们需要将其转为整数型,因此将其减去'0',变为ASCII码相减即可得到最终值, 在减一即可
int x = board[i][j] - '0' - 1;
line[i][x] = column[j][x] = block[i / 3][j / 3][x] = true;
}
}
}
backtrack(board, 0);
}
public void backtrack(char[][] board, int index){
//当传入的index等于spaces长度,说明所有的空白已经填完
if(index == spaces.size()){
//此时说明有数独的解,将标志位设置为true
flag = true;
return;
}
//先把空白格拿出来, 空白格中存储了位置信息(i,j)
int[] space = spaces.get(index);
int i = space[0], j = space[1];
//判断这两个位置的所有值是否有符合条件的,有就填入;
//注意:若是flag已经为true,则不需要再进行循环;这已经表示空白格已被填写完毕,若不加该条件,每次回溯到上一变量还会修改board中的值,造成错误
for(int k = 0; k < 9 && !flag; k++){
if(!line[i][k] && !column[j][k] && !block[i / 3][j / 3][k]){
//将所有值改为true,表示用过了
line[i][k] = column[j][k] = block[i / 3][j / 3][k] = true;
//将该位置的值赋为当前值
board[i][j] = (char)(k + '0' + 1);
//从此状态继续查找下一个位置是否有符合条件的解
backtrack(board, index + 1);
//若无符合条件的解则将其回溯至上一个状态
line[i][k] = column[j][k] = block[i / 3][j / 3][k] = false;
}
}
}
}
题目:给你一个 无重复元素 的整数数组 candidates
和一个目标整数 target
,找出 candidates
中可以使数字和为目标数 target
的 所有 不同组合 ,并以列表形式返回。你可以按 任意顺序 返回这些组合。
candidates
中的 同一个 数字可以 无限制重复被选取 。如果至少一个数字的被选数量不同,则两种组合是不同的。
对于给定的输入,保证和为 target
的不同组合数少于 150
个。
思想:寻找所有可行解,则使用搜索回溯进行
定义一个递归函数dfs(target, combine, idx)
,表示当前在candidates
数组的第idx
位,还剩target
要组合,已经组合的列表为combine
递归终止条件:target < 0
,或者candidiates
数组已经用完
由于题设中candidates
中的 同一个 数字可以 无限制重复被选取 ,因此在函数中有两种方式:
可以选择跳过不用第idx
个数:就是从最后一个元素开始组成target
,这样可以避免重复问题
可以选择使用第idx
个数
总结:先找到每次能够跳跃到的边界值以及在这个边界范围内能够跳到的最远位置,将能跳到的最远位置作为下一次的边界
代码:
class Solution {
public List> combinationSum(int[] candidates, int target) {
//设置二维列表返回最终结果
List> res = new ArrayList<>();
//设置临时列表存储每一次可能组成target的值
List list = new ArrayList<>();
//使用dfs + 回溯算法
dfs(candidates, target, list, res, 0);
return res;
}
public void dfs(int[] candidates, int target, List list, List> res, int idx){
//终止条件1: candidates中的元素被用完了
if(idx == candidates.length){
return;
}
//终止条件2:target已经为0,说明找到了一组可行解
if(target == 0){
res.add(new ArrayList<>(list));
return;
}
//第一种选择方式:跳过第idx个数,从第idx + 1个数开始
dfs(candidates, target, list, res, idx + 1);
//第二种选择方式:重复使用第一个元素
if(target - candidates[idx] >= 0){
//将当前值存入list中,作为一个可能解
list.add(candidates[idx]);
target = target - candidates[idx];
dfs(candidates, target, list, res, idx);
//如果不是可行解,回溯到上一个状态
list.remove(list.size() - 1);
}
}
}
题目:给定一个候选人编号的集合 candidates
和一个目标数 target
,找出 candidates
中所有可以使数字和为 target
的组合。
candidates
中的每个数字在每个组合中只能使用 一次 。
注意:解集不能包含重复的组合。
思想:该题和上一题的区别在于每个数字在每个组合中只能使用一次,因此需要处理重复的集合
对集合先排序,方便处理重复性
当target
小于candidates[i]
时,candidates[i + 1]
以后的值也一定大于target
,因此要进行一次剪枝
当当前元素和当前元素的前一个元素相等时,说明当前元素出发一定是重复集合,因此进行剪枝
总结:要理解递归的本质,才能找到合适的剪枝方法,达到不重复的要求
代码:
class Solution {
public List> combinationSum2(int[] candidates, int target) {
//创建一个二维集合存储结果
List> res = new ArrayList<>();
if(candidates.length == 0){
return res;
}
//对数组排序,以便解决重复问题
Arrays.sort(candidates);
//创建一个存储每一次可行解的列表
Deque deque = new ArrayDeque<>(candidates.length);
//进行dfs搜索
dfs(candidates, target, res, deque, 0);
return res;
}
public void dfs(int[] candidates, int target, List> res, Deque deque, int index){
//终止条件:如果target为0,说明搜索完毕
if(target == 0){
res.add(new ArrayList<>(deque));
return;
}
for(int i = index; i < candidates.length; i++){
//减去重复集合的两个操作
//如果target值小于数组中值,则直接退出循环(排好序后的值,如果当前值大于target,则以后都会大于target)
if(candidates[i] > target){
break;
}
//如果当前值和下一个值相等,则结束这一次循环
if(i > index && candidates[i] == candidates[i - 1]){
continue;
}
//如果都没问题,则将其作为第一个可能可行解的值
deque.addLast(candidates[i]);
dfs(candidates, target - candidates[i], res, deque, i + 1);
//如果有问题,将其回溯
deque.removeLast();
}
}
}
题目:给定一个可包含重复数字的序列 nums
,按任意顺序 返回所有不重复的全排列。
思想:可以看作有n
个排成一行的空格,从数组nums
中取一个数向空格填入,每个数只能用一次
定义一个标记数组vis
标记已填过的数
为了不重复,需要对数组排序,并且当当前值和上一个值相同时,结束当次执行的回溯操作
总结:重点是要懂得如何解决重复问题:排序 + 标记 + 去重
代码:
class Solution {
public List> permuteUnique(int[] nums) {
//创建列表存储每一次的结果和最终结果
List> res = new ArrayList<>();
List list = new ArrayList<>();
//若nums为空直接返回
if(nums == null || nums.length == 0){
return res;
}
//对数组排序,满足不重复要求
Arrays.sort(nums);
//一个标记位,表示该位置是否存入了排列的数字
boolean[] flag = new boolean[nums.length];
//进行回溯
backtrack(nums, 0, res, list, flag);
return res;
}
public void backtrack(int[] nums, int index, List> res, List list, boolean[] flag){
//终止条件,当选到最后一个元素就终止
if(index == nums.length){
res.add(new ArrayList<>(list));
}
for(int i = 0; i < nums.length; i++){
//当前元素和上一个元素相等时结束循环,避免重复
//如果当前值已经被用过即flag[i]为true,说明重复了需要结束当次循环
if((i > 0 && nums[i] == nums[i - 1] && !flag[i - 1]) || flag[i]){
continue;
}
list.add(nums[i]);
flag[i] = true;
backtrack(nums, index + 1, res, list, flag);
//失败则回溯
flag[i] = false;
//删除上一次的状态值
list.remove(index);
}
}
}
题目:给定两个整数 n
和 k
,返回范围 [1, n]
中所有可能的 k
个数的组合。
你可以按 任何顺序 返回答案。
思想:
从1
开始进行搜索,加入到集合中,然后从当前值开始继续向下搜索,若出现问题则将其回溯至上一个状态
注意:将存入的index
作为循环的起点,从而解决值的重复问题
总结:回溯算法其实是一个套路算法:大概形式为:
public void backtrack(int n, int k, List> res, List list, int index){
//当搜索完毕时的终止条件
if(list.size() == k){
res.add(new ArrayList<>(list));
return;
}
//进行回溯
for(int i = index;i <= n; i++){
//加入元素
list.add(i);
//当前状态出发的下一状态搜索
backtrack(n, k, res, list, i + 1);
//回溯
list.remove(list.size() - 1);
}
}
代码:
class Solution {
public List> combine(int n, int k) {
//创建数组存取结果值
List> res = new ArrayList<>();
List list = new ArrayList<>();
backtrack(n, k, res, list, 1);
return res;
}
public void backtrack(int n, int k, List> res, List list, int index){
//当搜索完毕时,添加结果
if(list.size() == k){
res.add(new ArrayList<>(list));
return;
}
//进行回溯
for(int i = index;i <= n; i++){
list.add(i);
backtrack(n, k, res, list, i + 1);
list.remove(list.size() - 1);
}
}
}
题目:给你一个整数数组 nums
,数组中的元素 互不相同 。返回该数组所有可能的子集(幂集)。
解集 不能 包含重复的子集。你可以按 任意顺序 返回解集。
思想:
从1
开始进行搜索,加入到集合中,然后从当前值开始继续向下搜索,若出现问题则将其回溯至上一个状态
注意:将存入的index
作为循环的起点,从而解决值的重复问题
总结:先找到每次能够跳跃到的边界值以及在这个边界范围内能够跳到的最远位置,将能跳到的最远位置作为下一次的边界
代码:
class Solution {
public List> subsets(int[] nums) {
//创建数组存取结果值
List> res = new ArrayList<>();
List path = new ArrayList<>();
if(nums.length == 0 || nums == null){
return res;
}
backtrack(nums, res, path, 0);
return res;
}
public void backtrack(int[] nums, List> res, List path, int index){
//空值也属于子集
res.add(new ArrayList<>(path));
//回溯套路操作
for(int i = index; i < nums.length; i++){
path.add(nums[i]);
backtrack(nums, res, path, i + 1);
path.remove(path.size() - 1);
}
}
}
回溯算法能够寻找到所有的可行解,若其中一个解状态不可行,则将其回溯至上一状态;
适用的场景为:寻找所有可行解的情况
因此回溯算法一般流程大概是:
进行题目要求的操作
回溯到上一次的状态;代码一般为
public void backtrack(int n, int k, List> res, List list, int index){
//当搜索完毕时的终止条件
if(list.size() == k){
res.add(new ArrayList<>(list));
return;
}
//进行回溯
for(int i = index;i <= n; i++){
//加入元素
list.add(i);
//当前状态出发的下一状态搜索
backtrack(n, k, res, list, i + 1);
//回溯
list.remove(list.size() - 1);
}
}
解决回溯算法中的重复问题一般方式:排序 + 标记 + 去重