代码随想录算法训练营第二十四天|回溯算法、Leetcode77 组合

● 回溯算法

● 回溯算法介绍

回溯算法实际上是一个类似枚举的搜索过程,目的是再搜索尝试的过程中国寻找到问题的解,当发现条件不满足时就会进行回溯,尝试其他路径。最简单的一个例子就是迷宫,求解条件为找到迷宫出口,我们从入口出发,会遇到很多岔路,也就是回溯算法的不同路径,当遇到“死路”的时候,即求解条件不满足,需要回溯走其他可能通向终点的路径,直到获得从入口到出口的路径集;
同时,回溯与递归会同时出现,往往在递归的下一步就是进行回溯。

因为回溯是一种搜索尝试的算法,也就是说回溯的本质是穷举,列举出所有可能,然后在可能集中找出结果集,在该过程虽然可以进行一些剪枝操作,但回溯的穷举本质没有改变,因此回溯算法并不高效;但采用回溯算法虽然效率比较低,但对于某些问题而然使用回溯算法避无可避,因此当遇到特定问题,只能使用回溯算法进行解决。

● 回溯算法解决的问题

我们将回溯算法解决的问题进行以下分类:
(1)组合问题:N个数里面按照一定规则找出k个数的集合;
(2)切割问题:一个字符串按一定规则有几种切割方式;
(3)子集问题:一个N个数的集合中有多少符合条件的子集;
(4)排列问题:N个数按一定规则排列,有几种排列方式;
(5)棋盘问题:N皇后、数组等问题。

● 理解回溯算法

在二叉树遍历、深度优先搜索中都与递归回溯组合在一起,对于回溯算法解决的问题也都可以抽象为树形结构进行解决。因为回溯算法解决的都是在集合中递归查找自己,集合的大小构成了树的宽度,递归的深度构成了树的深度,除了子集问题,其他几类问题的解都是在叶子结点处返回结果。

● 回溯算法模板

(1)回溯函数参数和返回值
回溯算法中函数返回值一般为void,传入参数则需要依据具体实现逻辑才能确定。

void backtracking(<参数>

(2)回溯函数终止条件
和二叉树递归一样,回溯函数拥有自己的终止条件,一般为搜索到叶子结点存放答案后结束递归。

if(<终止条件>)
{
	<存放结果>;
	return;
}

(3)回溯算法的遍历过程
回溯算法一般实在集合中递归搜索,集合大小构成树的宽度,递归的深度构成树的深度,因此回溯算法的遍历过程一般为:

for(<选择:本层集合中元素>)
{
	<处理节点>;
	backtracking(<路径><选择列表>);
	<回溯,撤销处理结果>;
}

对于回溯算法的遍历过程,for循环因为是遍历一层集合中的元素,因此是横向遍历,而backtracking是回撤某一条路径上的操作,是纵向遍历,两者结合能够将所有可能结果遍历完。

● Leetcode77 组合

题目链接:Leetcode77 组合
视频讲解:代码随想录|组合
题目描述:给定两个整数 n 和 k,返回范围 [1, n] 中所有可能的 k 个数的组合。
你可以按 任何顺序 返回答案。

示例 1:
输入:n = 4, k = 2
输出:
[
[2,4],
[3,4],
[2,3],
[1,2],
[1,3],
[1,4],
]
示例 2:
输入:n = 1, k = 1
输出:[[1]]

提示:
· 1 <= n <= 20
· 1 <= k <= n

● 解题思路

在回溯算法理论中提到,每一个回溯算法的问题都可以抽象为树形结构解决。
以示例一为例,回溯深度为2,单层元素为4:
树形结构
按照回溯三部曲:
(1)确定回溯函数参数和返回值:
代码定义了全局变量result保存结果集,path记录当前路径,因此返回值为void,传入参数为整数n和k,同时下一次回溯函数中的元素为去除前一个元素之后的剩余全部,因此我们需要从i + 1个元素开始寻找结果;
(2)确定回溯函数终止条件:
对于抽象的树状结构,终止条件为到叶子结点时将该条路径的元素存入结果集中,对应到题目也就是当path中的元素大小与k相等时,存入结果;
(3)确定回溯算法的遍历过程:
我们用startIndex记录每次回溯算法开始的位置,每向后遍历一个元素,就向path中加入,在回溯结束之后需要将path的元素弹出,进行回溯操作,也就是从其他回退一步继续走其他路径。

同时,对于本题可以进行剪枝操作;所谓剪枝操作,就是将不满足条件的路径wipe off掉,如示例一中因为当startIndex遍历到4时,不满足path.size() == 2,因此这条路径就被剪枝:
剪枝
剪枝操作被体现在for循环中,我们将i的遍历范围进行修改即可剪枝;
不难发现,当path内插入元素,则还能再向path中插入k - path.size()个元素,对于n个数而言,当i遍历到n - (k - path.size()) + 1时,之后的元素就无法满足返回条件,即被剪枝。

● 代码实现

class Solution {
private:
    vector<vector<int>> result;
    vector<int> path;
    void backtracking(int n, int k, int startIndex)
    {
        //终止条件
        if(path.size() == k)
        {
            result.push_back(path);
            return;
        }

        for(int i = startIndex; i <= n; i++)
        {
            path.push_back(i);
            backtracking(n, k, i + 1);
            path.pop_back();
        }
    }
public:
    vector<vector<int>> combine(int n, int k) {
        backtracking(n, k, 1);
        return result;
    }
};

剪枝代码:

class Solution {
private:
    vector<vector<int>> result;
    vector<int> path;
    void backtracking(int n, int k, int startIndex)
    {
        if(path.size() == k)
        {
            result.push_back(path);
            return;
        }

        for(int i = startIndex; i <= (n - (k - path.size()) + 1); i++)
        {
            path.push_back(i);
            backtracking(n, k, i + 1);
            path.pop_back();
        }
    }
public:
    vector<vector<int>> combine(int n, int k) {
        backtracking(n, k, 1);
        return result;
    }
};
  • 23
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值