编程语言
首页 > 编程语言> > [图解算法]1,回溯算法,简单易懂的让你理解回溯算法

[图解算法]1,回溯算法,简单易懂的让你理解回溯算法

作者:互联网

今天做了几道关于回溯算法的题,看见几张图很不错和大家分享一下,看了之后对回溯有了新的认识

回溯算法的基本思想: 回溯算法的基本思想是:从一条路往前走,能进则进,不能进则退回来,换一条路再试。重点就是进行状态回退

话不多说直接上题

leetcode#78.子集

[题目描述]:
给你一个整数数组 nums ,数组中的元素 互不相同 。返回该数组所有可能的子集(幂集)。
解集 不能 包含重复的子集。你可以按 任意顺序 返回解集。

[示例1]:
输入:nums = [1,2,3]
输出:[[],[1],[2],[1,2],[3],[1,3],[2,3],[1,2,3]]

[示例2]:
输入:nums = [0]
输出:[[],[0]]

这个就是经典的回溯问题,先上一张图
在这里插入图片描述
画出递归树后可以看出:
先看1分支,选1之后还可以选择2,3,而这个位置就是一个回溯结点,如果选择2,遍历之后我们还要回来看3的所有情况,2和3都遍历完之后,我们还要回溯到根节点,查看如果第一个选的是2的情况,如果第一个是2那下面还可以选3,选择结束后回到根节点去选择3的情况,而这个回头就是回溯的核心思想

在这里插入图片描述
按照这个顺序进行遍历,把路径上的所有结果都添加到集合里,子集就好了,还有一个关键点,如何让它实现回溯,把路径上的东西添加进去,这个就需要有一个路径集合进行路径的保存和回溯

路径1
在这里插入图片描述
回溯到上一个分叉,选择另一种情况

在这里插入图片描述
该树都已经遍历完毕切换路径
在这里插入图片描述
这样路径往复就遍历完成了

我们上代码 重点看代码中的注释

class Solution {
	//声明一个集合去保存我们的子集
     List<List<Integer>> l=new ArrayList<>();
    
    //这个是题目的函数
    public List<List<Integer>> subsets(int[] nums) {
    
    //定义路径集合用来保存我们的路径
     List<Integer> path=new ArrayList<>();
     //调用我们定义的函数
     //0代表我们的起始位置,下面有对起始位置的解释
        dfs(nums,path,0);
        
        return l;
    }

 void dfs(int[] nums,List<Integer> path,int start){
 		//把当前路径的集合添加到我们的子集集合当中
       l.add(new ArrayList<>(path));
       
       //要注意i并不是从0开始,因为每次从0开始遍历会导致元素的重复
       //例如图中第2枝子树选择2之后就不要考虑去选择1了,因为1,2和2,1重复
       //start之前的数字都是前面元素已经选择过的情况,再从0开始会导致重复
        for (int i = start; i <nums.length; i++) {
        	//把遍历得数字添加到我们的路径当中
            path.add(nums[i]);
			//别忘了把传进去的值+1
            dfs(nums,path,i+1);
            
       //把上代码一行dfs对path的操作清除,就是把上一行d'f's刚添加的删除
            //这个就是回溯
            path.remove(path.size()-1);
        }
    }
}dfs

我们开始下一题

leetcode#46.全排列

[题目描述]
给定一个不含重复数字的数组 nums ,返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。

示例 1:
输入:nums = [1,2,3]
输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]

示例 2
输入:nums = [0,1]
输出:[[0,1],[1,0]]

示例 3:
输入:nums = [1]
输出:[[1]]

先上图
图片来自leetcode

这个是全排列问题,排列组合问题不存在和上面一样,前面的的树已经选择了这个数字下一个树就不能选择了,只要选择的顺序不同就可以,但是我们选择添加的结果已经不是路径了,而是路径最后一个值,当路径的长度(也就是存储的数字个数)等于我们的数组长度就进行存储

