递归、搜索与回溯——综合(上)

💁‍♂️个人主页:进击的荆棘

👇作者其它专栏:

《数据结构与算法》《算法》《C++起始之路》


目录

1.综合练习相关题解

2.floodfill算法

3.记忆化搜索


1.综合练习相关题解

1.1找出所有子集的异或总和再求和

算法思路:

所有子集可以解释为:每个元素选择在或不在一个集合中(因此,子集有2^n个)。本题需要求出所有子集,将它们的异或和相加。因为异或操作满足交换律,所以可以直接定义一个变量,直接记录当前状态的异或和。使用递归保存当前集合的状态(异或和),选择将当前元素添加至当前状态与否,并依次递归数组中下一个元素。当递归到空元素时,表示所有元素都被考虑到,记录当前状态(将当前状态的异或和添加至答案中)。

例:集合中元素为[1,2],则它的子集状态选择过程如下:

递归函数设计:void dfs(vector<int>& nums,int i)

参数:path(当前状态的异或和,i(当前需要处理的元素下标,处理过程:选择将其添加至当前状态或不进行操作);

返回值:无;

函数作用:选择对元素进行添加与否处理。

递归流程:

1.递归结束条件:当前下标与数组长度相等,即已经越界,表示已经考虑到所有元素;

a.将当前异或和添加至答案中,并返回;

2.考虑将当前元素添加至当前状态,当前状态更新为与当前元素值的异或和,然后递归下一个元素;

3.考虑不选择当前元素,当前状态不变,直接递归下一个元素;

cpp 复制代码
class Solution {
public:
    int ret=0;
    int path=0;
    int subsetXORSum(vector<int>& nums) {
        dfs(nums,0);
        return ret;
    }
    void dfs(vector<int>& nums,int i){
        ret+=path;
        for(i;i<nums.size();i++){
            path^=nums[i];
            dfs(nums,i+1);
            //恢复现场
            path^=nums[i];
        }
    }
};

1.2全排列 II

因为题目不要求返回的排列顺序,因此可以对初始状态排序,将所有相同的元素放在各自相邻的位置,方便之后操作。因为重复元素的存在,在选择元素进行全排列时,可能会存在重复排列,例:[1,2,1],所有的下标排列为:

1 123

2 132

3 213

4 231

5 312

6 321

按照以上下标进行排列的结果为:

1 121

2 112

3 211

4 211

5 112

6 121

可以看到,有效排列只有三种[1,1,2],[1,2,1],[2,1,1],其中每个排列都出现两次。因此,需要对相同元素定义一种规则,使得其组成的排列不会形成重复的情况:

1.可以将相同的元素按照排序后的下标顺序出现在排列中,通俗来讲,若元素s出现x次,则排序后的第2个元素s一定出现在第1个元素s后面,排序后的第3个元素s一定出现在第2个元素s后面,以此类推,此时的全排列一定不会出现重复结果。

2.例:a1=1,a2=1,a3=2,排列结果为[1,1,2]的情况只有一次,即a1在a2前面,因为a2不会出现在a1前面从而避免了重复排列。

3.在每一个位置上所考虑的可能情况并且不出现重复。

4.注意:若当前元素的前一个相同元素未出现在当前状态中,则当前元素也不能直接放入当前状态的数组,此做法可以保证相同元素的排列顺序与排序后的相同元素的顺序相同,即避免了重复排列出现。

5.通过深度优先搜索的方式,不断地枚举每个数在当前位置的可能性,并在递归结束时回溯到上一个状态,直到枚举完所有可能性,得到正确的结果。

递归函数设计:void dfs(vector<int>& nums)

返回值:无;

函数作用:查找所有合理的排列并存储在path列表中。

递归流程如下:

1.定义一个二维数组ret用来存放所有可能的排列,一个一维数组path用来存放每个状态的排列,一个一维数组check标记元素,然后从第一个位置开始进行递归;

2.递归结束条件:当path.size等于nums数组的长度时,说明已经处理完了所有数字,将当前数组存入结果中;

3.在每个递归状态中,枚举所欲下标i,若这个下标未被标记,并且在它之前的相同元素被标记过,则使用nums数组中当前下标的元素:

a.将check[i]标记为true;

b.将num[i]添加至path数组末尾;

c.将check[i]恢复现场,重新赋值为false,并删除path末尾元素表示回溯;

4.最后,返回ret。

cpp 复制代码
//两种思考方式
class Solution {
public:
    vector<vector<int>> ret;
    vector<int> path;
    bool check[9];
    vector<vector<int>> permuteUnique(vector<int>& nums) {
        sort(nums.begin(),nums.end());
        dfs(nums);
        return ret;
    }
    void dfs(vector<int>& nums){
        if(path.size()==nums.size()){
            ret.push_back(path);
            return ;
        }
        for(int i=0;i<nums.size();i++){
            //剪枝,只关注不合法的部分
            if(check[i]||(i!=0&&nums[i]==nums[i-1]&&!check[i-1]))
                continue;
            path.push_back(nums[i]);
            check[i]=true;
            dfs(nums);
            //回溯
            path.pop_back();
            check[i]=false;
            //剪枝,只关注合法的部分
            // if(!check[i]&&(i==0||nums[i]!=nums[i-1]||check[i-1])){
            //     path.push_back(nums[i]);
            //     check[i]=true;
            //     dfs(nums);
            //     //回溯
            //     path.pop_back();
            //     check[i]=false;
            // }
        }
    }
};

1.3电话号码的字母组合

算法思路:

每个位置可选择的字符与其它位置并不冲突,因此不需要标记已经出现的字符,只需要将每个数字对应的字符依次填入字符串中进行递归,在回溯时恢复现场即可。

●在递归之前我们需要定义一个字典hash,记录2~9各自对应的字符。

递归函数设计:void dfs(string& digits,int pos)

参数:pos(已经处理的元素个数)

返回值:无

函数作用:查找所有合理的字母组合并存储在path列表中。

递归函数流程如下:

1.递归结束条件:当pos等于digits的长度时,将path加入到ret中并返回;

2.取出当前处理的数字digit,根据phonMap取出对应的字母列表;

3.遍历hash,将当前字母加入到组合字符串path的末尾,然后递归处理下一个数字(传入pos+1,表示处理下一个数字);

4.递归处理结束后,将加入的字母从path末尾删除,表示回溯。

5.最终返回ret即可。

cpp 复制代码
class Solution {
    string hash[10]={"","","abc","def","ghi","jkl","mno","pqrs","tuv","wxyz"};
    vector<string> ret;
    string path;
public:
    vector<string> letterCombinations(string digits) {
        if(digits.size()==0) return ret;
        dfs(digits,0);
        return ret;
    }
    void dfs(string& digits,int pos){
        if(pos==digits.size()){
            ret.push_back(path);
            return ;
        }
        for(auto ch:hash[digits[pos]-'0']){
            path.push_back(ch);
            dfs(digits,pos+1);
            //恢复现场
            path.pop_back();
        }
    }
};

1.4括号生成

算法思路:

从左往右进行递归,在每个位置判断放置左右括号的可能性,若此时放置左括号,则放置左括号继续进行递归,右括号同理。

一种判断括号是否合法的方法:从左往右遍历,左括号的数量始终大于等于右括号的数量,并且左括号的总数量与左括号的总数量相等。因此在递归时需要进行以下判断:

1.放入左括号时需判断此时左括号数量是否小于字符串总长度的一半(若左括号的数量大于等于字符串长度的一半时继续放置左括号,则左括号的总数量一定大于右括号的总数量);

2.放入右括号时需判断此时右括号数量是否小于左括号数量。

递归函数设计:void dfs(int& n,int l,int r)

参数:l(当前状态的字符串中的左括号的数量),r(当前状态的字符串中的右括号的数量);

返回值:无;

函数作用:查找所有合理的括号序列并存储在path列表中。

递归函数参数设置为当前状态的左括号数量以及当前状态的右括号数量,递归流程如下:

1.递归结束条件:当前状态字符串左括号数量与n相等,记录当前状态并返回;

2.若此时左括号数量小于n,则在当前状态的字符串末尾添加左括号并继续递归,递归借宿撤销添加操作;

3.若此时右括号数量小于左括号数量,则在当前状态的字符串末尾添加右括号并递归,递归结束撤销添加操作;

cpp 复制代码
class Solution {
    vector<string> ret;
    string path;
public:
    vector<string> generateParenthesis(int n) {
        dfs(n,0,0);
        return ret;
    }
    void dfs(int& n,int l,int r){
        //因为先加的左括号,后加的右括号,当右括号数量==n时,满足条件
        if(r==n){
            ret.push_back(path);
            return ;
        }
        if(l<n){
            path+='(';
            dfs(n,l+1,r);
            path.pop_back();
        }
        if(l>r){
            path+=')';
            dfs(n,l,r+1);
            path.pop_back();
        }
    }
};

1.5组合

算法思路(回溯):

题目要求从1到n中选择k个数的所有组合,其中不考虑顺序。也就是说,[1,2]和[2,1]等价。需要找出所有的组合,但不能重复计算相同元素的不同顺序的组合。对于选择组合,需要进行如下流程:

1.所有元素分别作为首位元素进行处理;

2.在之后的位置上同理,选择所有元素分别作为当前位置元素进行处理;

3.为避免计算重复组合,规定选择之后位置的元素时必须比前一个元素大,这样就不会有重复的组合([1,2]和[2,1]中[2,1]不会出现)。

递归函数设计:void dfs(int n,int k,int pos)

参数:pos(当前需要进行处理的位置);

返回值:无;

函数作用:某个元素作为首为元素出现时,查找所有可能的组合。

具体实现方法:

1.定义一个二维数组和一维数组。二维数组用来记录所有组合,一维数组用来记录当前状态下的组合。

2.遍历1到n-k+1,已当前数作为组合的首位元素进行递归(从n-k+1到n作为首位元素时,组合中一定不会存在k个元素)。

3.递归函数的参数为当前步骤以及n和k。递归流程如下:

a.结束条件:当前组合中已经有k个元素,将当前组合存进二维数组并返回。

●剪枝:若当前位置之后的所有元素放入组合也不能满足组合中存在k个元素,直接返回。

b.从当前位置的下一个元素开始遍历到n,将元素赋值到当前位置,递归到下一个元素。

cpp 复制代码
class Solution {
    vector<vector<int>> ret;
    vector<int> path;
public:
    vector<vector<int>> combine(int n, int k) {
        dfs(n,k,1);
        return ret;
    }
    void dfs(int n,int k,int pos){
        if(path.size()==k){
            ret.push_back(path);
            return ;
        }
        for(int i=pos;i<=n;i++){
            path.push_back(i);
            dfs(n,k,i+1);
            //恢复现场
            path.pop_back();
        }
    }
};

1.6目标和

算法思路(回溯):

对于每个数,可以选择加上或减去它,依次枚举每一个数字,在每个数都被选择时检查得到的和是否等于目标值。若等于,则记录结果。

需要注意的是,为了优化时间复杂度,可以提前计算出数组中的所有数字的和sum,以及数组长度len(这里用pos代替与nums长度做比较即可)。这样可以快速判断当前的和减去剩余的所有数是否已经超过了目标值target,或当前的和加上剩下的数的和是否小于target,若满足条件,则直接回溯。

递归流程:

1.递归结束条件:pos与数组长度相等判断当前状态的sum是否与目标值相等,若是计数加1;

2.选择当前元素进行加操作,递归下一个位置,并更新参数sum;

3.选择当前元素进行减操作,递归下一个位置,并更新参数sum;

●特别地,此问题可以转化为另一个问题:若所有元素初始状态均为减,选择其中几个元素将它们的状态修改为加,计算修改后的元素和与目标值相等的方案个数。

1.选择其中x个元素进行修改,并且这x个元素的和为y;

2.检查使得-sum+2*y=target(移项:y=(sum+target)/2)成立的方案个数,即选择x个元素和为(sum+target)/2的方案个数;

a.若sum+target为奇数,则不存在这种方案;

3.递归流程:

a.传入参数:pos(当前要处理的元素下标),sum(当前状态和),nums(元素数组),aim(目标值:(sum+target)/2);

b.递归结束条件:pos与数组长度相等,判断当前sum是否与目标值相等,若是返回1,否则返回0;

c.返回递归选择当前元素以及递归不选择当前元素函数值的和。

cpp 复制代码
//将sum搞成全局,相较于形参,效率略低
class Solution {
    int ret=0;
public:
    int findTargetSumWays(vector<int>& nums, int target) {
        dfs(nums,target,0,0);
        return ret;
    }
    void dfs(vector<int>& nums,int target,int pos,int sum){
        if(pos==nums.size()){
            if(sum==target){
                ret++;
            }
            return ;
        }
        
        dfs(nums,target,pos+1,sum+nums[pos]);

        dfs(nums,target,pos+1,sum-nums[pos]);
    }
};

1.7组合总和

算法思路:

candidates的所有元素互不相同,因此在递归状态时只需要对每个元素进行如下判断:

1.跳过,对下一个元素进行判断;

2.将其添加至当前状态中,在选择添加当前元素时,之后仍可以继续选择当前元素(可以重复选择同一元素)。

●因此,在选择当前元素并向下传递下标时,应直接传递当前元素下标。

递归函数设计:void dfs(vector<int>& candidates,int target,int pos)

参数:target(用全局sum与target作比较),pos(当前需要处理的元素下标);

返回值:无;

函数作用:向下传递两个状态(跳过或选择当前元素),找出所有组合使得元素和为目标值。

递归函数流程:

1.结束条件:

a.当前需要处理的元素下标越界;

b.当前状态的元素和大于目标值;

c.当前状态的元素与目标值相同;

2.跳过当前元素,当前状态不变,对下一个元素进行处理;

3.选择将当前元素添加至当前状态,并保留状态继续对当前元素进行处理,递归结束时撤销添加操作。

cpp 复制代码
//法一:按数组中元素位置枚举
class Solution {
    vector<vector<int>> ret;
    vector<int> path;
    int sum=0;
public:
    vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
        dfs(candidates,target,0);
        return ret;
    }
    void dfs(vector<int>& candidates,int target,int pos){
        if(pos>=candidates.size()||sum>target) return ;
        if(sum==target){
            ret.push_back(path);
            return ;
        }
        for(int i=pos;i<candidates.size();i++){
            path.push_back(candidates[i]);
            sum+=candidates[i];
            dfs(candidates,target,i);
            path.pop_back();
            sum-=candidates[i];
        }
    }
};
//法二:按数组中元素个数
class Solution {
    vector<vector<int>> ret;
    vector<int> path;
    int aim=0;
public:
    vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
        aim=target;
        dfs(candidates,0,0);
        return ret;
    }
    void dfs(vector<int>& candidates,int pos,int sum){
        if(sum==aim){
            ret.push_back(path);
            return ;
        }
        if(sum>aim||pos>=candidates.size()) return ;
        //for(int i=0;i*candidates[pos]<=aim;i++){
        //优化
        for(int i=0;i*candidates[pos]+sum<=aim;i++){
            if(i>0) path.push_back(candidates[pos]);
            dfs(candidates,pos+1,sum+i*candidates[pos]);
        }
        //恢复现场
        //for(int i=0;i*candidates[pos]+sum<=aim;i++){
        //优化
        for(int i=1;i*candidates[pos]+sum<=aim;i++){
            path.pop_back();
        }
    }
};

