1、回溯法 = 回溯搜索法 = 穷举(本质)+剪枝 (不高效)
2、回溯函数 = 递归函数 (回溯是递归的副产品,只要有递归就会有回溯)递归必须有终止条件
3、回溯法解决的问题都可以抽象为N叉树形结构(二叉树中经常用到递归),集合的大小就构成了树的宽度,递归的深度,都构成的树的深度。
4、回溯算法模板框架如下:for循环横向遍历,递归纵向遍历,回溯不断调整结果集。
- 确定回溯函数的返回值和参数 (在处理过程中确定)
- 函数结束条件+操作(返回或保存结果等)
- 函数每层遍历的逻辑:遍历该层的每一个元素 + 处理 + 撤销处理结果(回溯)
void backtracking(参数) {
if (终止条件) {
存放结果;
return;
}
for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
处理节点;
backtracking(路径,选择列表); // 递归
回溯,撤销处理结果
}
}
5、回溯法,一般可以解决如下几种问题:
- 组合问题:N个数里面按一定规则找出k个数的集合:
- 回溯法就是解决这种k层for循环嵌套的问题,起始点可选字符处,去重+剪枝
- 1、 77. 组合, 216. 1-9组合总和(不可重复) III,17. 电话号码的字母组合,39. 组合总和(可重复)40. 组合总和 II(不可重复)解集不能包含重复的组合40. 组合总和 II(不可重复)
- 切割问题:一个字符串按一定规则有几种切割方式
- 子集问题:一个N个数的集合里有多少符合条件的子集
- 子集问题是要收集所有节点的结果,而组合问题和排列问题是收集叶子节点的结果。
- 4、 找出所有满足条件的子集(幂集):遍历这个树的时候,把所有满足条件的节点都记录下来,不满足的直接排除,就是要求的子集集合。
- 排列问题:N个数按一定规则全排列,有几种排列方式
- 棋盘问题:N皇后,解数独等
1、, 216. 1-9组合总和(不可重复) III,17. 电话号码的字母组合,39. 组合总和(可重复)40. 组合总和 II(不可重复)解集不能包含重复的组合40. 组合总和 II(不可重复)
- 结束条件:组合完成(列表内有k各元素),保存组合结果,结束、返回
- 正常情况:依次 从当前可选序列中,添加一个数据,并在当前状态下继续处理数据,处理完后恢复原状(当前层的起始状态)。
- 解集不能包含重复的组合:N叉树每层for遍历的时候,判断当前数值有没有在该层使用过
- if(i>index && candidates[i] == candidates[i-1]) continue; //index为当前层的起始位置
- 前提:candidates数组是有序的(即重复的数值在一起)
- //同一层中数值相同的元素,不可重复使用,避免组合的重复
- 组合中同一数值是否可以重复被选取:取决于下一次可选元素的起始位置
- 可重复:comSum(candidates,target-candidates[i],i);//下次查找范围包含当前值
- 不可重复:comSum(candidates,target-candidates[i],i+1);//下次查找范围不包含当前值
- 剪枝操作:数组的选取范围,
- 当前剩余的元素个数是否满足需要的组合候选元素数
- 当前的组合值是否已经不满足条件,后续的其他组合不用遍历。前提:有序数组中按序选取。
- 解集不能包含重复的组合:N叉树每层for遍历的时候,判断当前数值有没有在该层使用过
//找出k 个数的组合
class Solution {
List<List<Integer>> res = new LinkedList<>();
List<Integer> path = new LinkedList<>();
public List<List<Integer>> combine(int n, int k) {
combineTrack(n,k,1);
return res;
}
public void combineTrack(int n, int k, int start){
if(path.size() == k){//组合完成
res.add(new LinkedList<>(path));
return;
}
//start 还没包含在组合中,属于查找范围,(start-1) + k-path.size()<= n
for(int i = start; i <= n-k+path.size()+1; i++){
path.add(i);
combineTrack(n,k,i+1);
path.remove(path.size()-1);//回溯
}
}
}
//找出所有相加之和为 n 的 k 个数的组合
class Solution {
List<List<Integer>> res = new LinkedList<>();
List<Integer> path = new LinkedList<>();
public List<List<Integer>> combinationSum3(int k, int n) {
combine(k,n,1);
return res;
}
public void combine(int k, int target, int start){
if(target < 0 ) return;
if(path.size() == k) {
if(target == 0){
res.add(new LinkedList<>(path));
}
return;
}
//利用for循环遍历 (执行时间更短)
// for(int i = start; i <= 9-(k-path.size()-1); i++){
// path.add(i);
// target -= i;
// combine(k,target,i+1);
// target += i;
// path.remove(path.size()-1);
// }
//递归遍历
// 剪枝
if(start-1+k-path.size() > 9) return;
//使用当前节点
path.add(start);
target -= start;
combine(k,target,start+1);
target += start;
path.remove(path.size()-1);
// 不使用当前节点
combine(k,target,start+1);
}
}
//17. 电话号码的字母组合:仅包含数字 2-9 的字符串,返回所有它能表示的字母组合
class Solution {
List<String> res = new ArrayList<>();
//定义映射关系
String[] numMap = {"","","abc","def","ghi","jkl","mno","pqrs","tuv","wxyz"};
public List<String> letterCombinations(String digits) {
if(digits == null || digits.length() == 0) return res;
StringBuffer letter = new StringBuffer(digits.length());
buildLetter(digits,0,letter);
return res;
}
public void buildLetter(String digits, int index, StringBuffer letter){
if(index >= digits.length()){//所有数值对应的字符都已确定
res.add(letter.toString());
return;
}
int num = digits.charAt(index)-'0';
String nMap = numMap[num];
for(int i = 0; i<nMap.length(); i++){
letter.append(nMap.charAt(i));
buildLetter(digits,index+1,letter);
letter.deleteCharAt(letter.length()-1);
}
}
}
// candidates 中可以使数字和为目标数 target 的 所有 不同组合,数值可重复使用
//注意区分:组合(无顺序)、排序(有顺序)
class Solution {
List<List<Integer>> res = new LinkedList<>();
List<Integer> path = new LinkedList<>();
public List<List<Integer>> combinationSum(int[] candidates, int target) {
Arrays.sort(candidates);
comSum(candidates,target,0);
return res;
}
public void comSum(int[] candidates,int target, int index){//index表示元素的选取范围,避免组合间的排序不同内容重复
if(target == 0){
res.add(new LinkedList<>(path));
return;
}
for(int i=index; i<candidates.length && candidates[i] <= target;i++){
if(target-candidates[i] < 0) break;
path.add(candidates[i]);
comSum(candidates,target-candidates[i],i);//同一数指可以重复使用,所以范围包括i
path.remove(path.size()-1);
}
}
}
//candidates 中的每个数字在每个组合中只能使用 一次 ,且解集不能包含重复的组合。
class Solution {
List<List<Integer>> res = new LinkedList<>();
List<Integer> path = new LinkedList<>();
public List<List<Integer>> combinationSum2(int[] candidates, int target) {
Arrays.sort(candidates);为了将重复的数字都放到一起,升序排列,便于去重和剪枝
comSum(candidates,target,0);
return res;
}
public void comSum(int[] candidates,int target, int index){//index表示元素的选取范围,避免组合间的排序不同内容重复
if(target == 0){
res.add(new LinkedList<>(path));
return;
}
for(int i=index; i<candidates.length && candidates[i] <= target;i++){
if(i>index && candidates[i] == candidates[i-1]) continue; //同一层中数值相同的元素,不可重复使用,避免组合的重复
if(target-candidates[i] < 0) break;
path.add(candidates[i]);
comSum(candidates,target-candidates[i],i+1);//同一数指不可以重复使用,所以范围不包括i
path.remove(path.size()-1);
}
}
}
2、131. 分割回文串
结束条件:到达字符串结尾,存储结束
层遍历情况: 从当前起始位置,不断的取子字符串,判断该字符串是否为回文字符串,是则存入path,不是则下一字符串。在当前状态下深度遍历+回溯。
class Solution {
List<List<String>> res = new ArrayList<>();
List<String> path = new ArrayList<>();
public List<List<String>> partition(String s) {
backTracking(s,0);
return res;
}
public void backTracking(String s, int index){
if(index >= s.length()){
res.add(new ArrayList<>(path));
return;
}
for(int i = index; i<s.length(); i++){
if(isPalindrome(index,i,s)){
String str = s.substring(index,i+1);//不包含结束索引对应的字符
path.add(str);
}else{
continue;//找下一个符合条件的字符串
}
backTracking(s,i+1);//不能重复分割
path.remove(path.size()-1);
}
}
public boolean isPalindrome(int begin, int end, String str){
while(begin < end){
if(str.charAt(begin) != str.charAt(end)) return false;
begin++;
end--;
}
return true;
}
}
3、93. 复原 IP 地址,
有效 IP 地址 正好由四个整数(每个整数位于 0
到 255
之间组成,且不能含有前导 0
),整数之间用 '.'
分隔。给定一个只包含数字的字符串 s,返回可能的IP地址。
结束条件:
- 字符串中剩余元素过多或过少,不能构成正确的IP地址,遍历结束
- 字符串的字符全部遍历过了, 且 IP地址包含4位,保存结果,当前遍历结束
正常层遍历:
- 从当前起始位置选取字符,判断所选字符串对应的数值是否符合IP要求,不符合则j结束遍历,符合则IP中添加新元素,继续向下遍历,遍历后消除当前操作。
class Solution {
List<String> res = new ArrayList<>();
List<String> IP = new ArrayList<>();
StringBuilder res1;
public List<String> restoreIpAddresses(String s) {
res1 = new StringBuilder(s.length()+3);
backTracking(s,0);
return res;
}
public void backTracking(String s, int index){
if(s.length() - index < 4 - IP.size()) return;//剩余元素不足
if(s.length() - index > 3*(4 - IP.size())) return;//剩余元素过多
//保存结果
if(index == s.length() && IP.size() == 4){
for(int i = 0; i < 3; i++){
res1.append(IP.get(i)+".");
}
res1.append(IP.get(3));
res.add(new String(res1));
res1.delete(0,res1.length());
}
for(int i=index; i<index+3 && i<s.length(); i++){//IP地址的每个位置上最多为3位数
if(i > index && s.charAt(index) == '0') return;//不能含有前导 0,否则当前分割方法不对,返回,注意取出的值为字符
String str = s.substring(index,i+1);
int num = Integer.parseInt(str);
if(num >= 0 && num <= 255){//每个整数位于 0 到 255 之间组成
IP.add(str);
backTracking(s,i+1);
IP.remove(IP.size()-1);
}else{
continue;
}
}
}
}
4、 找出所有满足条件的子集(幂集):遍历这个树的时候,把所有满足条件的节点都记录下来,不满足的直接排除,就是要求的子集集合。
78. 子集:数组中的元素 互不相同 。返回该数组所有可能的子集(幂集)。
90. 子集 II:数组可能包含重复元素,请你返回该数组所有可能的子集(幂集)。子集不重复
491. 递增子序列:所有该数组中不同的递增子序列(可含相同数值), 子集至少有两个元素且不重复
//数组中的元素 互不相同 。回该数组所有可能的子集(幂集)。
class Solution {
List<List<Integer>> res = new ArrayList<>();
List<Integer> path = new ArrayList<>();
public List<List<Integer>> subsets(int[] nums) {
buildSubset(nums,0);
return res;
}
public void buildSubset(int[] nums, int index){
res.add(new ArrayList<>(path));//树中的每一节点都保存,不需要条件判断
if(index == nums.length) return;
for(int i = index; i < nums.length; i++){
path.add(nums[i]);
buildSubset(nums, i+1);
path.remove(path.size()-1);
}
}
}
//解集 不能 包含重复的子集(即原集合中有重复元素)
class Solution {
List<List<Integer>> res = new ArrayList<>();
List<Integer> path = new ArrayList<>();
public List<List<Integer>> subsetsWithDup(int[] nums) {
Arrays.sort(nums);//层遍历时去重需要排序
findSubset(nums,0);
return res;
}
public void findSubset(int[] nums, int index){
res.add(new ArrayList<>(path));//每一节点都保存结果。
if(index >= nums.length) return;
for(int i = index; i < nums.length; i++){
if(i > index && nums[i] == nums[i-1]) continue;//同一层中不要有重复元素
path.add(nums[i]);
findSubset(nums,i+1);
path.remove(path.size()-1);
}
}
}
//所有该数组中不同的递增子序列,递增子序列中至少有两个元素
class Solution {
List<List<Integer>> res = new ArrayList<>();
List<Integer> path = new ArrayList<>();
public List<List<Integer>> findSubsequences(int[] nums) {
backTracking(nums,0);
return res;
}
public void backTracking(int[] nums, int index){
if(path.size() >= 2){
res.add(new ArrayList<>(path));
}
for(int i = index; i<nums.length; i++){
if(!path.isEmpty() && path.get(path.size()-1) > nums[i])
continue;//不满足递增条件
if(!find(nums, index, i))
continue;//不满足子集不重复,层重复
path.add(nums[i]);
backTracking(nums,i+1);
path.remove(path.size()-1);
}
}
public boolean find(int[] nums, int index, int i){
for(int j = index; j<i; j++){
if(nums[i] == nums[j]) return false;
}
return true;
}
}
5、排列:有顺序区分,除路径中的已有元素其他元素局可选+去层重复,利用标记数组去重
46. 全排列:不含重复数字的数组,返回其所有可能的全排列。可以通过路径是否已经包含该元素,进行判断当前元素是否可以使用。不存在重复子集
47. 全排列 II:可包含重复数字的序列,返回所有不重复的全排列。
- 层内不重复:数组排序,层遍历中,nums[i-1] == nums[i],且nums[i-1]已经遍历过了,去除
- 路径中不同元素可重复:nums[i]使用过了,去除。使用used数组进行标记
//不含重复数字的数组nums ,返回其 所有可能的全排列
class Solution {
List<List<Integer>> res = new ArrayList<>();
List<Integer> path = new ArrayList<>();
public List<List<Integer>> permute(int[] nums) {
backTracking(nums);
return res;
}
public void backTracking(int[] nums){//除路径中已有元素,其他元素均可选,不需要设置选取范围
if(path.size() == nums.length){
res.add(new ArrayList<>(path));
return;
}
for(int i = 0; i<nums.length; i++){
if(path.contains(nums[i])) continue;
path.add(nums[i]);
backTracking(nums);
path.remove(path.size()-1);
}
}
}
// 可包含重复数字的序列 nums, 返回所有不重复的全排列。
// 注意 如何去重
class Solution {
List<List<Integer>> res = new ArrayList<>();
List<Integer> path = new ArrayList<>();
public List<List<Integer>> permuteUnique(int[] nums) {
boolean[] used = new boolean[nums.length];//用于标记元素是否已经使用
Arrays.fill(used,false);
Arrays.sort(nums);//排序,用于层内去重
backTrack(nums, used);
return res;
}
private void backTrack(int[] nums, boolean[] used) {
if(path.size() == nums.length){
res.add(new ArrayList<>(path));
return;
}
for(int i = 0; i< nums.length; i++){
if(i>0 && nums[i] == nums[i-1] && used[i-1] == false) continue;//层重复
if(used[i] == true) continue;//在路径中使用过了
path.add(nums[i]);
used[i] = true;
backTrack(nums,used);
used[i] = false;
path.remove(path.size()-1);
}
}
}
6、棋盘:遍历棋盘各个位置,判断可放置元素的合法性,合法则放置并判断之后的情况,不行则回溯。可根据当前状态的返回值,判断是否需要继续回溯遍历,返回true,表示在当前状态下其他位置均合理。
51. N 皇后:按照国际象棋的规则,皇后可以攻击与之处在同一行或同一列或同一斜线上的棋子。如何使N个皇后彼此之间不能相互攻击。
class Solution {
List<List<String>> res = new LinkedList<>();
public List<List<String>> solveNQueens(int n) {
char[][] chess = new char[n][n];
for(char[] c: chess){
Arrays.fill(c,'.');
}
helpNQueens(chess, n, 0);
return res;
}
public void helpNQueens(char[][] chess, int n, int row){
if(row == n){
List<String> res1 = new ArrayList<>();
for(char[] c: chess){
res1.add(String.copyValueOf(c));
}
res.add(new LinkedList<>(res1));
return;
}
for(int i = 0; i < n; i++){//每行的每列
if(isValid(chess, n, row, i)){//判断位置是否合法
chess[row][i] = 'Q';
helpNQueens(chess, n, row+1);
chess[row][i] = '.';
}
}
}
public boolean isValid(char[][] chess, int n, int row, int col){
//同行,不可能,一行只放一个
//同列
for(int i = 0; i< row; i++){
if(chess[i][col] == 'Q')
return false;
}
//同一斜线,主对角线
for(int i = row-1,j = col-1; i>=0 && j>=0; i--,j--){
if(chess[i][j] == 'Q')
return false;
}
//同一斜线,副对角线
for(int i = row-1, j = col+1; i>=0 && j<n; i--,j++){
if(chess[i][j] == 'Q')
return false;
}
return true;
}
}
51. N 皇后: 填充空格来解决数独问题。数独的解法需 遵循如下规则:
数字 1-9 在每一行只能出现一次。
数字 1-9 在每一列只能出现一次。
数字 1-9 在每一个以粗实线分隔的 3x3 宫内只能出现一次。(请参考示例图)
数独部分空格内已填入了数字,空白格用 '.' 表示。
class Solution {
public void solveSudoku(char[][] board) {
helpSolveSudoku(board);
}
public boolean helpSolveSudoku(char[][] board){
for(int i = 0; i< board.length; i++ ){
for(int j = 0; j< board[0].length; j++){
if(board[i][j] != '.') continue;
for(char v = '1'; v <= '9'; v++){//存放的是字符
if(isvalid(board,i,j,v)){
board[i][j] = v;
if(helpSolveSudoku(board)) return true;//找到正确的值就一层层向上告知
board[i][j] = '.';
}
}
return false;//9个数均不合适,说明前面排列错误;返回
}
}
return true;//已正确填充
}
public boolean isvalid(char[][] board, int row, int col, char v){
// 行内不重复
for(int j = 0; j<board.length; j++){
if(board[row][j] == v) return false;
}
// 列内不重复
for(int i = 0; i<board[0].length; i++){//一行元素的长度:列数
if(board[i][col] == v) return false;
}
// 九宫格内不重复
for(int i = row/3 *3; i<(row/3+1)*3; i++){
for(int j = col/3 *3; j<(col/3+1)*3; j++){
if(board[i][j] == v) return false;
}
}
return true;
}
}