leetcode (五) DFS 回溯

17.电话号码 **

思路:
根据每个数字对应的字母列出他们所有的排列组合

  • 最终答案vector< string > res(1,"");初始化为一个空串 ,映射表chars[8]对应字符串
  • 遍历每一个数字,对于每个数字找到他在映射表中的所有备选字母
  • 将所有字母添加到res所有元素后,构成一个新的verctor< string >now
  • 将now赋值给res,进行下一次循环

在这里插入图片描述

输入数字为23时对应的流程

在这里插入图片描述
在这里插入图片描述

class Solution {
public:

    string chars[8]={"abc","def","ghi","jkl","mno","pqrs","tuv","wxyz"};

    vector<string> letterCombinations(string digits) {
          vector<string> res(1,"");

          if(!digits.size())return {};     //输入数字为空返回{}

          for(auto u:digits){                     //遍历每个数字
              vector<string> now;

              for(auto c:chars[u-'2'])            //找到该数字对应的字母
                    for(auto it:res){
                        now.push_back(it+c);      //将每个字母加入到res之后构成一个新的Vector
                    }
            res=now;           
          }

          return res;
    }
};

79.单词搜索 **

思路:对于每个单词都当做开头进行搜索
注意写法和技巧

在这里插入图片描述

在这里插入图片描述

class Solution {
public:
    int n,m;

    bool exist(vector<vector<char>>& board, string word) {
        n=board.size(),m=board[0].size();     //行,列的值

        if(!n||!m)return false;
 
        for(int i=0;i<n;i++)
           for(int j=0;j<m;j++)
               if(dfs(board,i,j,word,0))return true;   //对于矩阵里的每个点,把他们当做开头,搜索一遍

        return false;

    }

    bool dfs(vector<vector<char>> &board,int x,int y,string &word,int count){
        if(board[x][y]!=word[count])return false;   //不匹配返回false

        if(count==word.size()-1)return true;        //当前是word的最后一个字母且匹配,则搜索到,返回true

        board[x][y]='.';       //赋非常规值,代表已经遍历过,如果下一层再向这个方向走,就会返回

        int dx[4]={0,0,1,-1},dy[4]={1,-1,0,0};     //定义要搜索的四个方向

        for(int i=0;i<4;i++){
            int a=x+dx[i],b=y+dy[i];

            if(a>=0&&a<n&&b>=0&&b<m){
                if(dfs(board,a,b,word,count+1))return true;  //向四个方向分别搜索,如果搜索到返回true
            }
        }

        board[x][y]=word[count];                //回溯,该层恢复原状

        return  false;                           //所有方向都不完全匹配则返回false
    }
};

46.全排列

排列顺序:对于1~n的每一个位置,再nums选择一个数(还有一种是数选位置,思想和高中排列组合一模一样,这里写法有些不同)
定义一个vector< bool > st 判断该数在当前路径是否使用过
vector< int >存储每一次dfs最后的答案

在这里插入图片描述
在这里插入图片描述

class Solution {
public:
    vector<vector<int>> res;

    vector<bool> st;                //判重

    vector<int>  path;              //存储每次的结果

    int n;

    vector<vector<int>> permute(vector<int>& nums) {
        n=nums.size();

        if(!n)return {{}};

        st=vector<bool>(n);

        dfs(0,nums);

        return res;
    }

    void dfs(int u,vector<int> & nums){
         if(u==n){                            //搜索到第n层时返回,注意如果一共有3个数,那么搜索的次数是4次
             res.push_back(path); 

             return ;
         }

         for(int i=0;i<n;i++){            //对于每个位置,枚举nums的每一个数
             if(!st[i]){
             path.push_back(nums[i]);

             st[i]=true;

             dfs(u+1,nums);           

             path.pop_back();         //每次放进向量的数要拿出

             st[i]=false;
            }
         }

         return;
    }
};

47 全排列II

我们把数字序列看做带有编号的模式,如[1,1,2,2,2]是第一个1,第二个1,第一个2,第二个2,第三个2,并且人为规定顺序,即第二个1永远只能出现在第一个1的后面,这样就不会导致重复的问题
准备工作是首先将nums排序以保证相同的数有序的出现在一起
思路一:

  • 数选位置(本题解法)
  • 从头开始dfs每一个数,枚举当前数可能出现在什么位置(start,end),dfs下一个数的时候,如果下一个数和当前数相同,则下一个数的位置只能从(i+1,end)中枚举(i是上一个数最后确定的位置)否则下一个数从(0,end)中枚举
  • 位置选数
  • 从头开始dfs每一个位置(和上一题一样),对于每一个位置选数,如nums为
    [111222333],第一个位置只能从第一个1第一个2第一个3中选,如果它选择了1,那么第二个位置只能从第二个1第一个2第一个3中选,以此类推