1.8字母大小写全排列

算法思路:

只需要对英文字母进行处理,处理每个元素时存在三种情况:

1.不进行处理;

2.若当前字母是英文字母并是大写,将其修改为小写;

3.若当前字母是英文字母并是小写,将其修改为大写;

递归函数设计:void dfs(string& s,int pos)

参数:pos(当前需要处理的位置);

返回值:无;

函数作用:查找所有可能的字符串集合,并将其记录在path列表。

从前向后按序进行递归,递归流程:

1.递归结束条件:当前需要处理的元素下标越界,表示处理完毕,记录当前状态并返回;

2.对当前元素不进行任何处理,直接递归下一位元素;

3.判断当前元素是否为小写字母,若是,将其修改为大写字母并递归下一个元素,递归结束时回溯现场;

4.判断当前元素是否为大写字母,若是,将其修改为小写字母并递归下一个元素,递归结束时回溯现场;

cpp 复制代码
class Solution {
    vector<string> ret;
    string path;
public:
    vector<string> letterCasePermutation(string s) {
        dfs(s,0);
        return ret;
    }
    void dfs(string& s,int pos){
        if(path.size()==s.size()){
            ret.push_back(path);
            return ;
        }
        //不改变
       path+=s[pos];
       dfs(s,pos+1);
       path.pop_back();//恢复现场
       //改变
       if(s[pos]<'0'||s[pos]>'9'){
            path+=change(s[pos]);
            dfs(s,pos+1);
            path.pop_back();
       }
    }
    char change(char ch){
        if(ch>='a'&&ch<='z') return ch-32;
        else return ch+32;
    }
};

