代码随想录:回溯算法篇

回溯

77.组合

可以将回溯问题,想象成树结构进行求解,其中for循环代表横向遍历,递归的过程代表纵向遍历。每层 节点的个数(图中矩形的个数) 代表每层循环的次数。
回溯求解图
代码如下:

class Solution {
private:
    vector<vector<int>> res;
    vector<int> row;
    void backtracking(int n, int k, int startindex){
    //回溯函数终止条件
        if(row.size()==k){
            res.push_back(row);
            return;
        }
    //单层搜索过程
        for(int i=startindex; i<=n; i++){
            row.push_back(i);
            backtracking(n, k, i+1);
            row.pop_back();
        }
        return;
        /*剪枝优化
        for(int i = startIndex; i <= n - (k - path.size()) + 1; i++){
            row.push_back(i);
            backtracking(n, k, i+1);
            row.pop_back();
        }
        return;
        */

    }
public:
    vector<vector<int>> combine(int n, int k) {
        res.clear();
        row.clear();//可以不写
        backtracking(n,k,1);
        return res;
    }
};

组合优化:

在这里插入图片描述
可以通过剪枝操作,进行优化,回溯算法其实就是一个穷举的过程,并没有节省多少算法的运行时间,但可以通过剪枝操作,对回溯进行优化。剪枝操作实际上就是找到递归中每层循环的起始位置,若起始位置之后的元素已经不满足我们需要的元素个数,就没有搜索的必要了。
在这里插入图片描述

216.组合总和3

剪枝代码如下:

//剪枝
class Solution {
private:
    vector<vector<int>> res;
    vector<int> row;
    void backtracking(int k, int targetsum, int sum, int start){
        //剪枝
        if(sum > targetsum){
            return;
        }

        if(row.size()==k){
            if(sum ==  targetsum){
                res.push_back(row);
            }
            return;
        }

        for(int i=start; i<=9-(k-row.size())+1; i++)//剪枝
        {
            row.push_back(i);
            sum+=i;
            backtracking(k,targetsum,sum,i+1);
            sum-=i;
            row.pop_back();
        }

        return;
    }

public:
//找出所有相加之和为 n 的 k 个数的组合,只使用数字1-9,每个数字用一次
    vector<vector<int>> combinationSum3(int k, int n) {
        res.clear();
        row.clear();
        backtracking(k, n, 0, 1);
        return res;
    }
};

自解代码:

class Solution {
private:
    vector<vector<int>> res;
    vector<int> row;
    void backtracking(int k, int n, int start){
        if(row.size()==k){
            int sum=0;
            for(int i=0; i<k; i++){
                sum+=row[i];
            }
            if(sum == n){
                res.push_back(row);
            }
            return;
        }

        for(int i=start; i<=9; i++){
            row.push_back(i);
            backtracking(k,n,i+1);
            row.pop_back();
        }

        return;
    }

public:
//找出所有相加之和为 n 的 k 个数的组合,只使用数字1-9,每个数字用一次
    vector<vector<int>> combinationSum3(int k, int n) {
        res.clear();
        row.clear();
        backtracking(k, n, 1);
        return res;
    }
};

17.电话号码的字母组合

代码如下:

class Solution {
private:
    string map[10]={
        "",
        "",
        "abc",
        "def",
        "ghi",
        "jkl",
        "mno",
        "pqrs",
        "tuv",
        "wxyz"
    };
public:
    vector<string> res;
    string row;   
    void traversal(string& digits, int index){
        if(digits.size()==index){
            res.push_back(row);
            return;
        }

        int in=digits[index]-'0';
        for(int i=0; i<map[in].size();i++){
            row.push_back(map[in][i]);
            traversal(digits,index+1);
            row.pop_back();
        }
        return;
    }
    
    vector<string> letterCombinations(string digits) {
        res.clear();
        row.clear();
        if(digits=="") return {};
        traversal(digits,0);
        return res;
    }
};

39.组合总和

与之前组合问题不同,本题中candidates中的元素可以重复使用,(应注意for循环时,i是从index开始,避免结果vector中出现重复元素) 代码如下:

class Solution {
private:
    void backtracking(vector<int>& vec, int sum, int target, int index){
        if(sum>target){
            return;
        }
        if(sum == target){
            res.push_back(path);
            return;
        }
        for(int i=index; i<vec.size(); i++){
            path.push_back(vec[i]);
            sum+=vec[i];
            backtracking(vec, sum, target,i);
            sum-=vec[i];
            path.pop_back();
        }
        return;
    }
public:
    vector<vector<int>> res;
    vector<int> path;
    vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
        res.clear();
        path.clear();
        //sort(candidates.begin(), candidates.end());
        backtracking(candidates,0,target,0);
        return res;
    }
};

