穷举vs暴搜vs深搜vs回溯vs剪枝

目录

什么是回溯算法

回溯算法的模板

回溯算法的应用

组合问题

排列问题

子集问题

一、全排列

1.题目链接:46. 全排列

2.题目描述:

3.解法

🍒算法思路:

🍒算法代码:

二、子集 

1.题目链接:78. 子集

2.题目描述:

3.解法

🍒算法思路:

🍒算法代码:


什么是回溯算法

  1. 回溯算法是⼀种经典的递归算法,通常用于解决组合问题、排列问题和搜索问题等。
  2. 回溯算法的基本思想:从⼀个初始状态开始,按照⼀定的规则向前搜索,当搜索到某个状态无法前进时,回退到前⼀个状态,再按照其他的规则搜索。回溯算法在搜索过程中维护⼀个状态树,通过遍历状态树来实现对所有可能解的搜索。
  3. 回溯算法的核心思想:“试错”,即在搜索过程中不断地做出选择,如果选择正确,则继续向前搜索;否则,回退到上⼀个状态,重新做出选择。回溯算法通常⽤于解决具有多个解,且每个解都需要搜索才能找到的问题。

回溯算法的模板

void backtrack(vector<int>& path, vector<int>& choice, ...) {
	// 满⾜结束条件
	if (/* 满⾜结束条件 */) {
		// 将路径添加到结果集中
		res.push_back(path);
		return;
	}
	// 遍历所有选择
	for (int i = 0; i < choices.size(); i++) {
		// 做出选择
		path.push_back(choices[i]);
		// 做出当前选择后继续搜索
		backtrack(path, choices);
		// 撤销选择
		path.pop_back();
	}
}
  • 其中, path 表示当前已经做出的选择, choices 表示当前可以做的选择。在回溯算法中,我们需要做出选择,然后递归地调用回溯函数。如果满足结束条件,则将当前路径添加到结果集中;否则,我们需要撤销选择(恢复现场),回到上⼀个状态,然后继续搜索其他的选择。
  • 回溯算法的时间复杂度通常较高,因为它需要遍历所有可能的解。但是,回溯算法的空间复杂度较低,因为它只需要维护⼀个状态树。在实际应用中,回溯算法通常需要通过剪枝等方法进行优化,以减少搜索的次数,从而提高算法的效率。

回溯算法的应用

组合问题

组合问题是指从给定的⼀组数(不重复)中选取出所有可能的 k 个数的组合。例如,给定数集 [1,2,3],要求选取 k=2 个数的所有组合。

结果为:

[1,2]
[1,3]
[2,3]

排列问题

排列问题是指从给定的⼀组数(不重复)中选取出所有可能的 k 个数的排列。例如,给定数集 [1,2,3],要求选取 k=2 个数的所有排列。

结果为:

[1,2]
[2,1]
[1,3]
[3,1]
[2,3]
[3,2]

子集问题

子集问题是指从给定的⼀组数中选取出所有可能的⼦集,其中每个⼦集中的元素可以按照任意顺序排列。例如,给定数集 [1,2,3],要求选取所有可能的⼦集。

结果为:

[]
[1]
[2]
[3]
[1,2]
[1,3]
[2,3]
[1,2,3]

总结:

  • 回溯算法是一种非常重要的算法,可以解决许多组合问题、排列问题和搜索问题等。回溯算法的核心思想是搜索状态树,通过遍历状态树来实现对所有可能解的搜索。回溯算法的模板非常简单,但是实现起来需要注意一些细节,比如如何做出选择、如何撤销选择等。

一、全排列

1.题目链接:46. 全排列

2.题目描述:

给定一个不含重复数字的数组 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]]

提示:

  • 1 <= nums.length <= 6
  • -10 <= nums[i] <= 10
  • nums 中的所有整数 互不相同

3.解法

🍒算法思路:

  • 典型的回溯题目,我们需要在每⼀个位置上考虑所有的可能情况并且不能出现重复。通过深度优先搜索的方式,不断地枚举每个数在当前位置的可能性,并回溯到上⼀个状态,直到枚举完所有可能性,得到正确的结果。
  • 每个数是否可以放入当前位置,只需要判断这个数在之前是否出现即可。具体地,在这道题目中,我们可以通过⼀个递归函数 backtrack 和标记数组 visited 来实现全排列。

递归函数设计:void backtrack(vector<vector<int>>& res, vector<int>& nums, vector<bool>& visited, vector<int>& ans, int step, int len)

  1. 参数:step(当前需要填⼊的位置),len(数组⻓度);
  2. 返回值:无;
  3. 函数作用:查找所有合理的排列并存储在答案列表中。

递归流程如下:

1. ⾸先定义⼀个⼆维数组 res 用来存放所有可能的排列,⼀个⼀维数组 ans 用来存放每个状态的排列,⼀个⼀维数组 visited 标记元素,然后从第⼀个位置开始进行递归;

2. 在每个递归的状态中,我们维护⼀个步数 step,表⽰当前已经处理了几个数字;

3. 递归结束条件:当 step 等于 nums 数组的长度时,说明我们已经处理完了所有数字,将当前数组存入结果中;

4. 在每个递归状态中,枚举所有下标 i,若这个下标未被标记,则使用 nums 数组中当前下标的元素:

  • 将 visited[i] 标记为 1;
  • ans 数组中第 step 个元素被 nums[i] 覆盖;
  • 对第 step+1 个位置进⾏递归;
  • 将 visited[i] 重新赋值为 0,表⽰回溯;

5. 最后,返回 res。

  • 特别地,我们可以不使⽤标记数组,直接遍历 step 之后的元素(未被使⽤),然后将其与需要递归的位置进行交换即可。

🍒算法代码:

class Solution 
{
    vector<vector<int>> ret;
    vector<int> path;
    bool check[7];
public:
    vector<vector<int>> permute(vector<int>& nums) 
    {
        dfs(nums);
        return ret;
    }
    void dfs(vector<int>& nums)
    {
        if(path.size() == nums.size())
        {
            ret.push_back(path);
            return;
        }
        for(int i = 0; i < nums.size(); i++)
        {
            if(check[i] == false)
            {
                path.push_back(nums[i]);
                check[i] = true;
                dfs(nums);
                // 回溯-恢复现场
                path.pop_back();
                check[i] = false;
            }
        }
    }
};

二、子集 

1.题目链接:78. 子集

2.题目描述:

给你一个整数数组 nums ,数组中的元素 互不相同 。返回该数组所有可能的子集(幂集)。

解集 不能 包含重复的子集。你可以按 任意顺序 返回解集。

示例 1:

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

示例 2:

输入:nums = [0]
输出:[[],[0]]

提示:

  • 1 <= nums.length <= 10
  • -10 <= nums[i] <= 10
  • nums 中的所有元素 互不相同

3.解法

🍒算法思路:

  • 为了获得 nums 数组的所有⼦集,我们需要对数组中的每个元素进⾏选择或不选择的操作,即 nums 数组⼀定存在 2^(数组长度) 个子集。对于查找子集,具体可以定义⼀个数组,来记录当前的状态,并对其进行递归。
  • 对于每个元素有两种选择:1. 不进行任何操作;2. 将其添加至当前状态的集合。在递归时我们需要保证递归结束时当前的状态与进行递归操作前的状态不变,而当我们在选择进行步骤 2 进行递归时,当前状态会发生变化,因此我们需要在递归结束时撤回添加操作,即进行回溯。

递归函数设计:void dfs(vector<vector<int>>& res, vector<int>& ans, vector<int>& nums, int step)

  1. 参数:step(当前需要处理的元素下标);
  2. 返回值:无;
  3. 函数作用:查找集合的所有子集并存储在答案列表中。

递归流程如下:

1. 递归结束条件:如果当前需要处理的元素下标越界,则记录当前状态并直接返回;

2. 在递归过程中,对于每个元素,我们有两种选择:

  • 不选择当前元素,直接递归到下⼀个元素;
  • 选择当前元素,将其添加到数组末尾后递归到下⼀个元素,然后在递归结束时撤回添加操作;

3. 所有符合条件的状态都被记录下来,返回即可。

 

🍒算法代码:

解法一:

class Solution 
{
    vector<int> path;
    vector<vector<int>> ret;
public:
    vector<vector<int>> subsets(vector<int>& nums) 
    {
        // 解法一
        dfs(nums, 0);
        return ret;
    }

    void dfs(vector<int>& nums, int i)
    {
        if(i == nums.size())
        {
            ret.push_back(path);
            return;
        }
        // 选
        path.push_back(nums[i]);
        dfs(nums, i + 1);
        path.pop_back();
        // 不选
        dfs(nums, i + 1);// 恢复现场
    }
};

解法二: 

class Solution 
{
    vector<int> path;
    vector<vector<int>> ret;
public:
    vector<vector<int>> subsets(vector<int>& nums) 
    {
        // 解法二
        dfs(nums, 0);
        return ret;
    }

    void dfs(vector<int>& nums, int pos)
    {
        ret.push_back(path);
        for(int i = pos; i < nums.size(); i++)
        {
            path.push_back(nums[i]);
            dfs(nums, i + 1);
            path.pop_back();// 恢复现场
        }
    }
};
  • 27
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

南风与鱼

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值