1.9优美的排列

算法思路:

需要在每个位置上考虑所有的可能情况并且不能出现重复。通过深度优先搜索的方式,不断地枚举每个数在当前位置的可能性,并回溯到上一个状态,直到枚举完所有可能性,得到正确的结果。

需要定义一个变量来记录所有可能的排列数量,一个一维数组check标记元素,然后从第一个位置开始进行递归;

递归函数设计:void dfs(int pos,int n)

参数:pos(当前需要处理的位置);

返回值:无;

函数作用:在当前位置填入一个合理的数字,查找所有满足条件的排列。

递归流程:

1.递归结束条件:当pos等于n时,说明已经处理完了所有数字,将当前数组存入结果中;

2.在每个递归状态中,枚举所有下标x,若这个下标未被标记,并且满足题目条件之一:

a.将check[i]标记为true;

b.对第pos+1个位置进行递归;

c.将check[i]重新赋值为0,表示回溯;

cpp 复制代码
class Solution {
    int ret=0;
    bool check[16];
public:
    int countArrangement(int n) {
        dfs(1,n);
        return ret;
    }
    void dfs(int pos,int n){
        if(pos==n+1){ 
            ret++;
            return ;
        }
        for(int i=1;i<=n;i++){
            if(!check[i]&&(pos%i==0||i%pos==0)){
                check[i]=true;
                dfs(pos+1,n);
                check[i]=false;//恢复现场
            }
        }
    }
};

