力扣刷题笔记——回溯

本文为代码随想录的学习笔记捏,可自行搜索公众号学习

目录

组合问题

电话号码字母组合

 分割字符串

131. 分割回文串 - 力扣(LeetCode)https://leetcode.cn/problems/palindrome-partitioning/

IP地址分割

求子集问题

子集

78. 子集 - 力扣(LeetCode)https://leetcode.cn/problems/subsets/

 递增子序列

重新安排⾏程

n 皇后问题


回溯是一种暴力搜索的算法,可解决以下问题:

为什么是暴力搜索呢,因为这个方法的思想就是列举全部可能的结果,从中取出符合题目要求的结果。

所以说,回溯不高效,并不高效但不代表没用,对于有的问题我们也只能使用回溯解决。

下面的问题就是例子:

  1. 组合问题:N个数里面按照一定的规则找出k个数的集合
  2. 分割问:一个字符串按照一定规则有几种切割方式
  3. 子集:一个N个数的集合中有多少符合条件的子集
  4. 排列:N个数按照一定规则全排列,有几种排列方式
  5. 棋盘问题:N皇后,解数独
  6. 其他

回溯是递归的副产品,只要有递归就会有回溯,回溯的本质为穷举,

集合的大小决定了回溯树的宽度,递归的深度决定了回溯树的深度

做题公式如下

void backtracking(参数)
{
    if(终止条件){
        存放结果;
        return;
    }

    for(选择:本层集合中的元素){
        处理节点;        
        backtracking(参数)
        回溯撤销结果
    }
}

组合问题

77. 组合 - 力扣(LeetCode)https://leetcode.cn/problems/combinations/

class Solution {
public:
    vector<vector<int>> res;//用于存放结果
    vector<int> path;//用来存放符合条件结果
    vector<vector<int>> combine(int n, int k) {
          
        backtracking(n,k,1);
        return res;
    }

    void backtracking(int n,int k,int index){
        if(path.size()==k){
             res.push_back(path);
             return;
         }
        for(int i=index;i<=n;i++){
            path.push_back(i);//处理节点
            backtracking(n,k,i+1);//递归
            path.pop_back();//回溯
        }
    }
    
};

剪枝优化:

path.size()为已经选定的数据

k-path.size()为还需要的选择的数据

那么索引开始的位置至多为n-(k-path.size())+1

+1是为了左边闭合,因为我们的索引是从一开始

修改结果:

for(int i=index;i<n-(k-path.size())+1;i++){

}

递归树:

 减去红色位置的遍历的路径

216. 组合总和 III - 力扣(LeetCode)https://leetcode.cn/problems/combination-sum-iii/

class Solution {
public:
    vector<vector<int>> res;
    vector<int> path;
    vector<vector<int>> combinationSum3(int k, int n) {
        backtracking(k,n,1);
        return res;
    }


    void backtracking(int k,int sum,int index){
        if(sum==0&&path.size()==k){
            res.push_back(path);
            return;
        } 
        if(sum<0||path.size()>k){
            return;
        }

        for(int i=index;i<=9;i++){
            //对于节点的处理
            path.push_back(i);
            sum-=i;

            backtracking(k,sum,i+1);

            sum+=i;
            path.pop_back();
        }
    }
};

39. 组合总和 - 力扣(LeetCode)https://leetcode.cn/problems/combination-sum/ 本问题和之前问题的主要区别就于:元素是可以重复选取的,并且没有个数的限制。

思考回溯函数的参数:

  • 数组
  • 目标值
  • 开始的索引startindex
  • 路径中数的总和(可以直接通过target得出)

终止条件:

if(sum==target){ res.push_back(path); return;}
if(sum>target) return;

 ac的代码:

class Solution {
public:
    vector<vector<int>> res;
    vector<int> path;
    vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
        backtracking(candidates,target,0,0);
        return res;
    }


    void backtracking(vector<int>& candidates,int target,int sum,int startindex){
        if(sum==target){ res.push_back(path); return;}
        if(sum>target) return;


        for(int i=startindex;i<candidates.size();i++){
            //对于本节点的处理
            sum+=candidates[i];
            path.push_back(candidates[i]);
            //递归
            backtracking(candidates,target,sum,i);

            //回溯
            sum-=candidates[i];
            path.pop_back();
        }
    }
};