剪枝优化:**一开始对candidates数组进行排序,当下一层的总和sum(即本层的sum+candidates[i])已经大于target,就可以结束本轮for循环。
在这里插入图片描述
for循环剪枝代码如下:

for (int i = index; i < candidates.size() && sum + candidates[i] <= target; i++)

40.组合总和2(有陷阱)

此题是candidates数组中有重复元素,并且要求我们不能重复使用数组中的元素,所以我们就要对同一树层中的元素进行去重,即横向方向去重。第一种代码如下:

class Solution {
private:
    void backtracking(vector<int>& vec, int target, int sum, int index){
        if(sum>target) return;
        if(target == sum){
            res.push_back(path);
            return;
        }  

        for(int i=index; i<vec.size()&&sum+vec[i]<=target; i++){
            //对同一树层使用过的元素跳过
            if(i>index && vec[i]==vec[i-1]) continue;
            path.push_back(vec[i]);
            sum+=vec[i];
            backtracking(vec, target, sum, i+1);
            sum-=vec[i];
            path.pop_back();
        }
        return;
    }

public:
    vector<vector<int>> res;
    vector<int> path;
    vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {
        res.clear();
        path.clear();
        sort(candidates.begin(), candidates.end());
        backtracking(candidates, target, 0, 0);
        return res;

    }
};

以上代码是未使用used标记数组,使用used标记数组代码如下:

class Solution {
private:
    void backtracking(vector<int>& vec, int target, int sum, int index, vector<bool>& used){
        if(sum>target) return;
        if(target == sum){
            res.push_back(path);
            return;
        }  

        for(int i=index; i<vec.size()&&sum+vec[i]<=target; i++){
            //对同一树层使用过的元素跳过
            //used[i-1]=false 说明同一数层candidates[i-1]使用过
            //used[i-1]=true 说明同一树枝candidates[i-1]使用过

            if(i>0 && vec[i]==vec[i-1] && used[i-1]==false) continue;

            path.push_back(vec[i]);
            used[i]=true;
            sum+=vec[i];
            backtracking(vec, target, sum, i+1, used);
            sum-=vec[i];
            used[i]=false;
            path.pop_back();
        }
        return;
    }

public:
    vector<vector<int>> res;
    vector<int> path;
    vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {
        res.clear();
        path.clear();
        vector<bool> used(candidates.size(),false);
        sort(candidates.begin(), candidates.end());
        backtracking(candidates, target, 0, 0, used);
        return res;

    }
};

131.分割回文串**(hard)

代码如下:

class Solution {
private:
    void backtracking(string s, int index){
        if(index >= s.size()){
            res.push_back(path);
            return;
        } 

        for(int i=index; i<s.size(); i++){
            if(palindromic(s,index,i)){
                string str=s.substr(index, i-index+1);
                path.push_back(str);
            }
            //这个过程不太理解 不满足回文串直接跳过
            //满足回文串则进行递归
            else{
                continue;
            }
            backtracking(s,i+1);
            path.pop_back();
        }
        return;
    }
	//判断是否是回文串
    bool palindromic(string s, int start, int end){
        for(int i=start,j=end; i<j; i++,j--){
            if(s[i]!=s[j]){
                return false;
            }
        }
        return true;
    }
public:
    vector<vector<string>> res;
    vector<string> path;
    vector<vector<string>> partition(string s) {
        res.clear();
        path.clear();
        backtracking(s,0);
        return res;
    }
};

本题是切割问题,切割过程与之前组合问题类似,切割问题也可以抽象为一棵树形结构:
在这里插入图片描述
切割线(图中的红线)切割到字符串的结尾位置,说明了找到一种切割方法。

93.复原ip地址*

代码如下:

class Solution {
private:
    vector<string> res;
    void backtracking(string& s, int index, int pointnum){
        if(pointnum==3){
            if(isvalid(s,index,s.size()-1)){
                res.push_back(s);
            }
            return;
        }

        //单层搜索逻辑
        for(int i=index; i<s.size(); i++){
            if(isvalid(s,index,i)){ //判断[index,i]区间的子串是否合法
                s.insert(s.begin()+i+1, '.'); //在i的后面加一个.
                pointnum++;
                backtracking(s,i+2,pointnum); 插入逗点之后下一个子串的起始位置为i+2
                //回溯
                pointnum--;
                s.erase(s.begin()+i+1);
            }
        }
        return;
        
    }