1.10N 皇后

算法思路:

首先,我们在第一行放置第一个皇后,然后遍历棋盘的第二行,在可行的位置放置第二个皇后,然后再遍历第三行,再可型的位置放置第三个皇后,以此类推,直到放置了n个皇后为止。

需要用一个数组来记录每一行放置的皇后的列数。在每一行中,尝试放置一个皇后,并检查是否会和前面已经放置的皇后冲突。若没有冲突,继续递归地放置下一行的皇后,直到所有的皇后都放置完毕,然后把这个方案记录下来。

在检查皇后是否冲突是,可以用一个数组来记录每一列是否已经放置了皇后,并检查当前要放置的皇后是否会和已经放置的皇后冲突。对于对角线,可以用两个数组来记录从左上角到右下角的每一条对角线是否已经放置了皇后,以及从右上角到左下角的每一条对角线是否已经放置了皇后。

●对于对角线是否冲突的判断可以通过以下流程解决:

1.从左上到右下:相同对角线的行列之差相同;

2.从右上到左下:相同对角线的行列之和相同。

因此,需要创建用于存储解决方案的二维数组字符串数组ret,用于存储每个皇后的为止的一维整数数组path,以及用于记录每一列和对角线上是否已经有皇后的bool数组checkCol,checkDig1,checkDig2。