剪枝优化:

如果 sum + candidates[i]  的值也就是下一轮的sum大于target的情况下,没有必要进入下一轮的递归,在for循环中添加条件
sum + candidates[i] <= target

40. 组合总和 II - 力扣(LeetCode)https://leetcode.cn/problems/combination-sum-ii/ 40题和上题比较相似,但是存在细微的差别

  • 给定的集合中元素可以重复
  • 最终的结果中组合不能重复

针对这两个问题,我们需要进行结果的去重,去重的是在本层递归中已经使用过的相同元素。

 那么我们使用一个数组来标识数组中的元素是否被使用过,使用和给定数组同样大小的标识数组,创建used数组标识是否使用访问过某个元素。

参数思考:

  • 题目函数给定的参数
  • sum代表当前的元素的总和
  • startindex代表当前需要开始递归的位置
  • used标识元素是否使用过的数组

结束条件和上题一样。

ac的代码如下

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

    void backtracking(vector<int>& candidates,int target,int sum,int startindex,    vector<bool>& used){
        if(sum==target){ res.push_back(path); return;}
        if(sum>target) return;

        
        for(int i=startindex;i<candidates.size() && sum+candidates[i]<=target;i++){
            //相对于同层之前已经使用过的不能再次使用
           
            //对于本节点的处理
            sum+=candidates[i];
            path.push_back(candidates[i]);
            used[i]=true;
            //递归
            backtracking(candidates,target,sum,i+1,used);

            //回溯
            used[i]=false;
            sum-=candidates[i];
            path.pop_back();
        }
    }
};

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

意思是现在这个i位置的元素和i-1位置元素是一样的,但是i位置的元素(false)已经用过了,有那条路径,我们进行剪枝,防止结果重复。

used[i - 1] == true ,说明同⼀树⽀ candidates[i - 1] 使⽤过
used[i - 1] == false ,说明同⼀树层 candidates[i - 1] 使⽤过

电话号码字母组合

17. 电话号码的字母组合 - 力扣(LeetCode)https://leetcode.cn/problems/letter-combinations-of-a-phone-number/思考需要传输哪些参数:

  • path.size()确定好代表字母的按键个数
  • 按键位置index
  • 按键总个数n

 参考了一下:

发现参数n可以不用传参,但都可以的没影响。

class Solution {
public:
    vector<string> res;
    vector<char> path;
    vector<string> letterCombinations(string digits) {
        if(digits.size()==0) return res;
        backtracking(digits.size(),0,digits);
        return res;
    }


    void backtracking(int n,int index,string digits){
        //终止条件
        if(n==path.size()){
            string s="";
            for(auto i:path){
                s+=i;
            }
            res.push_back(s);
            return ;
        }

        int sum=0;
        int flag=digits[index]-'0';
        if(flag==9 || flag==7) sum=4;
        else sum=3;

        for(int i=0;i<sum;i++){
            //特殊处理
            int offset=flag>7?1:0;
            char c=(flag-2)*3+offset+i+'a';

            path.push_back(c);

            backtracking(n,index+1,digits);

            path.pop_back();
        }

    }
};

 分割字符串

131. 分割回文串 - 力扣(LeetCode)https://leetcode.cn/problems/palindrome-partitioning/

 本题需要将分割为回文字符串的各种方式添加到最终的结果数组中,以分割位置为索引和组合问题比较相似。

思考传参:

  1. 字符串s
  2. 切割的索引index

思考终止条件:
如果切割的位置到了字符串的最后一个位置,那么跳出循环,添加路径上的数到结果数组。

 ac代码如下:

class Solution {
public:
    vector<vector<string>> result;
    vector<string> path;
    vector<vector<string>> partition(string s) {
        backtracking(s,0);
        return result;
    }
    bool ishuiwen(const string& s,int start,int end){
        for(int i=start,j=end;i<j;i++,j--){
            if(s[i]==s[j]){
                continue;
            }else{
                return false;
            }
        }
        return true;
    }
    void backtracking(const string& s,int index){
        if(index>=s.length()){
            result.push_back(path);
            return;
        }

        for(int i=index;i<s.length();i++){
            if(ishuiwen(s,index,i)){
                string str=s.substr(index,i-index+1);
                path.push_back(str);
            }
            else{
                continue;
            }

            backtracking(s,i+1);

            path.pop_back();
        }



    }
};

IP地址分割

93. 复原 IP 地址 - 力扣(LeetCode)https://leetcode.cn/problems/restore-ip-addresses/与上一道题分割字符串相似,本题也可使用回溯,以分割地址为索引,添加点号,回溯时删掉。

思考参数

void backtracking(string& s,int pointNum,int startindex);

s:字符串

pointNum:已经添加的点号的数量,本体为3时符合返回的条件

startindex:下次分割位置开始选择的索引

终止返回条件

  1. 添加了三个点
  2. 第四段数据符合要求 

添加isvalid函数用于判断切割的字串:

start-end之间的字串是否符合要求 

class Solution {
public:
    vector<string> res; 
    vector<string> restoreIpAddresses(string s) {
        res.clear();
        if(s.size()<4 || s.size()>12) return res;
        backtracking(s,0,0);
        return res;
    }

    bool isvalid(const string& s,int start,int end){

        if (start > end) {
            return false;
            }
      
        if (s[start] == '0' && start != end) { // 0开头的数字不合法
                        return false;
        }

        int num = 0;
        for (int i = start; i <= end; i++) {
                if (s[i] > '9' || s[i] < '0') { // 遇到⾮数字字符不合法
                return false;
            }
            num = num * 10 + (s[i] - '0');
            if (num > 255) { // 如果⼤于255了不合法
            return false;
            }
        }
        return true;
    }
    void backtracking(string& s,int sum,int startindex){
        if(sum==3){
            if(isvalid(s,startindex,s.size()-1)){
                res.push_back(s);
            }
            return;
        }

        for(int i=startindex;i<s.size();i++){

            if(isvalid(s,startindex,i)){
                s.insert(s.begin()+i+1,'.');
                backtracking(s,sum+1,i+2);
                s.erase(s.begin() + i + 1);
            }else break;

        }   
    }
};

求子集问题

子集

78. 子集 - 力扣(LeetCode)https://leetcode.cn/problems/subsets/

 分割和组合是求回溯树的叶子节点的值,而子集问题是收集树中的所有的节点。

for的遍历从startindex开始,到数组的最后

class Solution {
public:
    vector<vector<int>> res;
    vector<int> vec;
    vector<vector<int>> subsets(vector<int>& nums) {

        backtracking(nums,0);
        return res;
    }

    void backtracking(vector<int>& nums,int index){
      
        res.push_back(vec);
        

        for(int i=index;i<nums.size();i++){
            vec.push_back(nums[i]);
            backtracking(nums,i+1);
            vec.pop_back();
        }
    }
};

 递增子序列

491. 递增子序列 - 力扣(LeetCode)https://leetcode.cn/problems/non-decreasing-subsequences/这道题也是求子序列,但是需要的是递增的子序列,因此需要进行判断,同时不允许重复,因此对结果进行去重,添加set,对于每层的元素进行去重

思考参数

void backtracking(vector<int>& nums,int index);

index为遍历到的索引位置

终止条件

题目要求两个元素及以上,重复的两个元素作为特殊的序列 

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


    void backtracking(vector<int>& nums,int index){
        if(path.size()>1){
            res.push_back(path);
        }
        //对每层的结果进行去重
        unordered_set<int> uset;
        for(int i=index;i<nums.size();i++){

            if ((!path.empty() && nums[i] < path.back())
                || uset.find(nums[i]) != uset.end()) {
                continue;
                }

            uset.insert(nums[i]);
            path.push_back(nums[i]);

            backtracking(nums,i+1);

            path.pop_back();
        }
    }
};

重新安排⾏程

332. 重新安排行程 - 力扣(LeetCode)https://leetcode.cn/problems/reconstruct-itinerary/

本题的标签是深度搜索,但深度搜索中是包含回溯的思想的。

