回溯算法及解题模板


1. 前言

    回溯算法的基础是递归,如果你对递归还不熟悉的话,建议先去看看递归的概念,做一些递归的练习题,也可以看我之前写的递归的文章:递归算法详解


2. 什么是回溯算法?

    回溯算法其实是一种穷举的搜索方法,利用回溯解决问题就是在穷举所有的可能,然后找到我们想要的答案。使用回溯算法时需要考虑如下的三个问题:
    (1) 可选列表:所有可作出的选择;
    (2) 已选列表:也就是之前已经做出的选择;
    (3) 结束条件:当已选列表满足题目条件,可以结束穷举;


3. 回溯算法的框架

vector<elemType> res = {} 
void backtrace(可选列表, 已选列表){
    if(到达结束条件){
        res.push_back(已选列表);
        return;
    }

    for(选择 : 选择列表){
    	排除无效的选择(剪枝)
        做选择;
        backtrace(可选列表, 已选列表);
        撤销选择;
    }
}

[注]:上述框架中剪枝的意思是对已经访问过或者不符合题意的元素进行跳过


4. 针对不同题型的解题模板

1)所给数组中的元素互不相同,结果包含所有元素

这类题型的题有:
    LeetCode 46. 全排列
    LeetCode 51. N皇后
    LeetCode 52. N皇后II
    LeetCode 37. 解数独
解题模板:

    vector<vector<int>> res; //存放结果
    vector<int> visited; //用来标记已经填过的数

    void backtrack(vector<int>& nums, vector<int> track){
        if(track.size() == nums.size()){ //结束条件
            res.push_back(track);
            return;
        }
        for(int i = 0; i < nums.size(); i++){
            if(visited[i]) continue; //剪枝,访问过的元素就跳过,visited[i]也可以是一个返回值为bool值的函数来代替
            track.push_back(nums[i]); //做选择
            visited[i] = 1; //标记为已访问过
            backtrack(nums, track);
            track.pop_back(); //撤销选择
            visited[i] = 0; //标记为未访问过
        }
    }
    
    vector<vector<int>> problem(vector<int>& nums) {
        visited.resize(nums.size(), 0); //全部值设置为0,表示都未访问过
        vector<int> track; //初始化路径为空,即track=[]
        backtrack(nums, track);
        return res;
    }

2)所给数组中的元素包含重复元素,结果包含所有元素

由于题目所给数组中会包含重复的元素,因此如果直接按照框架进行回溯结果中会包含重复的答案,举个例子,假定给定的数组为 n u m s = [ 1 , 3 , 3 , 4 ] nums = [1, 3, 3, 4] nums=[1,3,3,4],则遍历的结果中就会包含两个 [ 3 , 3 , 1 , 4 ] [3, 3, 1, 4] [3,3,1,4],其中第一个 [ 3 , 3 , 1 , 4 ] [3, 3, 1, 4] [3,3,1,4]中的首元素3是原数组位置2上的3,第二个 [ 3 , 3 , 1 , 4 ] [3, 3, 1, 4] [3,3,1,4]中的首元素3是原数组位置3上的3,这样穷举结果中就会包含重复的结果,我们要避免这样的情况首先需要先对数组排序,然后对重复数字第2次,第3次,…,第n次的出现进行剪枝,下面是代码模板:

    vector<vector<int>> res; //存放结果
    vector<int> visited; //用来标记已经填过的数

    void backtrack(vector<int>& nums, vector<int> track){
        if(track.size() == nums.size()){ //结束条件
            res.push_back(track);
            return;
        }
        for(int i = 0; i < nums.size(); i++){
        	//避免重复的核心是要在判断条件中加上!visited[i-1], 因为按照从左到右的访问顺序是nums[i-1]先访问,
        	//再到nums[i],而当这个遍历完成后就会撤销选择,也就是visited[i-1]恢复为0,然后先访问nums[i]再访
        	//问nums[i-1],此时通过visited[i-1]=0来剪枝就能避免重复的结果
            if((i > 0 && nums[i] == nums[i-1] && !visited[i-1]) || visited[i]) continue; 
            track.push_back(nums[i]); //做选择
            visited[i] = 1; //标记为已访问过
            backtrack(nums, track);
            track.pop_back(); //撤销选择
            visited[i] = 0; //标记为未访问过
        }
    }
    
    vector<vector<int>> problem(vector<int>& nums) {
        sort(nums.begin(), nums.end()); //排序数组
        visited.resize(nums.size(), 0); //全部值设置为0,表示都未访问过
        vector<int> track; //初始化路径为空,即track=[]
        backtrack(nums, track);
        return res;
    }