递归函数设计:void dfs(int n,int row)

参数:row(当前需要处理的行数);

返回值:无;

函数作用:在当前行放入一个不发生冲突的皇后,查找所有可行的方案使得放置n个皇后不发生冲突。

函数递归流程:

1.结束条件:若row等于n,则表示已经找到一组解决方案,此时将path存储到字符串数组ret中,并返回;

2.枚举当前行的每一列,判断该列、两个对角线上是否已经有皇后:

a.若有皇后,则继续枚举下一行;

b.否则,在该位置放置皇后,并将checkCol,checkDig1,checkDig2对应的位置设置为true,表示该列和对角线上已经有皇后;

i.递归调用dfs函数,搜索下一行皇后的位置。若该方案递归结束,则在回溯时需要将checkCol,checkDig1,checkDig2对应的位置设为false,然后继续枚举下一列;

cpp 复制代码
class Solution {
    vector<string> path;
    vector<vector<string>> ret;
    //分别检查列,主对角线,副对角线,行不用检查,因为一定不可能有同行的
    bool checkCol[10],checkDig1[20],checkDig2[20];
public:
    vector<vector<string>> solveNQueens(int n) {
        path.resize(n);
        for(int i=0;i<n;i++)
            path[i].append(n,'.');
        dfs(n,0);
        return ret;
    }
    //对每行枚举
    void dfs(int n,int row){
        if(row==n){
            ret.push_back(path);
            return ;
        }
        //对列枚举
        for(int col=0;col<n;col++){
            //剪枝
            if(!checkCol[col]&&!checkDig1[col-row+n]&&!checkDig2[col+row]){
                path[row][col]='Q';
                checkCol[col]=checkDig1[col-row+n]=checkDig2[col+row]=true;
                dfs(n,row+1);
                //恢复现场
                path[row][col]='.';
                checkCol[col]=checkDig1[col-row+n]=checkDig2[col+row]=false;
            }
        }
    }
};