在这里插入图片描述

//数选位置
class Solution {
public:
    vector<int> path;              //每一种方案

    vector<vector<int>>res;        //答案

    vector<bool>  st;              //标记向量

    int n;

    vector<vector<int>> permuteUnique(vector<int>& nums) {
          n=nums.size();

          if(!n)return {{}};

          sort(nums.begin(),nums.end());

          path=vector<int> (n);                 //为path开辟空间可以直接用下标赋值,避免使用push_back函数

          st=vector<bool> (n);

          dfs(nums,0,0);                        //从第0个数,起始位置是0开始dfs

          return res;    
    }

    void dfs(vector<int> &nums,int u,int start){
        if(u==n){                               //枚举n+1次时输出一串序列
            res.push_back(path);

            return;
        }

        for(int i=start;i<n;i++){              //枚举nums[u]可能存放的位置(start,end)
            if(!st[i]){                       //如果当前位置没有被占,那么为它赋值
                path[i]=nums[u];        
                
                st[i]=true;

                dfs(nums,u+1,u+1<n&&nums[u]==nums[u+1]?i+1:0);    //搜索下一个数字存放的位置,如果nums[u]==nums[u+1]搜索(i+1,end)否则搜索(0,end);

                st[i]=false;

            }
        }
    }
};

78 子集

找出一组数的子集,区别于排列组合,只是寻找出所有可能的数的组合
例如[1,2,3]就有23 个子集(每一个数字只有两种情况,选和不选,一共三个数字所以是23,包括空集)

在这里插入图片描述

递归写法

  • 加入空集
  • 枚举1~n位置个数的情况
  • 思想是位置选数
  • 对于一个dfs,搜索nums(start,end),对于他的下一个dfs’,搜索nums(i+1,end)-- 确保下一层不回头枚举 i是dfs最后确定的元素下标
class Solution {
public:
    vector<vector<int>> res;

    vector<int> path;

    int n;

    vector<vector<int>> subsets(vector<int>& nums) {
         n=nums.size();

         res.push_back({});

         for(int i=1;i<=n;i++){             //枚举一个元素,两个元素...n个元素的情况
             dfs(nums,0,i,0);
         }

         return res;    
    }

    void dfs(vector <int> &nums,int u,int count,int start){
        if(u==count){                //u=count输出
            res.push_back(path);

            return;
        }

        for(int i=start;i<n;i++){       //对于每个位置u,寻找从(start,end)
                path.push_back(nums[i]);

                dfs(nums,u+1,count,i+1);   //为避免重复下一个元素start置为i+1

                path.pop_back();         //回溯
        }
    }
};


接下来写一个y总的另类解法
思路是每一种情况正好可用二进制表示
例如[1,2,3],n=3,正好对应8个二进数000,001,010…
每个二进制数都对应一个子集
如000对应空集{},001对应{3},101对应{1,3}等

在这里插入图片描述

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

         int n=nums.size();

         for(int i=0;i<1<<n;i++){       //i<1右移n位  即枚举0~n-1的二进制数

             vector<int> now;

             for(int j=0;j<n;j++){
                if(i>>j&1)now.push_back(nums[j]);  //对于每个二进制数都做左移处理,如果当前位存在,则加入该元素
             }

             res.push_back(now);
         }
         return res;
    }
};

90 子集II **

  • 解一:
    在前一题的基础上,首先我们知道了枚举无重复元素nums的时候每一层枚举不会回头(保证不出现同元素不同排列),并且每层起始位置+1
    这题枚举存在重复元素nums时,只要保证当前层不会再出现相同的元素,即循环的时候每次跳到下一个不同的元素位置

在这里插入图片描述

class Solution {
public:
    vector<vector<int>> res;

    vector<int> path;

    int n;

    vector<vector<int>> subsetsWithDup(vector<int>& nums) {
         n=nums.size();

         res.push_back({});

         sort(nums.begin(),nums.end());    //保证相同数出现在一起

         for(int i=1;i<=n;i++){             //枚举一个元素,两个元素...n个元素的情况
             dfs(nums,0,i,0);
         }

         return res;    
    }

