代码随想录算法训练营Day28|回溯part01

117 阅读5分钟

回溯算法理论基础

回溯本质上就是递归,是递归的副产品。回溯函数也就指递归函数。 但回溯本质上也是穷举,某些时候可以加一些剪枝,但本质上仍然是一种暴力搜索。

回溯适用的问题

通常是一些比较困难,除了搜索所有解空间没别的办法的问题。

  1. 组合问题:N个数里面按一定规则找出k个数的集合
  2. 切割问题:一个字符串按一定规则有几种切割方式
  3. 子集问题:一个N个数的集合里有多少符合条件的子集
  4. 排列问题:N个数按一定规则全排列,有几种排列方式
  5. 棋盘问题:N皇后,解数独等等

理解回溯

所有使用回溯法解决的问题都可以抽象为树结构。 回溯法解决的都是在集合中递归查找子集,集合的大小就构成了树的宽度,递归的深度就构成了树的深度

回溯三部曲模版

  1. 回溯函数的参数和返回值:返回值通常为空
  2. 回溯函数的结束条件 满足结束条件,就保存下结果,返回
  3. 回溯搜索的遍历过程
for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
    处理节点;
    backtracking(路径,选择列表); // 递归
    回溯,撤销处理结果
}

LeetCode 77 组合

题目链接:leetcode.cn/problems/co…

文档讲解:programmercarl.com/0077.组合.htm…

视频讲解:www.bilibili.com/video/BV1ti…

思路

考虑一条选择路径,当已经选择m时,后面的数只能在m+1到n之间选择,否则就会对同一种组合重复访问(组合无序)

考虑一条未完成路径,其缺少的个数大于m+1~n之间的个数,已经无法满足这样的组合,可以直接剪枝返回

考虑回溯三要素:

  1. 回溯函数的参数和返回值 参数:要选的组合长度k,最大限制n 返回值:空 全局变量:结果集合result,当前选择路径path
  2. 回溯函数的结束条件 path长度为k时,把path副本记录在result中,返回 如果path最后一个数与n之间的个数小于仍需要选择的数值个数,返回
  3. 回溯搜索的遍历过程 假设m是path最后一个数,循环选择m+1~n之间的数 1. 加入path 2. 递归调用回溯函数 3. 还原path

解法

class Solution {
	List<List<Integer>> result;	
	List<Integer> path;
		
	public List<List<Integer>> combine(int n, int k) {	
		result = new ArrayList<>();		
		path = new ArrayList<>();		
		backTracking(n, k);		
		return result;	
	}	
	  	
	public void backTracking(int n, int k) {	
		if (path.size() == k) {		
			result.add(new ArrayList<>(path));			
			return;		
		}		
		int depth = path.size();		
		// 剪枝		
		if (depth != 0 && k - depth > n - path.get(depth - 1)) {		
			return ;		
		}		
		// 横向遍历		
		int i;		
		if (depth == 0) {		
			i = 1;		
		}		
		else {		
			i = path.get(depth - 1) + 1;		
		}		
		for (; i < n+1; i++) {		
			path.add(i);			
			backTracking(n, k);			
			path.remove(path.size() - 1);		
		}	
	}
}

LeetCode 216 组合总和III

题目链接:leetcode.cn/problems/co…

文档讲解:programmercarl.com/0216.组合总和II…

视频讲解:www.bilibili.com/video/BV1wg…

思路

使用k个不同的1~9之间的数,组合出和为n的组合

考虑剪枝:

  1. 和上题类似,由于组合无序,选择数字时只能向后选。如果可供选择的数量小于仍需要的数量,就无法在子树里找到满足要求的组合
  2. 本题的条件有求和,如果可供选择的数字全部大于现在的组合中缺的数值,也可以返回。
  3. 同样如果和sum已经大于等于目标和n,且数量不够,返回

考虑回溯三要素:

  1. 回溯函数的参数和返回值 参数:总和n,个数k 返回值:空 全局变量:结果集合result,选择路径path,选择路径的和sum
  2. 回溯函数的结束条件 path长度等于k且sum等于n,记录到result,返回 path满足剪枝条件,返回
  3. 回溯搜索的遍历过程 对FROM path最后一个数+1 TO 9进行遍历 1. 把数加入path,更新sum 2. 递归调用回溯函数 3. 还原path和sum

解法

class Solution {	
	List<List<Integer>> result;	
	List<Integer> path;	
	int sum;
	
	public List<List<Integer>> combinationSum3(int k, int n) {	
		result = new ArrayList<>();		
		path = new ArrayList<>();		
		sum = 0;		
		backTracking(k, n);		
		return result;	
	}		  
	
	public void backTracking(int k, int n) {	
		int depth = path.size();		
		if (depth == k) {		
			if (sum == n) {			
				result.add(new ArrayList<>(path));			
			}			
			return ;		
		}		
		if (sum >= n) {		
			return ;		
		}		
		if (depth != 0 && (k-depth) > (9-path.get(depth-1))) {		
			return ;		
		}		
		if (depth != 0 && path.get(depth-1)+1 > n-sum) {		
			return ;		
		}				  
		
		int i;		
		if (depth == 0) {		
			i = 1;		
		}		
		else {		
			i = path.get(depth-1)+1;		
		}		
		for (; i < 10; i++) {		
			path.add(i);			
			sum += i;			
			backTracking(k, n);			
			sum -= i;			
			path.remove(path.size()-1);		
		}	
	}
}

LeetCode 17 电话号码的字母组合

题目链接:leetcode.cn/problems/le…

文档讲解:programmercarl.com/0017.电话号码的字…

视频讲解:www.bilibili.com/video/BV1yV…

思路

先构造出一个int映射到字符list的map,方便查阅

考虑回溯三要素:

  1. 回溯函数的参数和返回值 参数:数字字符串digits 返回值:空 全局变量:map,结果集合result,选择路径path
  2. 回溯函数的结束条件 path长度等于digits,path的副本加入result
  3. 回溯搜索的遍历过程 对下一个数字,循环其可以对应的字母 1. 把字母加入path 2. 递归调用回溯函数 3. 把path还原

解法

class Solution {
	Map<Character, List<Character>> map;	
	StringBuilder path;	
	List<String> result;
		
	public List<String> letterCombinations(String digits) {	
		map = new HashMap<>();		
		map.put('2', Arrays.asList('a', 'b', 'c'));		
		map.put('3', Arrays.asList('d', 'e', 'f'));		
		map.put('4', Arrays.asList('g', 'h', 'i'));		
		map.put('5', Arrays.asList('j', 'k', 'l'));		
		map.put('6', Arrays.asList('m', 'n', 'o'));		
		map.put('7', Arrays.asList('p', 'q', 'r', 's'));		
		map.put('8', Arrays.asList('t', 'u', 'v'));		
		map.put('9', Arrays.asList('w', 'x', 'y', 'z'));				  
		
		result = new ArrayList<>();		
		path = new StringBuilder();		
		if (digits.length() == 0) {		
			return result;		
		}		
		backTracking(digits);		
		return result;	
	}
		  	
	public void backTracking(String digits) {	
		if (digits.length() == path.length()) {		
			result.add(path.toString());			
			return ;		
		}		
		char digit = digits.charAt(path.length());		
		for (char ch : map.get(digit)) {		
			path.append(ch);			
			backTracking(digits);			
			path.deleteCharAt(path.length()-1);		
		}	
	}
}

今日收获总结

今日学习时长2小时,回溯思想和递归类似,但回溯需要对某些状态的还原多加考虑