1.11有效的数独

算法思路:

创建三个数组标记行、列以及3*3小方格中是否出现1~9之间的数字即可。

cpp 复制代码
class Solution {
    bool row[9][10];
    bool col[9][10];
    bool grid[3][3][10];//将数独划分为9块
public:
    bool isValidSudoku(vector<vector<char>>& board) {
        for(int i=0;i<9;i++){
            for(int j=0;j<9;j++){
                if(board[i][j]!='.'){
                    int num=board[i][j]-'0';
                    //是否已出现
                    if(row[i][num]||col[j][num]||grid[i/3][j/3][num])
                        return false;
                    row[i][num]=col[j][num]=grid[i/3][j/3][num]=true;
                }
            }
        }
        return true;
    }
};

1.12解数独

算法思路:

为了存储每个位置的元素,需要定义一个二维数组。首先,记录所有已知的数据,然后遍历所有需要处理的位置,并遍历数字1~9。对于每个位置,检查该数字是否可以存放在该位置,同时检查行、列和九宫格是否唯一。

可以使用一个二维数组来记录每个数字在每一行中是否出现,一个二维数组来记录每个数字在每一列中是否出现。对于九宫格,可以以行和列除以3得到的商作为九宫格的坐标,并使用一个三位数组来记录每个数字在每一个九宫格中是否出现。在检查是否存在冲突时,只需检查行、列和九宫格里对应的数字是否已被标记。若数字至少有一个位置(行、列、九宫格)被标记,则存在冲突,因此不能在该位置放置当前数字。

●在本题中,需要直接修改给出的数组,因此在找到一种可行的方法时,应停止递归,以防正确的方法被覆盖。

初始化定义:

1.定义行、列、九宫格标记数组以及找到可行方法的标记方法,将它们初始化为false。

2.定义一个数组来存储每个需要处理的位置。

3.将题目给出的所有元素的行、列以及九宫格坐标标记为true。

4.将所有需要处理的位置存入数组。

递归函数设计:void dfs(vector<vector<char>>& board)

返回值:无;

函数作用:查找数独答案。

递归流程:

1.结束条件:已经处理完所有需要处理的元素。若找到可行的解决方案,则将标记变量更新为true并返回。

2.获取当前需要处理的元素的行列值。

3.遍历数字1~9。若当前数字可以填入当前位置,并标记遍历未被赋值为true,则将当前位置的行、列以及九宫格坐标标记为true,将当前数字赋值给board数组中的相应位置元素,然后对下一个位置进行递归。

4.递归结束,撤回标记。