    void dfs(vector <int> &nums,int u,int count,int start){
        if(u==count){                  //u=count输出
            res.push_back(path);

            return;
        }

        for(int i=start;i<n;i++){       //对于每个位置u,寻找从(start,end)

                path.push_back(nums[i]);

                dfs(nums,u+1,count,i+1);   //为避免重复下一个元素start置为i+1

                path.pop_back();           //回溯

                while(i<n-1&&nums[i]==nums[i+1])i++;   //跳过这个位置相同的元素(剪枝)
        }
    }
};

  • 解二:
    枚举每一种可能的情况
    如[11122]
    那么1出现次数:0,1,2,3
    2出现的次数0,1,2
    对出现次数全排列,一共12中可能(包含空集)
class Solution {
public:
    vector<vector<int>>res;

    vector<int>path;

    int n;

    vector<vector<int>> subsetsWithDup(vector<int>& nums) {
    
         n=nums.size();

         sort(nums.begin(),nums.end());

         dfs(nums,0);

         return res;

    }

    void dfs(vector<int> &nums,int u){

        if(u==n){                   //u是nums元素下标,搜到最后一个元素下一个位置时返回一种情况
            res.push_back(path);     

            return ;
        }

        int k=0;

        while(u+k<n&&nums[u]==nums[u+k])k++;    //找到当前元素相同的个数k

        for(int i=0;i<=k;i++){                  //枚举这个元素可能出现的次数
            dfs(nums,u+k);                      //枚举下一个不同元素的出现次数

            path.push_back(nums[u]);            //最后放入元素,写在dfs前会缺少i=0的情况
        }

        for(int i=0;i<=k;i++)path.pop_back();  //回溯
    }
};

dfs情况如[11122]
首次匹配1为0,2为0,1,2的情况
即path为[],[2],[22],加入res,然后清空(回溯)
然后匹配1为1,2为0,1,2的情况
path为[1],[1,2],[1,2,2],加入res,然后清空,以此类推


对比集合I和集合II的第一种朴素写法,发现都没有添加标记数组st
因为这两题都具有共有的特征–不回头枚举,即下一层元素和上一层不可能存在重复情况所以不必添加st数组

216.组合总和

例如k=3,从前向后枚举每一个位置需要填哪一个数,由于不能包含重复组合,所以每次dfs需要更新为(start+1,end)即不存在重复情况
dfs时加上一统计sum,如果sum==n并且搜索到k层时即可输出一个path

在这里插入图片描述

class Solution {
public:

    vector<vector<int>> res;

    vector<int> path;

    int nums;

    int sum;

    vector<vector<int>> combinationSum3(int k, int n) {    

        nums=k;

        dfs(0,1,n);

        return res;
    }

    void dfs(int u,int start,int n){

        if(u==nums&&sum==n){         //搜到第k层并且sum=n即找到一组解
            res.push_back(path);

            return ;
        }

        for(int i=start;i<10;i++){   
            if(sum<n){              //如果总和小于n,枚举这个数字

                sum+=i;

                path.push_back(i);

                dfs(u+1,i+1,n);     //下一层从第i+1个数字开始枚举

                sum-=i;            //回溯

                path.pop_back();
            }
        }

    }
};

贴上y总从后往前dfs的写法
例k=3,从第k个位置开始向前枚举

class Solution {
public:
    vector<vector<int>> res;

    vector<int >path;

    vector<vector<int>> combinationSum3(int k, int n) {
         dfs(k,1,n);                 //从最后一个位置向前的dfs
         
         return res;
    }

    void dfs(int k,int start ,int n){      //n时当前和,每次减去一个枚举的数字直到正好为0
        if(!k){                           //如果k==0并且n==0时得到一个结果
            if(!n)res.push_back(path);

            return ;
        }

        for(int i=start;i<=10-k;i++){     //i应该满足i~9至少有k个数字即9-i+1>=k  即i<=10-k
            path.push_back(i);

            dfs(k-1,i+1,n-i);             //k-1,总和n-i

            path.pop_back();
        }
    }
};


52.N皇后 II

经典老番
这题没要求输出具体结果字符串,只统计数量
还是同样的按行枚举
col(n)–列,dg(2n)–正对角线,udg(2n)反对角线标记

在这里插入图片描述

class Solution {
public:
    int res;

    int n;

    vector<bool>col,dg,udg;

    int totalNQueens(int _n) {
        
        n=_n;

        col=vector<bool> (n);

        dg=udg=vector<bool> (2*n);

        dfs(0);

        return res;
    }

    void dfs(int u){
         if(u==n)res++;

         for(int i=0;i<n;i++){

             if(!col[i]&&!dg[i-u+n]&&!udg[i+u]){
                 col[i]=dg[i-u+n]=udg[i+u]=true;     //注意正对角线不要出现负数下标

                 dfs(u+1);

                 col[i]=dg[i-u+n]=udg[i+u]=false;
             }
         }
    }
};


