第七章 回溯算法part01
今日内容:
● 理论基础
● 77. 组合 详细布置
理论基础
回溯是递归的副产物,只要有递归,就一定有回溯。所以回溯函数也是递归函数,递归函数也是回溯函数。
回溯法: 回溯法解决的问题都可以抽象为树形结构。 所有回溯法的问题都可以抽象为树形结构!!!
因为回溯法解决的都是在结合中递归查找子集,集合的大小构成了树的宽度;递归的深度构成了树的深度。
回溯看起来很高深,但是其本质上并不是什么高效的算法,其本质就是穷举。
这里本来应该写回溯法的三要素,但目前我个人并不认为其与递归的三要素有何区别:
这里在此重复一下递归三要素:递归函数返回值与形参、终止条件、每一轮递归过程中的操作。
如果非要说回溯三要素与上述三要素有何区别的话,那就是在每一轮递归的过程中回进行一步回溯操作。这个要具体情况具体分析。
回溯算法的模板
void backtracking(参数) {
if (终止条件) {
存放结果;
return;
}
for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
处理节点;
backtracking(路径,选择列表); // 递归
回溯,撤销处理结果
}
}
77. 组合
未进行剪枝:
class Solution {
//path用来记录组合之后的一组结果 result用来存放所有的结果
List<Integer> path=new ArrayList<>();
List<List<Integer>> result=new ArrayList<>();
public List<List<Integer>> combine(int n, int k) {
backtracking(n,k,1);
return result;
}
//因为已经将path 以及 result设置为了全局变量 所以这里回溯算法并不需要返回东西
public void backtracking(int n,int k,int startIndex){
//也就是找到了一组满足条件的组合
//将其加入到result中
if(path.size()==k){
result.add(new ArrayList(path));
return ;
}
//经过了上面的if语句 一旦到了下面的for循环的话
//这个时候path就一定还没有找到足够的数
for(int i=startIndex;i<=n;i++){
path.add(i);
backtracking(n,k,i+1);
path.removeLast();
}
}
}
第七章 回溯算法part02
今日内容:
● 216.组合总和III
● 17.电话号码的字母组合 详细布置
216.组合总和III
暂未实现任何剪枝的代码 :具体思想看代码注释
class Solution {
List<List<Integer>> result=new ArrayList<>();
List<Integer> path=new ArrayList<>();
public List<List<Integer>> combinationSum3(int k, int n) {
int sum=0;
for(int i=1;i<=k;i++){
sum+=i;
}
//这里就是题中示例3给出的反例 就是我当前能够得到的最小和 都大于了n
//那么根本不可能找到有效的组合
if(sum>n)
return result;
backtracking(k,n,1);
return result;
}
//这里暂时不考虑剪枝
public void backtracking(int k,int n,int startIndex){
//回溯函数的终止条件 —— 找到了叶子节点(即path的size等于k 并且他们之和等于n)
if(path.size()==k){
int tempSum=0;
for(int index=0;index<k;index++){
tempSum+=path.get(index);
}
if(tempSum==n){
//这里一定要额外调用ArrayList的构造函数 利用path重新创建一个ArrayList
result.add(new ArrayList(path));
}
return;
}
//当path中的数的个数还没有达到k时
for(int i=startIndex;i<=9;i++){
path.add(i);
backtracking(k,n,i+1);
//回溯
path.removeLast();
}
}
}
上面的代码可进行小小的优化——在每取到一个数时候,就直接计算其sum
既可以将sum放在全局变量中,也可以将其传递进入回溯函数。
class Solution {
List<List<Integer>> result=new ArrayList<>();
List<Integer> path=new ArrayList<>();
public List<List<Integer>> combinationSum3(int k, int n) {
backtracking(k,n,1,0);
return result;
}
public void backtracking(int k,int n,int startIndex,int curSum){
if(path.size()==k){
if(curSum==n)
result.add(new ArrayList(path));
return;
}
for(int i=startIndex;i<=9;i++){
//每次将一个新的数放入path之后 curSum也要对应地加上这个数
path.add(i);
curSum+=i;
backtracking(k,n,i+1,curSum);
//回溯同理 curSum也要对应地减去这个数
path.removeLast();
curSum-=i;
}
}
}
17.电话号码的字母组合
这道题目:电话号码的字母组合。是涉及到不同集合之间的组合。
这里我一开始就想错了,知道是使用回溯,但是仍然纠结于用两层for循环(期望是两层for循环+递归来解答该题)。 个人谨记 : 一旦要使用了递归函数,其目的就是为了省略很多层的for循环。
class Solution {
//打表格
Map<Character,String> map=new HashMap<>();
//result用来记录最终结果
List<String> result=new ArrayList<>();
//StringBuilder path是用来在digits中给了多个“数字”时 在回溯函数中用来记录组合的
StringBuilder path=new StringBuilder();
public List<String> letterCombinations(String digits) {
//打表格
map.put('2',"abc"); map.put('3',"def"); map.put('4',"ghi"); map.put('5',"jkl");
map.put('6',"mno"); map.put('7',"pqrs");map.put('8',"tuv"); map.put('9',"wxyz");
//如果digits里面什么都没有
if(digits.equals("")){
return new ArrayList<>();
}
//如果digits只给了一个“数字”的话
if(digits.length()==1){
String str=map.get(digits.charAt(0));
for(int i=0;i<str.length();i++){
StringBuilder strBild=new StringBuilder();
strBild.append(str.charAt(i));
result.add(strBild.toString());
}
return result;
}
//接下来就剩下digits中多个“数字”的情况 要处理
backtracking(digits,0);
return result;
}
//
public void backtracking(String digits,int num){
//如果递归到了叶子节点 并且path的length等于digits.length ()
//这时候就说明我们已经找到了一个组合
if(path.length()==digits.length()){
//因为我们这里设定path是StringBuilder对象类型 要调用其toString()方法完成转换
result.add(path.toString());
return;
}
//获取当前“数字”所对应的字符串 如'2'对应"abc"
String s=map.get(digits.charAt(num));
for(int i=0;i<s.length();i++){
path.append(s.charAt(i));
backtracking(digits,num+1);
path.deleteCharAt(path.length()-1);
}
}
}
第七章 回溯算法part03
● 39. 组合总和
● 40.组合总和II
● 131.分割回文串 详细布置
39. 组合总和
本题是 集合里元素可以用无数次,那么和组合问题的差别 其实仅在于 startIndex上的控制
具体代码: 我感觉应该是考虑到了剪枝 就是判断curSum>target那里
个人心得记录:没想到这道理在没有看任何解析的情况下做出来了。个人总结主要原因如下:
-
我知晓了什么情况下使用递归OR回溯
-
当题目本身的数据结构就是有关树或者二叉树的,就要尝试使用递归
-
当题目的相关问题能够转换成一棵树的时候,就要尝试递归
-
不要忘了递归OR回溯三要素,
-
class Solution {
List<List<Integer>> result;
List<Integer> path;
public List<List<Integer>> combinationSum(int[] candidates, int target) {
//初始化
result=new ArrayList<>();
path=new ArrayList<>();
backtracking(candidates,target,0,0);
//因为result是全局的 backtracking调用完之后 result就更新了
return result;
}
//考虑使用回溯算法
//特别注意这里元素是可以无限制地重复选用的
public void backtracking(int[] candidates,int target,int curSum,int startIndex){
//终止条件:
//当前的和 大于了 目标和,那么return
if(curSum>target){
return;
}
//如果当前的和等于了目标和,那么就要存入result 当然也要return
if(curSum==target){
result.add(new ArrayList(path));
return;
}
for(int i=startIndex;i<candidates.length;i++){
//将该问题转换为一棵树 先处理当前遇到的节点
//先把当前节点加入到path中 curSum也要更新
curSum+=candidates[i];
path.add(candidates[i]);
//递归
//因为元素是可以重复使用的 所以还是从当前的i开始
backtracking(candidates,target,curSum,i);
//回溯
curSum-=candidates[i];
//path移除最后一个元素
path.remove(path.size()-1);
}
}
}
40.组合总和II
本题开始涉及到一个问题了:去重。
注意题目中给我们 集合是有重复元素的,那么求出来的 组合有可能重复,但题目要求不能有重复组合。
class Solution {
//总体思路感觉应该跟39组合总数一样。 但是这里解集不能包含重复的组合。
List<Integer> path;
List<List<Integer>> result;
boolean[] used;
public List<List<Integer>> combinationSum2(int[] candidates, int target) {
//这里尝试先排序 利用排序后的candidates再来组合 以此来尝试避免重复
Arrays.sort(candidates);
//初始化
used=new boolean[candidates.length];
Arrays.fill(used,false);
result=new ArrayList<>();
path=new ArrayList<>();
backtracking(candidates,target,0,0);
return result;
}
public void backtracking(int[] candidates,int target,int curSum,int startIndex){
if(curSum>target){
return;
}
if(curSum==target){
result.add(new ArrayList(path));
return;
}
for(int i=startIndex;i<candidates.length;i++){
//排除可能重复出现的组合
//因为我的candidates是排过序之后的
//所以唯一可能出现重复组合的情况就是 树的同一层中出现重复的数(而不是同一个树枝中)
if( i>0 && candidates[i]==candidates[i-1] && used[i-1]==false){
//开启下一轮
continue;
}
//遇到节点 先处理当前节点
curSum+=candidates[i];
path.add(candidates[i]);
used[i]=true;
//递归处理下一个节点
backtracking(candidates,target,curSum,i+1);
//回溯
curSum-=candidates[i];
path.remove(path.size()-1);
used[i]=false;
}
}
}
131.分割回文串
class Solution {
List<List<String>> result;
List<String> path;
public List<List<String>> partition(String s) {
result=new ArrayList<>();
path=new ArrayList<>();
backtracking(s,0);
return result;
}
//在分割问题中 startIndex可以类比成切割的那条线
public void backtracking(String s,int startIndex){
if(startIndex>=s.length()){
result.add(new ArrayList(path));
return;
}
for(int i=startIndex;i<s.length();i++){
//如果是回文子串 那么就进行记录
if(isPalindromicString(s,startIndex,i)){
String str=s.substring(startIndex,i+1);
path.add(str);
}
else
continue;
backtracking(s,i+1);
path.remove(path.size()-1);
}
}
//判断是否是回文子串
public boolean isPalindromicString(String s,int startIndex,int end){
//使用双指针来遍历
int left=startIndex;
int right=end;
while(left<=right){
if(s.charAt(left)!=s.charAt(right)){
return false;
}
left++;
right--;
}
return true;
}
}
28 第七章 回溯算法
● 93.复原IP地址
● 78.子集
● 90.子集II 详细布置
93.复原IP地址
真没想到 这道题我竟然能做出来。
个人记录: 因为时间原因,下次有时间看卡哥的讲解以及代码的优化问题
class Solution {
List<String> result;
List<String> path;
public List<String> restoreIpAddresses(String s) {
// 初始化
//result不用多说 就是用来存储返回的结果的。
//path是用来存储被切的每一小段 最后将path中存储的每一小段合在一起 就只会是result中的一个结果
result=new ArrayList<>();
path=new ArrayList<>();
backtracking(s,0,0);
return result;
}
public void backtracking(String s,int stratIndex,int IPlength){
//return的条件
//如果path被切成了四段 那么就是到了叶子结点
if(path.size()==4){
//如果是切割出来的是符合要求的IP地址
//将其连接在一起 并且存入到result之中
if(isIP(path) && IPlength==s.length()){
//将path中的每一小段合并在一起 然后存入到result中 记得加上点.
StringBuilder sBuilder=new StringBuilder();
for(int index=0;index<path.size()-1;index++){
sBuilder.append(path.get(index));
sBuilder.append('.');
}
sBuilder.append(path.get(path.size()-1));
result.add(new String(sBuilder.toString()));
}
return;
}
for(int i=stratIndex;i<s.length();i++){
//String中的substring方法是包前不包后
//这里尝试不做任何判断
String str=new String();
//因为所谓切除来的IP地址(暂不考虑IP地址是否正确) 它要满足两个基本要求
//1、必须要切四段 2、必须要将整个s切完
if(path.size()<4)
str=s.substring(stratIndex,i+1);
else
str=s.substring(stratIndex,s.length());
path.add(str);
IPlength+=str.length();
backtracking(s,i+1,IPlength);
//这里要先处理IPlength 再去path.remove
IPlength-=path.get(path.size()-1).length();
path.remove(path.size()-1);
}
}
//判断是否是符合要求的IP地址
//这里考虑的是这个IP地址已经被完完整整切完了
public boolean isIP(List<String> list){
if(list.size()!=4)
return false;
for(int i=0;i<list.size();i++){
//先判断不能是 前导0
String str=list.get(i);
if(str.length()>1 && str.charAt(0)=='0')
return false;
//每个整数必须位于 0 到255之间
//因为如果直接Integet.parseInt(str)的话 他可能会溢出 比如"5525511135" 所以首先只需要简单判断一下
//只要str的长度>=4了 那么它肯定就超过了255了 其长度<4的话 还有后面的那个条件 所以可以完成判断
if(str.length()>=4 || Integer.parseInt(str) - 255 >0)
return false;
}
return true;
}
}
78.子集
这道题我个人感觉比之前的都重要一些
它考察的点反而更加倾向于递归or回溯的三要素:
-
递归函数返回值以及形参是什么
-
递归函数什么时候return
-
每轮递归的时候具体做什么
这道题考察的重点在于递归函数什么时候return。 一开始我也是在头脑风暴,空想半天想不出来,但是一动笔在纸上画一画这棵树 ,一下子就明了了。
好记性不如烂笔头,古人诚不欺我。
class Solution {
//nums中给了2个数字 就要2个for循环 给了5个数字就要5个for循环
//在出现n个for循环 且完全不知道n是多少的情况下 就可以使用递归&回溯方法
//感觉这道题就是组合问题
//空集就单独一开始直接加入即可 暂不考虑放入递归中
List<Integer>path;
List<List<Integer>> result;
public List<List<Integer>> subsets(int[] nums) {
path=new ArrayList<>();
result=new ArrayList<>();
result.add(new ArrayList());
backtracking(nums,0);
return result;
}
//因为这道题奇怪的地方在于 它并不是很好判断什么时候到了叶子节点
//它并不像找到复核某个targetSum的组合问题
public void backtracking(int[]nums,int startIndex){
//比如根据题目中的示例 我们可以发现{1,2,3} 每次递归到3 就可以终止 然后return了
if(startIndex==nums.length)
return;
for(int i=startIndex;i<nums.length;i++){
path.add(nums[i])
//本题中应该不会出现“反例” 碰到就直接加入即可
result.add(new ArrayList(path));
backtracking(nums,i+1);
path.remove(path.size()-1);
}
}
}
90.子集II
这道题是在78子集 题目的基础上 新增了一个nums中可能包含重复元素的情况。
也就是说要进行去重的操作。因为一旦有重复的元素 就完全有可能出现重复的子集
class Solution {
List<Integer> path;
List<List<Integer>> result;
boolean[] used;
public List<List<Integer>> subsetsWithDup(int[] nums) {
path=new ArrayList<>();
result=new ArrayList<>();
//
used=new boolean[nums.length];
Arrays.fill(used,false);
//在nums经过排序的情况下, 如果nums[i-1]在此前已经取到集合了
//那么与nums[i-1]同值的nums[i]就没有任何用了 因为他们必定取到的是相同的集合
Arrays.sort(nums);
result.add(new ArrayList<>());
backtracking(nums,0);
return result;
}
public void backtracking(int[] nums,int startIndex){
if(startIndex==nums.length)
return;
for(int i=startIndex;i<nums.length;i++){
if (i > 0 && nums[i] == nums[i - 1] && used[i - 1] == false) {
continue;
}
path.add(nums[i]);
result.add(new ArrayList(path));
used[i] = true;
backtracking(nums, i + 1);
used[i] = false;
path.remove(path.size()-1);
}
}
}