目录
216.组合总和III
题目描述
解题思路
强调的是组合而不是排列,例如(1,2)和(2,1)则重复
为什么用回溯算法是因为否则就只能嵌套k层循环
任何一个回溯算法都可以抽象成一个树形结构
在回溯算法过程中,每一步只能往该元素的后一位元素作为开头开始取(例如当前为2,剩余的元素则取范围[3,9]中)
【1. 题目不能重复取,所以不能取当前值;2. 为什么不取1是因为如果取了则有可能存在(1,2)和(2,1)同时存在的情况,因为强调组合而不是排列即重复】
k控制了整个树形结构的深度,集合的[1,9]也就是n控制了宽度
引用自代码随想录中对应算法题目的图来辅助理解
代码
全局变量:
(path):一维数组,收集单条符合条件的路径【用LinkedList因为需要往尾部减掉或增加元素】
(result):二维数组,放满足题意的一维数组path集合【用ArrayList因为不知道解有多少个】
递归参数:
targetSum:要满足题意的和;
k:要满足题意的k是多少;
sum:收集当前单挑路径的当前和;
startIndex:控制接下来的for循环从哪个范围开始取值【题目要求从1开始】
终止条件:
已知k是深度,path是把取得元素放进去的一个容器。那么当path的当前大小等于题目要求的k就结束了(if path.size() == k)就没有必要往深度更深的地方递归了,再从满足的k的深度里面寻找当前之和等于目标之和的集合(if targetSum == sum)则往result里面添加满足的path(result.add(path))
从startIndex开始遍历树的宽度(for (int i=startIndex, i<9【1-9范围内】, i++)),当前之和更新(sum+=i)同时往paht里面加值(path.add(i)),然后开始下层递归backTracking(targetSum, k, sum, i+1)【为什么是i+1开始取,因为是从当前的数的下一个作为范围的起始开始】
回溯过程(sum-=i; path.removeLast(i))【把当前之和去掉当前元素,再从满足单条符合条件的路径中pop出当前元素:例如当前path是(1,2)要回溯到path是(1)然后递归启动变成path(1,3)】
剪枝操作:
1. 在代码最开始:在每一次进行下次递归之前先检查当前之和sum值是否已经大于targetSum,如果大于则没有必要再进行下次递归,直接返回(if (sum > targetSum) return )
2. for循环中对于i的边界起始位置的剪枝控制:如果k的数量等于某个值,则从某个值开始取则没意义【例如k=2时,则只遍历到8就可以了,因为遍历9的话后面没有数了,不够k=2个】
已知需要知道当前递归层中path里已经有几个元素了(path.size()),我们总共需要多少个元素(k),因此还需要的元素数量是(k-path.size())个。【那么如何让边界的起始位置停留在“至多”的头部呢?也就是需要n【树的宽度的范围上限】减去(k-path.size)【还需要的元素数量】+1【+1来补齐下标】】(把for循环中的i<9中的9替换成9-(k-path.size())+1)
引用自代码随想录中对应算法题目的图来辅助理解
class Solution {
// 存放解:因为解的总个数未知所以用ArrayList
List<List<Integer>> result = new ArrayList<>();
// 当前单条路径存放的元素集合
LinkedList<Integer> path = new LinkedList<>();
public List<List<Integer>> combinationSum3(int k, int n) {
// 当前sum初始为0;递归的时候每次要开始的范围的上界初始化为1,因为范围是从1-9取
backTracking(k, n, 0, 1);
return result;
}
// 解需要满足的数量、目标之和、当前之和、递归的时候每次要开始的范围的上界
private void backTracking(int k, int targetSum, int sum, int startIndex) {
// 还需要减枝情况:需要在最开始检查当前之和是否大于目标之和
if (sum > targetSum) return;
// 终止条件:当数量为k,如果sum也等于targetSum则返回
if (path.size() == k) if (sum == targetSum) {
// 往result里加入满足条件的path(需要new否则没有创建副本会影响到其他的新变量path)
result.add(new LinkedList(path));
return;
}
// 单层递归逻辑(遍历树的宽度,需要进行减枝,因为如果还需要的元素数量大于规定的范围剩余的数量则没必要遍历)
// 这里需要小于等于而不是小于因为已经+1补齐了0的下标
for (int i = startIndex; i <= 9-(k-path.size())+1; i++) {
// 当前之和加入当前元素
sum += i;
// path也加入当前元素
path.add(i);
// 递归下一个;此时传入的startIndex是i+1因为可取范围的上界不能是之前的值(因为否则会出现顺序相反的数组,此题强调组合即忽略顺序)也不能是当前值(否则有相同值重复出现的情况)
backTracking(k, targetSum, sum, i+1);
// 回溯
sum -= i;
path.removeLast();
}
}
}
93.复原IP地址
题目描述
解题思路
非正整数字符、数字前有0、每个部分大于255则不合法
抽象出的树形结构
引用自代码随想录中对应算法题目的图来辅助理解
代码
全局变量:
result:存放所有分割出来的合法的字符串【若干个字符串用ArrayList】
传入参数:
s:总字符串
startIndex:也就是切割线,因为不能重复分割,所以记录下一层递归分割的起始位置
pointSum:因为是ip地址形式,需要逗号分隔;
终止条件(分割的ip段数作为终止条件):
如果pointSum等于3则终止【其实就是树的深度是3,相当于如果超过3就没必要再往下层去分割了】;因为ip地址是4个字符子串,所以当pointSum等于3时还要再对最后一个子串进行合法性判断(if isValid(s, startIndex, s.size()-1))。如果合法则加入result数组
单层搜索的逻辑:
取数进行分割的过程(for (i=startIndex; i<s.size(); i++)
每一个字符串要先进行合法性判断,合法了再进行分割(if isValid(s, startIndex, i))【右区间是i是因为,i只是最开始是等于startIndex,后面会不断移动,最终移动停留的位置就是i的位置】如果合法再向下一层进行递归;先进行子串的改造,即插入逗号(insert函数),将pointSum+=1。再进行下一层递归(backTracking(s, i+2, pointSum))【i+2是因为还插入了一个逗号所以不是+1而是+2】
如果不合法则直接减掉分支跳出循环
回溯:
s.erase();
pointSum-=1;
判断子串是否合法:
非正整数字符、数字前有0、每个部分大于255则不合法
class Solution {
// 全局变量result:存储若干个可能的字符串答案
List<String> result = new ArrayList<>();
public List<String> restoreIpAddresses(String s) {
// 用StringBuilder节省时间
StringBuilder sb = new StringBuilder(s);
backTracking(sb, 0, 0);
return result;
}
// 传入参数:startIndex(控制分割线);pointSum(逗号的数量)
private void backTracking(StringBuilder s, int startIndex, int pointSum) {
// 终止条件:如果pointSum等于3则再判断最后一部分是否合法,如果合法则加入result
if (pointSum == 3) {
if (isValid(s, startIndex, s.length()-1)) {
result.add(s.toString());
} else return;
}
// 单层循环的逻辑:
// 循环条件,起点i从startIndex开始,字符串长度结束
for (int i = startIndex; i < s.length(); i++) {
// 先判断合法性,如果合法先进行加逗号改造字符串再进行递归:右区间是i是因为startIndex是起点,i会一直移动
if (isValid(s, startIndex, i)) {
s.insert(i+1, '.');
pointSum += 1;
// i+2而不是+1是因为加了个逗号
backTracking(s, i+2, pointSum);
// 回溯
s.deleteCharAt(i+1);
pointSum -= 1;
} else break;
}
}
private boolean isValid(StringBuilder s, int start, int end) {
// 把非法的情况都返回false,剩余情况都是true
// 起点索引大于终点索引则非法
if (start > end) return false;
// 如果是多位则首位不能为0但是如果是一位则可以为0
if (s.charAt(start) == '0' && start != end) return false;
// 每一段不能大于255
int sum = 0;
for (int i = start; i <= end; i++) {
// 每一位对应的整数值(减去'0'意味着去掉0的ascall码48)
int digit = s.charAt(i) - '0' ;
// 每处理一位则往左进一位
sum = sum * 10 + digit;
if (sum > 255) return false;
}
return true;
}
}