回溯算法(1)模板+组合问题+剪枝

回溯?

回溯 重点在于回头。回溯算法本身改变不了暴力搜索的本质。简单一点说:从一条路往前走,能进则进,不能进则退回来,换一条路再试。
回溯算法主要应用于:
组合问题:给定两个整数 n 和 k,返回范围 [1, n] 中所有可能的 k 个数的组合。(不考虑顺序,即{1,2} {2,1}认为同一种可能)
分割问题:给定一个字符串,如何分割回文字符串
排列问题:组合问题的拓展,即考虑顺序
棋盘问题:n皇后(以八皇后最为经典)

以组合问题为例,n=4,k=2为例。
按照人类的思想,我们首先会将1取出,然后找寻2;然后抛弃2,找寻3;抛弃3,找寻4;再抛弃1,以2为第一个元素······
抛弃的过程即为回头,即回溯。

处理回溯我们更不难发现一个规律,还是以上文提及的组合问题为例,我们需要横向的遍历(即第一个元素从1,2,3,4挨个寻找)还需要纵向的遍历(添加第二个元素,···直至第k个元素)

每一层的纵向遍历都需要经历横向搜索。
n代表横向长度
k代表纵向深度

在这里插入图片描述

又这个规律 我们发现需要通过构造循环嵌套递归才能方便解决这个问题。或许会有同学说,k=2时候两层for循环就可以解决问题?
但如果k是个未知数,那如何构造循环呢?

所以我们可以写出回溯问题的通用模板

void BackTracking(参数){
	if(判断条件){
		递归终止操作;
		return ;
	}
	for(....){
		...push_back();//记录这一层遍历的结果
		BackTracking(参数);//递归前往下一层
		...popback();//回溯
	}
	return;

}

具体例子的分析

以提及的组合问题为例:
给定两个整数 n 和 k,返回范围 [1, n] 中所有可能的 k 个数的组合。你可以按 任何顺序 返回答案。

题目链接在这里插入图片描述
解法:

class Solution {
public:
    vector<vector<int>> result;//记录最终结果的二维数组
    vector<int> temp;//记录每一种可能的temp数组
    void traceback(int n,int k,int start)//start参数设计用来保证不走回头路,即1在第一层选中,则后面的层中不会再遍历1......
    {
        if(k==temp.size())//中止条件为k个数字取满
        {
            result.push_back(temp);//将temp数组的结果写入result
            return;//非常重要,递归一定要有中止,不然就栈溢出了
        }
        for(int i=start;i<=n;i++)//遍历这一层的所有数字,从start位置开始
        {
            temp.push_back(i);//将此层的结果假如temp数组
            traceback(n,k,i+1);//递归前往下一层
            temp.pop_back();//回溯,即弹出这一层选中的,无需担心再上一行的递归函数中我们已经记录了结果,所以回溯是必要的。
        }
        return;
    }
    vector<vector<int>> combine(int n, int k) {
        temp.clear();
        result.clear();
        traceback(n,k,1);
        return result;
    }
};

另一道类似的题目:
给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。答案可以按 任意顺序 返回。给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。

在这里插入图片描述

题目链接

class Solution {
public:
    vector<string> result;
    vector<string> source={"","","abc","def","ghi","jkl","mno","pqrs","tuv","wxyz"};//起到类似字典的功能,建立索引    
    string s;
    void TraceBack(string digits,int index)//index与上文start类似,用来控制避免走回头路,即用来如字符串“123”通过index,可以确保下一层递归函数可以传入“23”
    {
        if(digits.size()==index)//递归的终止条件,即查完digits
        {
            result.push_back(s);
            return;//很重要!很重要!很重要!
        }
        int digit=digits[index]-'0';
        string src=source[digit];//确立当前digits中这个数字所对应的所有字母,即当前层所需要遍历的所有元素
        for(size_t i=0;i<src.size();i++)//遍历当前层
        {
            s.push_back(src[i]);
            TraceBack(digits,index+1);//递归,去往下一层
            s.pop_back();
        }
    }
    vector<string> letterCombinations(string digits) {
       s.clear();
       if(digits.size()==0){
           return result;
       }//很重要,若字符串为空,则会返回异常结果“”
       TraceBack(digits,0);
       return result;
    }
};

回溯的剪枝操作

我们可以更深层次的思考一个问题,还是以组合问题为例,假如n=4,k=4的时候是不是效率太低了。作为人我们当然知道 只有{1 2 3 4 }这一种可能,但是电脑不知道,他依然会 走 {2 3 4} {3 4} {4}等等没有必要的死路,假如n和k再放大 那显然造成了不必要的时间和栈 浪费,而避免这些死胡同,即将这些枝条减去,可以叫做剪枝!
在这里插入图片描述
仔细思考发现,我们可以再for循环就加以控制,假如即将遍历的循环中剩余的所有元素个数加上当前temp数组中存在的个数都无法满足k,那么我们完全不需要走这条路啊

于是乎我们可以对代码进行修改

class Solution {
public:
    vector<vector<int>> result;
    vector<int> temp;
    void traceback(int n,int k,int start){
        if(k==temp.size()){
            result.push_back(temp);
            return;
        }
        for(int i=start;i<=n-(k-temp.size())+1;i++)//此处修改,k-temp.size()即将k填满至少还要多少个数,n-(k-temp.size())+1为这个循环最多可以探索的位置,+1是因为算上了start位置,在n-(k-temp.size())+1再也无法满足k个的要求
        {
            temp.push_back(i);
            traceback(n,k,i+1);
            temp.pop_back();
        }
        return;
    }
    vector<vector<int>> combine(int n, int k) {
        temp.clear();
        result.clear();
        traceback(n,k,1);
        return result;
    }
};

以上即剪枝的优化

  • 6
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 6
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值