这类题型的题目有:
    LeetCode 47. 全排列II

3)所给数组中的元素各不相同,结果为子集,同时避免子集重复

子集重复的意思是说:当我们有数组 n u m s = [ 1 , 2 , 3 ] nums = [1,2,3] nums=[1,2,3]时,按照框架去穷举的话结果中同时包含子集 [ 1 , 3 ] [1,3] [1,3]和子集 [ 3 , 1 ] [3,1] [3,1],也会同时包含子集[1,2,3]、[1,3,2]、[2,1,3]、[2,3,1]、[3,1,2]、[3,2,1],其实上述子集中的元素是相同的,只是顺序不同,而有的题目不允许出现这样的结果,因此要避免子集重复。避免子集重复可以通过在穷举过程中避免回头实现,具体来说就是可以按顺序对每个位置进行穷举(选择当前位置或者不选择当前位置),这样结果中元素的前后位置就是固定:

    vector<vector<int>> res; //存放结果
    //不需要辅助的visited数组,因为这里是按从前到后遍历的,也就是说不会访问之前已经访问过的元素

    void backtrack(vector<int>& nums, vector<int> track,int cur){//这里多了一个指示当前位置的参数cur
        if(cur == nums.size()){ //结束条件
            res.push_back(track);
            return;
        }
        //选择当前位置
        track.push_back(nums[i]);
        backtrack(nums, track, cur+1);
        track.pop_back();
        //不选择当前位置
        backtrack(nums,track, cur+1);
        
    }

    vector<vector<int>> problem(vector<int>& nums) {
        vector<int> track; //初始化路径为空,即track=[]
        backtrack(nums, track, 0);
        return res;
    }

这类题型的题目有:
    LeetCode 39. 组合总和
    LeetCode 77. 组合
    LeetCode 78. 子集
    LeetCode 216. 组合总和III

4)所给数组中包含重复元素,结果为子集,同时避免结果重复和子集重复

这一类问题相当于2和3的结合体,因此解题首先需要对数组进行排序,然后要按位置穷举,同时做好剪枝:

    vector<vector<int>> res;
    vector<int> visited;

    void backtrack(vector<int>& nums, vector<int> &track, int cur){
        if(cur == nums.size()){
            if(结束条件)
                res.push_back(track);
            return;
        }

        if(!(cur > 0 && nums[cur] == nums[cur-1] && !visited[cur-1])){//如果以前穷举过相同数字,则会跳过当前元素的选择
            track.push_back(nums[cur]);
            visited[cur] = 1;
            backtrack(nums, track, cur+1);
            track.pop_back(); //撤销选择
            visited[cur] = 0;
        }

        backtrack(nums, track, cur+1);
    }

这类题型的题目有:
    LeetCode 40. 组合总和II
    LeetCode 90. 子集II


  • 1
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
以下是符号三角形回溯算法的C语言代码,附带输入输出和解题思路。 ```c #include <stdio.h> #define MAX_N 15 int N; // 三角形边长 char triangle[MAX_N][MAX_N + 1]; // 存储三角形 // 求解符号三角形最大值 int dfs(int i, int j, int sum) { if (i == N) { // 到达三角形底部,返回和 return sum; } // 往下走,选择左边或右边 int left = dfs(i + 1, j, sum + triangle[i][j]); int right = dfs(i + 1, j + 1, sum + triangle[i][j]); // 返回左右两边的最大值 return left > right ? left : right; } int main() { // 读入三角形 scanf("%d", &N); for (int i = 0; i < N; ++i) { scanf("%s", triangle[i]); } // 求解符号三角形最大值并输出 printf("%d\n", dfs(0, 0, 0)); return 0; } ``` 输入格式: 第一行包含一个整数N,表示三角形的边长。 接下来N行,每行包含一个长度为1到N+1的字符串,表示三角形。 输出格式: 一个整数,表示符号三角形的最大值。 解题思路: 符号三角形是一个类似于数塔的问题,只不过塔上的数字变成了符号。我们可以用类似于数塔的递归算法来求解。 假设我们正在处理第i行第j列的符号,当前已经得到的和为sum。我们可以往下走,选择左边或右边,分别得到左边的最大值left和右边的最大值right。那么当前符号的最大值就是sum加上左右两边的最大值的较大值。 最后,当我们到达三角形的底部时,直接返回sum即可。 完整代码附带了输入输出,可以直接运行。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值