cpp 复制代码
class Solution {
    bool row[9][10];
    bool col[9][10];
    bool grid[3][3][10];
public:
    void solveSudoku(vector<vector<char>>& board) {
        for(int i=0;i<9;i++){
            for(int j=0;j<9;j++){
                int num=board[i][j]-'0';
                if(board[i][j]!='.'){
                    row[i][num]=col[j][num]=grid[i/3][j/3][num]=true;
                }
            }
        }
        dfs(board);
    }
    bool dfs(vector<vector<char>>& board){
        for(int i=0;i<9;i++){
            for(int j=0;j<9;j++){
                if(board[i][j]=='.'){
                    for(int k=1;k<=9;k++){
                        if(!row[i][k]&&!col[j][k]&&!grid[i/3][j/3][k]){
                            board[i][j]=k+'0';
                            row[i][k]=col[j][k]=grid[i/3][j/3][k]=true;
                            if(dfs(board)) return true;
                            //恢复现场
                            board[i][j]='.';
                            row[i][k]=col[j][k]=grid[i/3][j/3][k]=false;
                        }
                    }
                    return false;
                }
            }
        }
        return true;
    }
};

1.13单词搜索

算法思路:

需要假设每个位置的元素作为第一个字母,然后向相邻的四个方向进行递归,并且不能出现重复使用同一个位置的元素。通过深度优先搜索的方式,不断地枚举相邻元素作为下一个字母出现的可能性,并在递归结束时回溯,直到枚举完所有可能性,得到正确的结果。

递归函数设计:bool dfs(vector<vector<char>>& board,int i,int j,string& word,int pos)

参数:i(当前需要进行处理的元素横坐标),j(当前需要进行处理的元素纵坐标),pos(当前已经处理的元素个数);

返回值:当前坐标元素作为字符串中下标pos的元素是否可以找到成立的字符串。

函数作用:判断当前坐标的元素作为字符串中pos的元素出现时,向四个方向传递,查找是否存在路径结果与字符串相同。

递归函数流程:

1.遍历每个位置,标记当前位置并将当前位置的字母作为首字母进行递归,并且在回溯时撤回标记。

2.在每个递归的状态中,维护一个步数pos,表示当前已经处理了几个字母。

●若当前位置的字母与字符串中的第pos个字母不相等,则返回false。

●若当前pos的值与字符串长度相等,表示存在一种路径使得word成立,返回true

3.对当前位置的上下左右四个相邻位置进行递归,若递归结果为true,则返回true。

4.若相邻的四个位置的递归结果都为false,则返回false。

●若使用将当前遍历到的字符赋值为空格,并在回溯时恢复为原来的字母的方法,则在递归时不会重复遍历当前元素,可达到不使用标记数组的目的。

cpp 复制代码
class Solution {
    bool grid[6][6];
public:
    bool exist(vector<vector<char>>& board, string word) {
        for(int i=0;i<board.size();i++){
            for(int j=0;j<board[0].size();j++){
                if(board[i][j]==word[0]){
                    grid[i][j]=true;
                    if(dfs(board,i,j,word,1)) return true;
                    //恢复现场
                    grid[i][j]=false;
                }
            }
        }
        return false;
    }
    //在某一位置的偏移量,上下左右
    int dx[4]={0,0,1,-1};
    int dy[4]={1,-1,0,0};
    bool dfs(vector<vector<char>>& board,int i,int j,string& word,int pos){
        //递归出口
        if(pos==word.size()) return true;
        //对四个方向进行判断
        for(int k=0;k<4;k++){
            int x=i+dx[k],y=j+dy[k];
            if(0<=x&&x<board.size()&&y>=0&&y<board[0].size()&&!grid[x][y]&&board[x][y]==word[pos]){
                grid[x][y]=true;
                if(dfs(board,x,y,word,pos+1)) return true;
                //恢复现场
                grid[x][y]=false;
            }
        }
        return false;
    }
};

1.14黄金矿工

算法思路:

枚举矩阵中所有的位置当成起点,来一次深度优先遍历,统计出所有情况下能收集到的黄金数的最大值即可。

