刷题笔记(二十一)--回溯算法:分割丶子集丶全排列问题

系列文章目录

刷题笔记(一)–数组类型:二分法
刷题笔记(二)–数组类型:双指针法
刷题笔记(三)–数组类型:滑动窗口
刷题笔记(四)–数组类型:模拟
刷题笔记(五)–链表类型:基础题目以及操作
刷题笔记(六)–哈希表:基础题目和思想
刷题笔记(七)–字符串:经典题目
刷题笔记(八)–双指针:两数之和以及延伸
刷题笔记(九)–字符串:KMP算法
刷题笔记(十)–栈和队列:基础题目
刷题笔记(十一)–栈和队列:Top-K问题
刷题笔记(十二)–复习:排序算法
刷题笔记(十三)–二叉树:前中后序遍历(复习)
刷题笔记(十四)–二叉树:层序遍历和DFS,BFS
刷题笔记(十五)–二叉树:属性相关题目
刷题笔记(十六)–二叉树:修改与构造
刷题笔记(十七)–二叉搜索树:关于属性问题
刷题笔记(十八)–二叉树:公共祖先问题
刷题笔记(十九)–二叉树:二叉搜索树的修改与构造
刷题笔记(二十)–回溯算法:组合问题

前言

这一篇博客是讲解一下回溯算法的其他题型。

分割问题:一个字符串按一定规律有几种切割方式
子集问题:一个N个数的集合有多少个符合条件的子集
全排列问题:N个数按照一定的条件排列,有几种排列方式

题录

131. 分割回文串

题目链接如下:

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 地址

题目链接如下:

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. 子集

题目链接如下:

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

题目链接如下:

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. 全排列

题目链接如下:

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

题目链接如下:

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. 递增子序列

题目链接如下:

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);
        }
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值