37.解数独 **

思路:同n皇后相似,遍历所有点,枚举出所有可能性(改题只有唯一解)
关键在于如何表达本题的标记(行,列和单元格)
我们用row[9][9]表示9行9数(每一行19是否被用过),col[9][9]表示9列9数(每一列19是否被用过),cell[3][3][9]表示3行3列一共9个单元格(看做整体)每个格子有9个数是否被使用过

在这里插入图片描述

class Solution {
public:
    bool row[9][9]={0},col[9][9]={0},cell[3][3][9]={0};  //全部初始化为false

    void solveSudoku(vector<vector<char>>& board) {
          //初始化board(将已有元素标记)
          for(int i=0;i<9;i++){
              for(int j=0;j<9;j++){
                  if(board[i][j]!='.'){
                      int c=board[i][j]-'1';     //1~9字符转化为0~8的下标

                      row[i][c]=col[j][c]=cell[i/3][j/3][c]=true;   //行列分别除以3定位到当前单元格
                  }
              }
          }

          dfs(board,0,0);
    }

    bool dfs(vector<vector<char>>& board,int x,int y){
        if(y==9)y=0,x++;          //列枚举到头,转换到下一行

        if(x==9)return  true;     //行枚举完,跳出

        if(board[x][y]!='.')return dfs(board,x,y+1);  //如果已经填过数,枚举下一个数
        
        for(int i=0;i<9;i++){

            if(!row[x][i]&&!col[y][i]&&!cell[x/3][y/3][i]){
                board[x][y]=i+'1';

                row[x][i]=col[y][i]=cell[x/3][y/3][i]=true;

                if(dfs(board,x,y+1))return true;                   //添加if判断,便于在搜到正确答案时及时返回

                row[x][i]=col[y][i]=cell[x/3][y/3][i]=false;

                board[x][y]='.';
            }
        }

        return false;
    }
};

注解:对比八皇后和解数独 ?何时应添加if判断使其返回?
可以看到在同样的搜索方式下,解数独添加了返回条件,而八皇后则没有
因为八皇后可能存在多组解,在枚举到最后一层时会添加当前答案到res中
随后回溯(清除状态)返回到上面的某层重新枚举
而数独游戏只存在一组解,并且没有在搜索到最后一层时保存结果,此时添加if条件就会在搜索到最后一层时(只有正确答案能搜索到最后一层)返回,否则回溯就会覆盖当前正确答案

473.火柴拼正方形 ****(经典剪枝)

剪得很骚
顺序:枚举正方形的每一条边

在这里插入图片描述

为什么从大到小枚举?
假设木棒从大到小摆放的可能(分支)是3,5,7,8,9种,越大的木棒选择的位置数量越少,那么总共的分支数量就是3* 5* 7* 8* 9种
如果先枚举大边,那么剪枝时每一次砍掉的分支就是5 * 7 * 8 * 9种
而枚举小边每一次砍掉的数量一定小于大边一次性砍掉的 数量,所以先枚举大边要更快,而先枚举小边会最慢,随机枚举处于两者之间

在这里插入图片描述

class Solution {
public:
    vector<bool> st;

    bool makesquare(vector<int>& nums) {
        int n=nums.size();

        int sum=0;

        for(int i=0;i<n;i++)sum+=nums[i];

        if(!sum||sum%4)return false;         //总长不被4整除一定不行

        st=vector<bool> (n);            

        sort(nums.begin(),nums.end());

        reverse(nums.begin(),nums.end());     //从大到小枚举

        return dfs(nums,0,0,sum/4);
    }

    bool dfs(vector<int> &nums,int u,int cur,int length){
        if(cur==length)u++,cur=0;

        if(u==4)return true;

        for(int i=0;i<nums.size();i++){
            if(!st[i]&&cur+nums[i]<=length){  

             st[i]=true;

             if(dfs(nums,u,cur+nums[i],length))return true;

             st[i]=false;                                      //执行到回溯部分说明if(dfs)部分返回false,表示使用当前nums[i]不能拼凑一个完整的边,需要进行剪枝
             
             while(i<nums.size()-1&&nums[i]==nums[i+1])i++;       //当前nums[i]不行,和他相等的肯定也不行,跳过

             if(!cur)return false;                                //如果当前是枚举的第一个木棒,说明此种方案不行,重新枚举

             if(cur+nums[i]==length)return false;                 //如果当前枚举的是最后一个木棒,此种方案同样不行,重新枚举

            }
        }

        return false;
    }
};



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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值