cpp 复制代码
class Solution {
    bool check[15][15];
    int ret=0;
public:
    int getMaximumGold(vector<vector<int>>& grid) {
        for(int i=0;i<grid.size();i++){
            for(int j=0;j<grid[0].size();j++){
                if(grid[i][j]!=0){
                    check[i][j]=true;
                    dfs(grid,i,j,grid[i][j]);
                    check[i][j]=false;
                }
            }
        }
        return ret;
    }
    int dx[4]={0,0,1,-1};
    int dy[4]={1,-1,0,0};
    void dfs(vector<vector<int>>& grid,int i,int j,int cnt){
        ret=max(ret,cnt);
        
        for(int k=0;k<4;k++){
            int x=i+dx[k],y=j+dy[k];
            if(x>=0&&x<grid.size()&&y>=0&&y<grid[0].size()&&!check[x][y]&&grid[x][y]!=0){
                check[x][y]=true;
                dfs(grid,x,y,cnt+grid[x][y]);
                //恢复现场
                check[x][y]=false;
            }
        }
    }
};

1.15不同路径 III

算法思路:

对于四个方向,可以定义两个一维数组dx,dy,大小为4,两数组中相对应的位置代表坐标偏移量。用一变量step表示所有无障碍方格的个数。题目要求到达目标位置时所有无障碍方格都存在路径中,可以定义一个变量记录cnt当前状态中为走过的无障碍方格个数,则当我们走到目标地点时只需判断cnt是否与step相等即可。在移动时需判断是否越界。

递归函数设计:void dfs(vector<vector<int>>& grid,int i,int j,int cnt)

参数:i,j(当前需要处理元素的坐标),cnt(当前走过的无障碍方格个数);

返回值:无;

函数作用:判断当前位置的四个方向是否可以添加至当前状态,查找在满足条件下从起始方格到结束方格的不同路径的数目。

递归流程:

1.递归结束条件:当前位置元素值为2,且此时cnt值与step值相同,则cnt++;

2.遍历四个方向,若移动后为越界,无障碍并且未被标记,则标记当前位置,并递归移动后的位置,在回溯时撤销标记操作。

cpp 复制代码
class Solution {
    bool check[21][21];
    int ret=0,step;
public:
    int uniquePathsIII(vector<vector<int>>& grid) {
        int bx=0,by=0;
        //查找0的个数
        for(int i=0;i<grid.size();i++){
            for(int j=0;j<grid[0].size();j++){
                if(grid[i][j]==0)
                    step++;
                else if(grid[i][j]==1){
                    bx=i,by=j;
                }
            }
        }
        step+=2;
        check[bx][by]=true;
        dfs(grid,bx,by,1);                    
        return ret;
    }
    int dx[4]={1,-1,0,0};
    int dy[4]={0,0,1,-1};
    void dfs(vector<vector<int>>& grid,int i,int j,int cnt){
        if(grid[i][j]==2){
            if(cnt==step)
                ret++;
            return ;
        }
        for(int k=0;k<4;k++){
            int x=i+dx[k],y=j+dy[k];
            if(x>=0&&x<grid.size()&&y>=0&&y<grid[0].size()&&grid[x][y]!=-1&&!check[x][y]){
                check[x][y]=true;
                dfs(grid,x,y,cnt+1);
                check[x][y]=false;
            }
        }
    }
};
相关推荐
水云桐程序员8 小时前
C++可以写手机应用吗
开发语言·c++·智能手机
平凡但不平庸的码农8 小时前
Go Slice 详解
算法·golang
Jasmine_llq12 小时前
《B3867 [GESP202309 三级] 小杨的储蓄》
算法·循环遍历·数组累加(模拟)·索引定位·顺序输出
啦啦啦_999912 小时前
案例之 逻辑回归_电信用户流失预测
算法·机器学习·逻辑回归
风筝在晴天搁浅12 小时前
快手/字节 CodeTop LeetCode 415.字符串相加
算法·leetcode
小黄人软件12 小时前
C++读写编辑CSV文件示例源码 用于数据导入导出,比Excel好使
开发语言·c++·excel
郭涤生12 小时前
C++各个版本的性能和安全性总结
开发语言·c++
DragonnAi13 小时前
猫咪如厕检测与分类识别系统系列【十四】 项目结构重新整理-即将开源完整算法
算法·开源
机器视觉_Explorer13 小时前
【halcon】编程技巧:鼠标擦除
图像处理·人工智能·深度学习·算法·视觉检测