思考本题:

        我们首先需要将给出的对应的关系映射成类似邻接表的形式,将他们之间的可到达的关系存储起来

        需要输出字母序排序在前面的结果

        在回溯时,最终的返回条件(终止条件)的设置

第一个问题,映射可到达的关系,一个机场可以到达多个机场,因此既可以是一对一,也可以是一对多,因此使用unordered_map<string,multiset<string>> 可以存储相关的信息 ,使用map也可以,map维持存储数据的顺序,同时我们也可以用map<string,unordered_map<string,int>> map来进行数据的存储

因为map会对能到达的机场进行排序,因此可以保证每次遍历的时候都是先遍历排序在前的机场。

思考结束的条件,一个机场,可以是出发的地方也是到达的地方

那么需要的机场数为票数加一

if(ticketnum+1=result.size()); 

class Solution {
public:
    vector<string> result;
    unordered_map<string,map<string,int>> targets;
    vector<string> findItinerary(vector<vector<string>>& tickets) {
        targets.clear();
        for(const vector<string>& vec:tickets){
            targets[vec[0]][vec[1]]++;
        }
        result.push_back("JFK");
        backtracking(tickets.size());
        return result;
    }


    bool backtracking(int ticketNum){
        if(ticketNum+1==result.size()){
            return true;
        }

        //对现在到达的机场可到达的机场进行遍历
        for(pair<const string,int>& target:targets[result[result.size()-1]]){
            if(target.second>0){
                result.push_back(target.first);
                target.second--;
                if(backtracking(ticketNum)) return true;//如果找到了一条路线,就是答案,立即返回
                target.second++;
                result.pop_back();
            }
        }

        return false;
    }
};

n 皇后问题

51. N 皇后 - 力扣(LeetCode)https://leetcode.cn/problems/n-queens/

n皇后问题要求我们给出所有的可能的排列方式,在二维数组上进行回溯,如下面的回溯树。

回溯的宽度为每列的长度,深度为行数,

构建回溯的逻辑:
for(int col=0;col<n;col++){

        处理节点

        back();递归

        回溯,撤销处理结果

思考终止条件,如果遍历到叶子节点,就是最后一行的时候,添加结果返回

if(row==n){

        result.push_back(chessboard);

        return;

我们思考什么时候,一个位置是可以进行插入棋子的,

在之前的行的棋子,不在将要插入位置的同一列,不在左上的斜线上和右上的斜线上 

bool isvalid(int row,int col,vector<string>& chessboard,int n){
        for(int i=0;i<row;i++){
            if(chessboard[i][col]=='Q') return false;
        }


        for(int i=row-1,j=col-1;i>=0&&j>=0;i--,j--){
            if(chessboard[i][j]=='Q') return false;
        }

        for(int i=row-1,j=col+1;i>=0&&j<n;i--,j++){
            if(chessboard[i][j]=='Q') return false;
        }

        return true;
    }

class Solution {
public:

    vector<vector<string>> result;
    void backtracking(int n,int row,vector<string>& chessboard){

        if(row==n){
            result.push_back(chessboard);
            return;
        }

        for(int i=0;i<n;i++){
            if(isvalid(row,i,chessboard,n)){
                chessboard[row][i] = 'Q';
                
                backtracking(n,row+1,chessboard);

                 chessboard[row][i] = '.';  
            }

        }
    }

    bool isvalid(int row,int col,vector<string>& chessboard,int n){
        for(int i=0;i<row;i++){
            if(chessboard[i][col]=='Q') return false;
        }


        for(int i=row-1,j=col-1;i>=0&&j>=0;i--,j--){
            if(chessboard[i][j]=='Q') return false;
        }

        for(int i=row-1,j=col+1;i>=0&&j<n;i--,j++){
            if(chessboard[i][j]=='Q') return false;
        }

        return true;
    }
    vector<vector<string>> solveNQueens(int n) {

        vector<string> vec(n,string(n, '.'));
        backtracking(n,0,vec);
        return result;
    }
};

猜你喜欢

转载自blog.csdn.net/qq_53633989/article/details/130450890
今日推荐