系列文章目录
刷题笔记(一)–数组类型:二分法
刷题笔记(二)–数组类型:双指针法
刷题笔记(三)–数组类型:滑动窗口
刷题笔记(四)–数组类型:模拟
刷题笔记(五)–链表类型:基础题目以及操作
刷题笔记(六)–哈希表:基础题目和思想
刷题笔记(七)–字符串:经典题目
刷题笔记(八)–双指针:两数之和以及延伸
刷题笔记(九)–字符串:KMP算法
刷题笔记(十)–栈和队列:基础题目
刷题笔记(十一)–栈和队列:Top-K问题
刷题笔记(十二)–复习:排序算法
刷题笔记(十三)–二叉树:前中后序遍历(复习)
刷题笔记(十四)–二叉树:层序遍历和DFS,BFS
刷题笔记(十五)–二叉树:属性相关题目
刷题笔记(十六)–二叉树:修改与构造
刷题笔记(十七)–二叉搜索树:关于属性问题
刷题笔记(十八)–二叉树:公共祖先问题
刷题笔记(十九)–二叉树:二叉搜索树的修改与构造
刷题笔记(二十)–回溯算法:组合问题
前言
这一篇博客是讲解一下回溯算法的其他题型。
分割问题:一个字符串按一定规律有几种切割方式
子集问题:一个N个数的集合有多少个符合条件的子集
全排列问题:N个数按照一定的条件排列,有几种排列方式
题录
131. 分割回文串
题目链接如下:
题目截图如下:
这道题的难点在哪里呢?就是关于字符串的分割问题
1.怎么把切割问题抽象为组合问题
2.你的递归终止条件
3.你如何进行切割
4.递归中如何处理
5.回文序列的判断
首先进行第一个问题,如何把切割问题抽象为组合问题,那么就先上模板
public class 分割回文串 {
List<List<String>> resList = new LinkedList<>();//用来保存最后正确的结果
Deque<String> myList = new LinkedList<>();//用来保存当前的结果
public List<List<String>> partition(String s) {
}
public void backstracking(int startIndex,String s){
}
}
然后就是第二个问题了,怎么终止?如果说后面没有字符串要切割了,那就终止。
if(startIndex > s.length()){
resList.add(new LinkedList<>(myList));
return;
}
然后就是第三个问题和第五个问题,你怎么截取子串,因为你只有回文子串才能添加,所以这里一起讨论
//如果说当前的子串是回文串,那么就把当前子串加入到子串集中
if(isSame(s,startIndex,i)){
String string = s.substring(startIndex,i+1);
myList.add(string);
}else{
continue;
}
//判断是否是回文
public boolean isSame(String s,int start,int end){
String tmpStr = s.substring(start, end + 1);
StringBuilder str = new StringBuilder(tmpStr).reverse();
return tmpStr.equals(str.toString());
}
最后就是如果进行切割,这个就可以用组合问题来理解了
import java.util.Deque;
import java.util.LinkedList;
import java.util.List;
public class 分割回文串 {
List<List<String>> resList = new LinkedList<>();//用来保存最后正确的结果
Deque<String> myList = new LinkedList<>();//用来保存当前的结果
public List<List<String>> partition(String s) {
backstracking(0,s);
return resList;
}
public void backstracking(int startIndex,String s){
//终止条件的判断:如果说后面已经没有地方可以切割了,那么就把当前的子串集合加入结果集中
if(startIndex > s.length()){
resList.add(new LinkedList<>(myList));
return;
}
for (int i = startIndex; i < s.length(); i++) {
//如果说当前的子串是回文串,那么就把当前子串加入到子串集中
if(isSame(s,startIndex,i)){
String string = s.substring(startIndex,i+1);
myList.add(string);
}else{
continue;
}
//去切割下一个子串
backstracking(i + 1,s);
myList.removeLast();
}
}
//判断是否是回文
public boolean isSame(String s,int start,int end){
String tmpStr = s.substring(start, end + 1);
StringBuilder str = new StringBuilder(tmpStr).reverse();
return tmpStr.equals(str.toString());
}
}
这里还需要特别提醒一下,你这里判断回文,可以使用双指针法,也可以使用StringBuilder方法,如果你使用的是后者也就是我的这种方法,那么一定要注意转化为string再进行比较。
93. 复原 IP 地址
题目链接如下:
题目截图如下:
这里直接给解法吧,先来第一个比较容易理解的
class Solution {
List<String> resList = new LinkedList<>();
public List<String> restoreIpAddresses(String s) {
if(s.length() > 12){
return resList;
}
backstracking(s,0,0);
return resList;
}
//count用来记录当前的小数点的个数
public void backstracking(String str,int startIndex,int count){
if(count == 3){
if(judge(str,startIndex,str.length() - 1)){
resList.add(str);
}
return;
}
for (int i = startIndex; i < str.length(); i++) {
if(judge(str,startIndex,i)){
count++;
str = str.substring(0,i + 1) + "." + str.substring(i + 1);
backstracking(str,i + 2,count);
str = str.substring(0,i+1) + str.substring(i +2);
count--;
}else{
break;
}
}
}
public boolean judge(String str,int begin,int end){
if(end - (begin - 1) > 3 || begin > end){
return false;
}
if(str.charAt(begin) == '0' && begin != end) return false;
int num = 0;
for (int i = begin; i <= end; i++) {
if(str.charAt(i) > '9' || str.charAt(i) < '0'){
return false;
}
num = num * 10 + str.charAt(i) - '0';
if(num > 255){
return false;
}
}
return true;
}
}
然后是接下来的这种,有剪枝操作的
class Solution {
List<String> result = new ArrayList<String>();
StringBuilder stringBuilder = new StringBuilder();
public List<String> restoreIpAddresses(String s) {
restoreIpAddressesHandler(s, 0, 0);
return result;
}
// number表示stringbuilder中ip段的数量
public void restoreIpAddressesHandler(String s, int start, int number) {
// 如果start等于s的长度并且ip段的数量是4,则加入结果集,并返回
if (start == s.length() && number == 4) {
result.add(stringBuilder.toString());
return;
}
// 如果start等于s的长度但是ip段的数量不为4,或者ip段的数量为4但是start小于s的长度,则直接返回
if (start == s.length() || number == 4) {
return;
}
// 剪枝:ip段的长度最大是3,并且ip段处于[0,255]
for (int i = start; i < s.length() && i - start < 3 && Integer.parseInt(s.substring(start, i + 1)) >= 0
&& Integer.parseInt(s.substring(start, i + 1)) <= 255; i++) {
// 如果ip段的长度大于1,并且第一位为0的话,continue
if (i + 1 - start > 1 && s.charAt(start) - '0' == 0) {
continue;
}
stringBuilder.append(s.substring(start, i + 1));
// 当stringBuilder里的网段数量小于3时,才会加点;如果等于3,说明已经有3段了,最后一段不需要再加点
if (number < 3) {
stringBuilder.append(".");
}
number++;
restoreIpAddressesHandler(s, i + 1, number);
number--;
// 删除当前stringBuilder最后一个网段,注意考虑点的数量的问题
stringBuilder.delete(start + number, i + number + 2);
}
}
}
78. 子集
题目链接如下:
题目截图如下:
这个题目和集合问题很相似了,就是最简单的哪一种题型。
import java.util.Deque;
import java.util.LinkedList;
import java.util.List;
public class 子集 {
List<List<Integer>> resList = new LinkedList<>();
Deque<Integer> list = new LinkedList<>();
public List<List<Integer>> subsets(int[] nums) {
backstracking(nums,0);
return resList;
}
public void backstracking(int[] nums,int index){
//不论啥时候哈,直接加入到结果集当中
resList.add(new LinkedList<>(list));
//这里可能会有人问:你没有终止条件嘛?起始是有的,就是当index走到nums的终点的时候
//也就是下面的这个循环根本进不去的时候
for (int i = index; i < nums.length; i++) {
list.add(nums[i]);
backstracking(nums,i + 1);
list.removeLast();
}
}
}
90. 子集 II
题目链接如下:
题目截图如下:
这个题目和我上一篇的刷题博客中的题目还是挺相似的。也就是说这里其实还是有另外一种思路的,就是使用一个数组来保存一个数字是否被遍历过,如果说你当前元素的前一个数组元素已经被遍历过了,那么就没有必要再去使用,但是这种方式显然没有这里直接判断来的直接。
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
public class 子集2 {
List<List<Integer>> resList = new LinkedList<>();
LinkedList<Integer> list = new LinkedList<>();
public List<List<Integer>> subsetsWithDup(int[] nums) {
Arrays.sort(nums);
backstracking(nums,0);
return resList;
}
public void backstracking(int nums[],int startIndex){
resList.add(new LinkedList<>(list));
for (int i = startIndex; i < nums.length; i++) {
//这里就是加了一个判断,如果说前一个元素已经被判断过了,那就直接跳过就行
//但是注意,这里是 i > 当前递归的初始下标
if(i > startIndex && nums[i] == nums[i - 1]){
continue;
}
list.add(nums[i]);
backstracking(nums,i+1);
list.removeLast();
}
}
}
46. 全排列
题目链接如下:
题目截图如下:
说真的,上篇博客的题目好好做了之后,单单这道题来讲,直接就可以秒了。
这里需要注意的就是因为每一次都是从头开始遍历,所以不需要初始下标了。仅仅就是需要判断一下当前元素是否已经存在过了。
import java.util.LinkedList;
import java.util.List;
public class 全排列 {
List<List<Integer>> resList = new LinkedList<>();//结果集
LinkedList<Integer> list = new LinkedList<>();
//回溯
public List<List<Integer>> permute(int[] nums) {
backstacking(nums);
return resList;
}
public void backstacking(int[] nums){
if(list.size() == nums.length){
resList.add(new LinkedList<>(list));
}
for (int i = 0; i < nums.length; i++) {
if(list.contains(nums[i])) continue;
list.add(nums[i]);
backstacking(nums);
list.removeLast();
}
}
}
47. 全排列 II
题目链接如下:
题目截图如下:
这道题目就是典型的去重题目,但是这里要注意了,我们的去重思路有两种,一种是树枝上去重,一种是树层上去重,树枝上去重比较繁琐,而树层上去重效率就比较高,可能会有同学问了,什么是树枝上去重,什么是树层上去重?关于这一点,还是去翻看我前面的博客,等等…我前面的博客写法不太好像不太适用于这个题目,那这道题就重新写一次吧
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
public class 全排列2 {
List<List<Integer>> resList = new LinkedList<>();
List<Integer> list = new LinkedList<>();
public List<List<Integer>> permuteUnique(int[] nums) {
//用一个boolean数组来记录元素遍历过与否,因为boolean数组初始值全部是false
boolean[] arr = new boolean[nums.length];
Arrays.fill(arr,false);
Arrays.sort(nums);
backstracking(nums,arr);
return resList;
}
public void backstracking(int[] nums,boolean[] arr){
if(list.size() == nums.length){
resList.add(new LinkedList<>(list));
}
for (int i = 0; i < nums.length; i++) {
//这里就是对同一树层剪枝
if(i > 0 && nums[i] == nums[i - 1] && arr[i - 1] == false){
continue;
}
//这里开始对没有使用过的树枝开始进行操作
if(arr[i] == false){
arr[i] = true;//标记当前树枝已经使用过,不能重复使用
list.add(nums[i]);
backstracking(nums,arr);
arr[i] = false;//回溯,表示使用完毕
list.remove(list.size() - 1);//回溯,说明同一树层nums[i]使用过
}
}
}
}
然后开始进行剖析,什么是树层剪枝?什么是树枝剪枝?在回答这个问题之前,首先看一下下面
是不是很惊奇?为啥这里true和false都可以?这就是树层剪枝和树枝剪枝的区别所在,是不是有点迷?
对于排列问题来说,树枝和树层都是可以的,但是树层的效率更高
因为树层去重之后就会减少递归的次数。这里的话具体可以看我刷题路线的指导:代码随想录
这里讲解的很仔细。
491. 递增子序列
题目链接如下:
题目截图如下:
class Solution {
private List<Integer> path = new ArrayList<>();
private List<List<Integer>> res = new ArrayList<>();
public List<List<Integer>> findSubsequences(int[] nums) {
backtracking(nums,0);
return res;
}
private void backtracking (int[] nums, int start) {
if (path.size() > 1) {
res.add(new ArrayList<>(path));
}
int[] used = new int[201];
for (int i = start; i < nums.length; i++) {
if (!path.isEmpty() && nums[i] < path.get(path.size() - 1) ||
(used[nums[i] + 100] == 1)) continue;
used[nums[i] + 100] = 1;
path.add(nums[i]);
backtracking(nums, i + 1);
path.remove(path.size() - 1);
}
}
}