    bool isvalid(string s, int start, int end){
        if(start>end)
            return false;
        //开头字符为0 非法
        if(s[start]=='0' && start!=end){
            return false;
        }
        int num=0;
        for(int i=start; i<end; i++){
            num=num*10+s[i]-'0';
        }
        if(num>255){
            return false;
        }
        return true;
    }

public:
    vector<string> restoreIpAddresses(string s) {
        res.clear();
        if(s.size()<4 || s.size()>12) return res;//算剪枝了
        backtracking(s,0,0);
        return res;
    }
};

在这里插入图片描述注:backtracking参数中不能定义const string& s,因为在函数体内会对字符串进行insert等操作。

78.子集(标准回溯模板题型)

代码如下:

class Solution {
private:
    vector<vector<int>> res;
    vector<int> path;
    void backtracking(vector<int>& nums, int index){
        res.push_back(path);//收集子集,要放在最上面,否则会漏掉自己
        //可不写
        if(index >= nums.size()){
            return;
        }
        //单层搜索过程:
        for(int i=index; i<nums.size(); i++){
            path.push_back(nums[i]);
            backtracking(nums,i+1);
            path.pop_back();
        }
        return;
    }

public:
    vector<vector<int>> subsets(vector<int>& nums) {
        res.clear();
        path.clear();
        backtracking(nums,0);
        return res;
    }
};

要清楚子集问题和组合问题、分割问题的的区别,子集是收集树形结构中树的所有节点的结果。而组合问题、分割问题是收集树形结构中叶子节点的结果。
在这里插入图片描述
上图中,树的第一个分支的所有得子集都是从最上层的for循环中i=0迭代回溯而来,即通过不断的递归回溯,入栈出栈的过程。许多人可能会深入研究整个递归过程的逻辑,我也一样,总是想要对整个过程一探究竟,我们可以拿出笔试试,不要只通过大脑进行模拟,这样很容易忘记递归回溯究竟走到了哪一层。

小结

对于组合问题,什么时候需要startIndex呢?

如果是一个集合来求组合的话,就需要startIndex,例如:回溯算法:求组合问题!回溯算法:求组合总和!

如果是多个集合取组合,各个集合之间相互不影响,那么就不用startIndex,例如:回溯算法:电话号码的字母组合!

90.子集2

在这里插入图片描述本题和40.组合总和2相似,就是在回溯的基础上进行去重,本题依然可以通过设置used数组进行求解,并且通过树枝去重树层去重的逻辑进行求解。需要注意的是去重的前提是,需要对vector进行排序。代码如下:

class Solution {
private:
    vector<vector<int>> res;
    vector<int> path;
    void backtracking(vector<int>& nums, int index){
        res.push_back(path);
        if(index >= nums.size()){
            return;
        }

        for(int i=index; i<nums.size(); i++){
            if(i>index && nums[i]==nums[i-1]) continue;
            path.push_back(nums[i]);
            backtracking(nums, i+1);
            path.pop_back();
        }
        return;
    }
public:
    vector<vector<int>> subsetsWithDup(vector<int>& nums) {
        res.clear();
        path.clear();
        //去重需要排序
        sort(nums.begin(),nums.end());
        backtracking(nums,0);
        return res;
    }
};

49.递增子序列

class Solution {
private:
    vector<vector<int>> res;
    vector<int> path;
    void backtracking(vector<int>& nums, int index){
        if(path.size()>=2){
            res.push_back(path);
        }
        //也可以用unordered_set<int> uset,去重,但是使用数组
        //来当做哈希,可以减少时间消耗,属于是一种优化的方法。
        //每层都重新定义一个used数组,来记录当前层是否有重复元素
        int used[201]={0};
        for(int i=index; i<nums.size(); i++){
            if((!path.empty() && nums[i]<path.back()) || used[nums[i]+100]==1){
                continue;
            }
            path.push_back(nums[i]);
            used[nums[i]+100]=1;//去重,本层重复的元素不再使用,直接跳过
            backtracking(nums,i+1);
            path.pop_back();
        }
        return;
    }
public:

    vector<vector<int>> findSubsequences(vector<int>& nums) {
        res.clear();
        path.clear();
        backtracking(nums,0);
        return res;
    }
};

题目给的示例为 [4,6,7,7] ,这个示例容易给我们造成误导,认为例子数组一定是递增或者递减,但实际上完全可以是 [4,7,6,7] 这种,示例如下:
在这里插入图片描述

46.全排列

代码如下:

class Solution {
private:
    //不是单纯的组合问题,排列问题也应该用到used
    vector<vector<int>> res;
    vector<int> path;
    void backtracking(vector<int>& nums, vector<bool>& used){
        if(path.size() == nums.size()){
            res.push_back(path);
            return;
        }

        for(int i=0; i<nums.size(); i++){
            if(used[i] == false){
                path.push_back(nums[i]);
                used[i]=true;       
                backtracking(nums, used);
                used[i]=false;
                path.pop_back();
            }
            else{
            //path里已经收录的元素,直接跳过
                continue;
            }
        }
        return;
    }
public:
    vector<vector<int>> permute(vector<int>& nums) {
        res.clear();
        path.clear();
        vector<bool> used(nums.size(), false);
        backtracking(nums, used);
        return res;
    }
};

在这里插入图片描述
排列问题的不同:

  • 每层都是从0开始搜索,而不是从index;
  • 需要used数组来记录path中放入了哪些元素。

47.全排列2(两种去重逻辑,used[i-1]=fasle或used[i-1]=true)

代码如下:
注意去重先排序,本题中 同一树层(横向for循环) 中相同元素不可以重复使用,同一树枝(纵向递归) 中相同元素可以重复使用,使用used数组记录当前元素是否已经被使用。

class Solution {
private:
    vector<vector<int>> res;
    vector<int> path;
    void backtracking(vector<int>& nums, vector<bool> used){
        if(path.size() == nums.size()){
            res.push_back(path);
            return;
        }
         
        for(int i=0; i<nums.size(); i++){
        	//used[i-1]=false也能通过,false是树层去重,true是树枝去重
        	//对于排列问题,树层去重效率更高
            if((i-1>=0 && nums[i]==nums[i-1]) && used[i-1]==true){
                continue;
            }
            if(used[i] == false){
                path.push_back(nums[i]);
                used[i]=true;
                backtracking(nums, used);
                used[i]=false;
                path.pop_back();
            }
        }
        return;
    }
public:
    //去重先排序 本题同一树层不可以重复,同一树枝可以重复,并需要used数组
    vector<vector<int>> permuteUnique(vector<int>& nums) {
        res.clear();
        path.clear();
        sort(nums.begin(), nums.end());
        vector<bool> used(nums.size(),false);
        backtracking(nums, used);
        return res;
    }
};

在这里插入图片描述
两种去重逻辑,used[i-1]=fasle或used[i-1]=true,树层上去重(used[i - 1] == false),的树形结构如下:
在这里插入图片描述
树枝上去重(used[i - 1] == true)的树型结构如下:
在这里插入图片描述

332.重新安排行程(hard,也可用图论深度优先搜索)

本题目锻炼了我们对容器的使用,同时回溯的返回值是bool类型,这说明我们只要找到唯一一个满足条件的行程,即唯一一个到叶子节点的路线, 即可返回。同时应对题目说的字典序要求,使用了map中对key值自动排序的特性进行了解决。代码如下:

class Solution {
private:
    //unordered<出发地点,map<到达地点,票的张数>> targets
    unordered_map<string,map<string,int>> targets;
    //有一条路径即可返回,即地方全到了,res中的数=票的数+1;
    bool backtracking(int ticketnum, vector<string>& res){
        if(res.size() == ticketnum+1){
            return true;
        }
        for(pair<const string, int>& target: targets[res[res.size()-1]]){
            if(target.second > 0){
                res.push_back(target.first);
                target.second--;
                if(backtracking(ticketnum, res)){
                    return true;
                }
                target.second++;
                res.pop_back();
            }
        }
        return false;
    }

public:
    vector<string> findItinerary(vector<vector<string>>& tickets) {
        targets.clear();
        vector<string> res;
        for(const vector<string>& vec: tickets){
            //targets[vec[0]]代表哈希表的key targets[vec[0]][vec[1]]++
            //代表map中的key对应的value++;
            targets[vec[0]][vec[1]]++; //记录映射关系
        }
        res.push_back("JFK");
        backtracking(tickets.size(), res);
        return res;
    }
};

在这里插入图片描述

51.n-皇后(hard)

代码如下:

class Solution {
private:
    vector<vector<string>> res;
    void backtracking(vector<string>& chess, int n, int row){
        //终止条件
        if(row == n){
            res.push_back(chess);
            return;
        }

        //单层搜索逻辑
        for(int col=0; col<n; col++){
            if(isvalid(row, col, chess, n)){
                chess[row][col]='Q';
                backtracking(chess, n, row+1);//回溯 求下一列
                chess[row][col]='.';//回溯撤销
            }
        }
        return;
    }