这个题的重点是对已经选择数字的判定,在本子分支中以及选择的数选择之后要判断这个数在本路径之前是否已经选择过,所有我们要用一个布尔数组进行状态存储,用空间换时间

class Solution {
	//定义集合去存储结果
    List<List<Integer>> l=new ArrayList<>();
    
    public List<List<Integer>> permute(int[] nums) {
    //定义boolean数组进行是否选择进行判断
        boolean[] status=new boolean[nums.length];
       //path路径
        List<Integer> path=new ArrayList<>();
       //调用我们的函数
        dsf(nums,nums.length,path,status);

        return l;
    }
    void dsf(int[] nums,int length,List<Integer> path,boolean[] status){
   //如果length等于0意思就是我们的数字已经使用完了,这时候就可以把本次的结果存储一下
            if(length==0){
                l.add(new ArrayList<>(path));
                return;
            }
        for (int i = 0; i < nums.length; i++) {
        //进行状态判断如果为false说明还没有使用该数字继续运行,如果为true说明已经选择过了开始下一次循环
               if(status[i])
                continue;
                //选择后就把该数字代表的boolean设为true说明已经选择过了
                status[i]=true;
                //对路径上的数字进行添加
                path.add(nums[i]);
                //选择数字之后别忘了把我们的length-1进行下一次调用
                dsf(nums,length-1,path,status);
                //回溯,把刚刚添加的删除,把刚刚选择过的设为false
                //回溯就是把状态设置回以前的状态
                path.remove(path.size()-1);
                status[i]=false;
        }
    }
}

我们开始下一题,回溯+剪枝

leetcode#22.括号生成

[题目描述]:
数字 n 代表生成括号的对数,请你设计一个函数,用于能够生成所有可能的并且 有效的 括号组合。

[示例1]:
输入:n = 3
输出:["((()))","(()())","(())()","()(())","()()()"]

[示例2]:
输入:n = 1
输出:["()"]

这个就需要配合剪枝的思想,我们生成的括号是一个一个往后加的
如果出现 “( )” 这种情况我们只能选择 “(” ,因为如果选择 了")"我们匹配的时候
出现这种情况 “( ) )” 后面就没法匹配,累计的时候也就是左边的括号数量大于右括号我们才能进行下面的操作

图片来自leetcode

重点就是对树进行剪枝操作,判断是否执行该子树
左边的括号数量大于右括号我们才能进行下面的操作 ,否则停止也就是剪枝

class Solution {
    public List<String> generateParenthesis(int n) {
    	//结果集
         List<String> l=new ArrayList<>();
        if(n==0){
            return l;
        }
        //前面字符串代表路径,n和n代表,我们当前的左右剩余的括号个数
        dfs("",n,n,l);

        return l;
    }
    private void dfs(String curStr,int left,int right,List<String> l){
    	//如果左右括号剩余都等于0则进行存储
        if(left==0&&right==0){
            l.add(curStr);
            return;
        }
        //剪枝操作,如果左边括号剩余大于右边,也就是右边的括号使用大于等于左边,就不再执行下面操作
        if(left>right){
            return;
        }
        //如果左边还有剩余进行下面操作
        if(left>0){
        //添加后别忘了把响应的数字减一
            dfs(curStr+"(",left-1,right,l);
        }
         //如果还有剩余进行下面操作
        if (right>0){
         //添加后别忘了把响应的数字减一
            dfs(curStr+")",left,right-1,l);
        }
    }
}

回溯最重要的就是找到回溯节点和停止条件,第一步最重要的是先画图
相应的思路也能清晰

[创作不易] 求点赞,求关注
你的关注和支持是我最大的动力,☆*: .。. o(≧▽≦)o .。.:☆,

标签:回溯,nums,int,路径,List,算法,path,易懂
来源: https://blog.csdn.net/qq_52314655/article/details/122506533