    bool isvalid(int row, int col, vector<string> chess, int n){
        //检查列
        for(int i=0; i<row; i++){
            if(chess[i][col]=='Q'){
                return false;
            }
        }
        //检查45度 左侧斜上
        for(int i=row-1,j=col-1; i>=0 && j>=0; i--,j--){
            if(chess[i][j] == 'Q'){
                return false;
            }
        }
        //检查135度 右侧斜上
        for(int i=row-1,j=col+1; i>=0 && j<n; i--,j++){
            if(chess[i][j] == 'Q'){
                return false;
            }
        }
       return true;
    }

public:
    vector<vector<string>> solveNQueens(int n) {
        res.clear();
        vector<string> chess(n,string(n,'.'));
        backtracking(chess, n, 0);
        return res;
    }
};

本题为经典回溯问题,n皇后数据形式为 n ∗ n n*n nn形式,可以将其想象成一个树形结构,并由此进行回溯求解。共为n行, 所以递归深入n层,定义一个row变量记录当前递归行数,当row==n,回溯终止;共有n列, 每层回溯循环n次,判断当前列是否可以放置皇后Q。
在这里插入图片描述

37.解数独(hard)

本题与以往不同,借用Carl哥的话,本题的解题形式是二维递归,采用两个for循环,以行列的方式递归。代码如下:

class Solution {
private:
    //二维回溯
    bool backtracking(vector<vector<char>>& board){
        for(int row=0; row<board.size(); row++){
            for(int col=0; col<board[0].size(); col++){
                if(board[row][col]=='.'){
                    for(char k='1'; k<='9'; k++){
                        if(isvalid(row, col, k, board)){
                            board[row][col]=k;
                            //如果找到合适的一组,立刻返回
                            //进入递归,本层递归中board[row][col]已经由.变成了1-9中的数字,
                            //所以继续递归下去,继续把.变成数字,直到所有的.都变成了数字,即找到了
                            //一组合法的结果,直接返回。
                            if(backtracking(board)){
                            	return true;
                            } 
                            board[row][col]='.';
                        }
                    }
                    return false;
                }
            }
        }
        return true; //遍历完没有返回false;说明找到了合适棋盘位置了
    }

    bool isvalid(int row, int col, char k,vector<vector<char>>& board){
        //数字 1-9 在每一行只能出现一次。
        //注意:是i<9,不是i<col
        for(int i=0; i<9; i++){
            if(board[row][i] == k){
                return false;
            }
        }
        //数字1-9 在每一列只能出现一次
        for(int i=0; i<9; i++){
            if(board[i][col] == k){
                return false;
            }
        }
        //数字 1-9 在每一个以粗实线分隔的 3x3 宫内只能出现一次。
        int startrow=(row/3)*3;
        int startcol=(col/3)*3;
        //将整个区域划分成九个3*3区域,startrow,startcol为其中某个区域的开始位置
        for(int i=startrow; i<startrow+3; i++){
            for(int j=startcol; j<startcol+3; j++){
                if(board[i][j]==k){
                    return false;
                }
            }
        }
        return true;
    }

public:
    void solveSudoku(vector<vector<char>>& board) {
        backtracking(board);
    }
};
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
代码随想录算法训练营是一个优质的学习和讨论平台,提供了丰富的算法训练内容和讨论交流机会。在训练营中,学员们可以通过观看视频讲解来学习算法知识,并根据讲解内容进行刷题练习。此外,训练营还提供了刷题建议,例如先看视频、了解自己所使用的编程语言、使用日志等方法来提高刷题效果和语言掌握程度。 训练营中的讨论内容非常丰富,涵盖了各种算法知识点和解题方法。例如,在第14天的训练营中,讲解了二叉树的理论基础、递归遍历、迭代遍历和统一遍历的内容。此外,在讨论中还分享了相关的博客文章和配图,帮助学员更好地理解和掌握二叉树的遍历方法。 训练营还提供了每日的讨论知识点,例如在第15天的讨论中,介绍了层序遍历的方法和使用队列来模拟一层一层遍历的效果。在第16天的讨论中,重点讨论了如何进行调试(debug)的方法,认为掌握调试技巧可以帮助学员更好地解决问题和写出正确的算法代码。 总之,代码随想录算法训练营是一个提供优质学习和讨论环境的平台,可以帮助学员系统地学习算法知识,并提供了丰富的讨论内容和刷题建议来提高算法编程能力。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* *3* [代码随想录算法训练营每日精华](https://blog.csdn.net/weixin_38556197/article/details/128462133)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 100%"] [ .reference_list ]

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值