算法学习(十六)—— 综合练习

目录

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

47. 全排列 Ⅱ

17. 电话号码的字母组合

22. 括号生成

77. 组合

494. 目标和

39. 组合总和

784. 字母大小写全排列

526. 优美的排列

51. N皇后

36. 有效的数独

37. 解数独

79. 单词搜索

1219. 黄金矿工

980. 不同路径 Ⅲ


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

1863. 找出所有子集的异或总和再求和 - 力扣(LeetCode)

 题目意思和我们之前的求子集很相似,只是这里多了几步,就是要把每个子集的结果都异或一遍,然后把所有值相加,下面来分析下这道题:

  • 还是老样子,步骤为:①先画决策树    ②再设计代码:全局变量,dfs函数体,回溯,剪枝以及递归出口
  • 先画决策树,如下图:
  • 首先是全局变量,int sum,作用是保存所有子集异或后相加的结果;int path,记录的是当前这一层两个数异或的结果,就是不再存1和2这两个数,直接存它们异或的结果
  • 然后就是dfs函数头的设计,和之前是一样的,dfs(nums, pos),pos 表示本次递归要从哪个数开始枚举
  • 然后就是回溯,这个我们可以利用一下异或运算的“消消乐”,就是1异或2,然后再异或一个2,之后的结果仍然是1,2的两次异或相互抵消,所以回溯的时候再异或一下即可
  • 最后就是递归出口,就是每次进入递归的时候,都要 sum += path
class Solution {
public:
    int path;
    int sum;
    int subsetXORSum(vector<int>& nums) 
    {
        dfs(nums, 0);
        return sum;    
    }
    void dfs(vector<int>& nums, int pos)
    {
        sum += path;
        for(int i = pos; i < nums.size(); i++)
        {
            path ^= nums[i];
            dfs(nums, i + 1);
            path ^= nums[i]; //恢复现场
        }
    }
};

47. 全排列 Ⅱ

47. 全排列 II - 力扣(LeetCode)

全排列我们在上一篇文章中已经讲过大体思路了:算法学习(十五)—— 回溯算法-CSDN博客

这道题只是多了“有重复元素”这一点,大逻辑是一样的,所以这里我们重点搞一下剪枝,其它的就不再赘述,下面来分析下这道题:

  • 这道题题目给我们的数组中是有重复元素的,所以全排列后的结果可能会有重复,所以题目就是要求我们干掉重复的全排列,返回不重复的结果;说到“干掉”,最先想到的就是“剪枝”操作,所以这道题的重点就是在“剪枝”上
  • 先画决策树,如下图:

所以在这道题中,我们的剪枝只会发生在两种情况:

  1. 同一个节点的所有分支中,相同的元素只能选择一次
  2. 同一个数只能用一次,用check数组来搞

然后就是如何用代码实现剪枝,其实和前面一样,就是加几个if判断即可,对于剪枝实现,我们有下面两种思考方式:

  1. 只关心“不合法”的分支:就是在递归之前坐下判断,如果这个分支不合法,就直接返回不再递归。当 check[i] == true || nums[i] == nums[i - 1] && check[i - 1] = false 时,就是该数已经使用过或者和前面一个数重复了,所以不合法,至于后面的与操作,以上面决策树的 "1 1 __ __" 为例,这个结果里的两个 1 其实是在不同层次的,第一个1是上一层的,第二个1是本层的,所以两个层次的不做比较;还有一个问题,当i == 0时,再 i - 1就越界了,所以还要加一个条件,最后的判断条件就是:check[i] == true || (i != 0 && nums[i] == nums[i - 1] && check[i - 1] == false) 
  2. 只关心“合法”的分支:就是当选择条件成立时再进入递归。check[i] == false,首先是必须要这个位置的值未被使用,才有机会进入递归;i == 0 || nums[i] != nums[i - 1] || check[i - 1] == true,就是两个数不一样,或者两个数一样但是处于不同层时,可以进递归;最后的判断条件就是check[i] == false && (i == 0 || nums[i] != nums[i - 1] || check[i - 1] == true 

 细节处理:

  • 如果题目给我们的是[1, 2, 1, 3, 1] 这样的数组,那么当我们递归到第二个1时,还要去前面看第一个1有没有用过,就很麻烦,所以,我们可以在最开始把数组排序一下
class Solution {
public:
    vector<vector<int>> vv;
    vector<int> path;
    bool check[9];

    vector<vector<int>> permuteUnique(vector<int>& nums) 
    {
        sort(nums.begin(), nums.end());
        dfs(nums, 0);
        return vv;
    }

    void dfs(vector<int>& nums, int pos)
    {
        if(pos == nums.size())
        {
            vv.push_back(path);
            return;
        }

        for(int i = 0; i < nums.size(); i++)
        {
            //思路一:只考虑不合法分支
            if(check[i] == true || (i != 0 && nums[i] == nums[i - 1] && check[i - 1] == false)) 
                continue;
            path.push_back(nums[i]);
            check[i] = true; //表明该位置已被使用

            dfs(nums, pos + 1);
            path.pop_back(); //恢复现场
            check[i] = false;

            //思路二:只考虑合法分支
            // if(check[i] == false && (i == 0 || nums[i] != nums[i - 1] || check[i - 1] == true))
            // {
            //     path.push_back(nums[i]);
            //     check[i] = true; //表明该位置已被使用

            //     dfs(nums, pos + 1);
            //     path.pop_back(); //恢复现场
            //     check[i] = false;
            // }
        }
    }
};

17. 电话号码的字母组合

17. 电话号码的字母组合 - 力扣(LeetCode)

这道题很简单,看示例1并参照图片很容易解决,但是,当给我们的数字只有两个的时候,我们可以用两层for循环解决,但是,当给我们的数字是9位数甚至更大时,再用循环嵌套就不太实际了,下面我们来分析下这道题:

  • 这道题还是用递归来搞的,先画决策树,如下图:
  • 然后是全局变量的设计,首先搞一个字符串数组,用来解决数组与字符串的映射关系
  • 然后是一个path,保存每个路径的值;最后就是ret,用来保存最终结果
  • 然后就是 dfs函数体的设计:dfs(digits, pos),就是根据题目给的数字翻译成字符串,然后pos代表字符串的具体某个位置
  • 最后就是回溯过程,就是把path的最后一个字符干掉即可
  • 最后就是递归出口,就是当递归到叶子节点时,保存path的结果即可
class Solution {
public:
    vector<string> ret;
    string path;
    string hash[10] = {"", "", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz"};

    vector<string> letterCombinations(string digits) 
    {
        if(digits.size() == 0) return ret;
        dfs(digits, 0); //从0开始翻译到最后一个位置
        return ret;
    }
    void dfs(const string& s, int pos)
    {
        if(pos == s.size())
        {
            ret.push_back(path);
            return;
        }

        //把该字符串所有情况翻译一下,加到path后面即可
        for(auto e : hash[s[pos] - '0']) //把字符转为数字,并找到对应的字符串
        {
            path.push_back(e);
            dfs(s, pos + 1);
            path.pop_back();
        }
    }
};

22. 括号生成

22. 括号生成 - 力扣(LeetCode)

题目很简单,就是给我们一个数字,要我们找到相同数量的有效括号的集合并返回,下面来分析下这道题:

  • 先来分析下什么是“有效括号的组合”:①左括号数量 = 右括号数量    ②从头开始的任意一个字串,左括号的数量 >= 右括号的数量;只要上面有一个点点不合法,那么就不是“有效括号的组合”
  • 我们要列举出括号,假设 n = 3,那么只需要6个空,然后往这6个空中暴力枚举符合要求的括号即可,先画决策树,如下图:
  • 首先是全局变量,left表示左括号的数量,right表示右括号数量,n表示括号的对数;然后就是path表示递归的路径
  • 然后就是dfs,不需要参数,因为参数已经在全局里设置好了;然后就是dfs干的事,其实也很简单,当左括号合法时,把左括号加到path里,右括号同理,这样就可以了
  • 回溯时,一样的,干掉path的最后一个
  • 递归出口,就是当right = n也就是右括号为n的时候,说明左括号和右括号全搞好了,就是到了叶子节点,直接返回即可
class Solution {
public:
    int left, right;
    vector<string> ret;
    string path;
    vector<string> generateParenthesis(int n) 
    {
        dfs(n);
        return ret;
    }
    void dfs(int n)
    {
        if(right == n) 
        {
            ret.push_back(path);
            return;
        }
        if(left < n) //先加左括号
        {
            path += "(";
            left++;

            dfs(n);
            
            path.pop_back(); //恢复现场
            left--;
        }
        if(right < left) //再加右括号
        {
            path += ")";
            right++;
            dfs(n);
            path.pop_back();
            right--;
        }
    }
};

77. 组合

77. 组合 - 力扣(LeetCode)

这种排列类型的题都是很相似的,所以我们的解题思路和决策树也都很相似,下面来分析下这道题:

  • 先画决策树,如下图:
  • 枚举的时候,只需要从该数的下一个数开始,比如 1 往后枚举是从 2 开始,2 往后枚举是从 3 开始,所以并不需要全局变量来辅助剪枝,只要控制下递归参数即可:dfs(pos),表示接下来的操作从哪个位置开始
  • 然后需要 path 记录路径,然后回溯时,把最后的数干掉即可,当碰到叶子节点时就可返回结果
class Solution {
public:
    vector<vector<int>> ret;
    vector<int> path;
    int N, K;
    vector<vector<int>> combine(int n, int k) 
    {
        N = n, K = k;
        dfs(1);
        return ret;
    }

    void dfs(int pos)
    {
        if(path.size() == K)
        {
            ret.push_back(path);
            return;
        }
        //从起始位置往后枚举
        for(int i = pos; i <= N; i++)
        {
            path.push_back(i);
            dfs(i + 1); //从添加的这个数的下一个位置开始
            path.pop_back();
        }
    }
};

494. 目标和

494. 目标和 - 力扣(LeetCode)

这道题其实我们小学时就见过,就是给你一个正数数组,给里面每个数都添加加号减号并能使式子的结果 == target,要我们返回可以通过这种方法构造运算结果 == target 的不同表达式的数目,下面来分析下这道题:

  • 先画决策树,如下图:
  • 所以这道题其实很简单,所以我们来搞两种形式的代码:①path 是全局变量时候的代码    ②path 作为参数时的代码
  • 代表性题目就是我们之前搞得子集那道题:算法学习(十五)—— 回溯算法-CSDN博客

代码一,就是当path为全局变量的时候:

class Solution 
{
    int path, ret;
    int a;
public:
    int findTargetSumWays(vector<int>& nums, int target) 
    {
        a = target;
        dfs(nums, 0);
        return ret;
    }

    void dfs(vector<int>& nums, int pos)
    {
        if(pos == nums.size())
        {
            //当数组的数已经枚举完并且path已经等于目标值,说明已经找到一种结果
            if(path == a) ret++; 
            return;
        }

        //加法
        path += nums[pos];
        dfs(nums, pos + 1);
        path -= nums[pos]; //恢复现场就是和前面反着来

        //减法
        path -= nums[pos];
        dfs(nums, pos + 1);
        path += nums[pos]; 
    }
};

代码二,就是path作为参数传递,思路和“子集‘的解法二类似:

class Solution 
{
    int ret, a;
public:
    int findTargetSumWays(vector<int>& nums, int target) 
    {
        a = target;
        dfs(nums, 0, 0);
        return ret;
    }

    void dfs(vector<int>& nums, int pos, int path) //path表示当前节点已经前面枚举之后相加或相减的结果
    {
        if(pos == nums.size())
        {
            //当数组的数已经枚举完并且path已经等于目标值,说明已经找到一种结果
            if(path == a) ret++; 
            return;
        }

        //加法
        dfs(nums, pos + 1, path + nums[pos]);

        //减法
        dfs(nums, pos + 1, path - nums[pos]);
    }
};

 

可以看到能是能通过,但是时间复杂度都很高,这是因为这道题的最优解其实不是爆搜,而是动态规划,我们后面学习动态规划时还会遇到

但是作为学习者,这道题我们需要掌握的就是当path作为全局和参数时,代码大体是如何写的

39. 组合总和

39. 组合总和 - 力扣(LeetCode)

组合总和有四道题,但是第一道是最经典的。题目给我们一个不重复的整数数组,同一个数字可以无限选取,然后题目要求我们从数组中选一些数加起来 == target,要我们找出能选多少不同的组合,下面来分析下这道题:

解法一:

  • 先画决策树,如下图:

解法一代码:

class Solution {
public:
    int a;
    vector<vector<int>> ret;
    vector<int> path;
    vector<vector<int>> combinationSum(vector<int>& candidates, int target) 
    {
        a = target;
        dfs(candidates, 0, 0);
        return ret;
    }

    void dfs(vector<int>& nums, int pos, int sum)
    {
        if(sum == a)
        {
            ret.push_back(path);
            return;
        }
        if(sum > a || pos == nums.size()) return;

        for(int i = pos; i < nums.size(); i++)
        {
            path.push_back(nums[i]);
            dfs(nums, i ,sum + nums[i]); //由于可以选重复的数,所以第二个参数是i,不是i + 1
            path.pop_back();
        }
    }
};

解法二: 

解法一是根据某一个位置来展开枚举的,解法二咱们就根据数来展开

  • 假设还是 [2, 3, 5] target = 8,这个例子,我们先看2,我们可以一次枚举0个2,1个2,2个2,3个2,4个2,5个2,得到的结果就是0,2,4,6,8,10,由于10已经大于target,所以枚举的2的个数到5时停止枚举,把结果为8的情况统计一下
  • 然后我们固定0个2的结果0,再次枚举3,也是枚举0个3,1个3,2个3,3个3,结果为0,3,6,9,9大于8了,停止枚举,由于没有结果为8,不统计
  • 然后我再次固定0个3的结果,往后再次枚举4的个数,就这样再次一直枚举,和上面相同的步骤
  • 然后再次返回到0个2的结果哪里,然后再次 固定 1个2 的结果2再次以相同的步骤再次枚举3和4,这就是解法二
  • 解法二的回溯需要等一个数全部枚举完后再回溯,这样可以减少很多不必要的加减运算

解法二代码: 

class Solution {
public:
    int a;
    vector<vector<int>> ret;
    vector<int> path;
    vector<vector<int>> combinationSum(vector<int>& candidates, int target) 
    {
        a = target;
        dfs(candidates, 0, 0);
        return ret;
    }

    void dfs(vector<int>& nums, int pos, int sum)
    {
        if(sum == a)
        {
            ret.push_back(path);
            return;
        }
        if(sum > a || pos == nums.size()) return;

        //解法二
        for(int i = 0; i * nums[pos] + sum <= a; i++) //加上sum可以减少枚举次数
        {
            if(i) path.push_back(nums[pos]);
            dfs(nums, pos + 1 , sum + i * nums[pos]);
        }
        //恢复现场
        for(int i = 1; i * nums[pos] + sum <= a; i++)
        {
            path.pop_back();
        }
    }
};

784. 字母大小写全排列

784. 字母大小写全排列 - 力扣(LeetCode)

题目很简单,看示例就能看懂,下面来分析下这道题:

  • 先画决策树,如下图:
class Solution {
public:
    string path;
    vector<string> ret;
    vector<string> letterCasePermutation(string s) 
    {
        dfs(s, 0);
        return ret;
    }
    void dfs(string s, int pos)
    {
        if(path.size() == s.length())
        {
            ret.push_back(path);
            return;
        }

        char ch = s[pos];
        //改变
        if((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z'))
        {
            char tmp = ch;
            if(ch >= 'a' && ch <= 'z') tmp -= 32;
            else tmp += 32;

            path.push_back(tmp);
            dfs(s, pos + 1);
            path.pop_back();
        }

        //不改变
        path.push_back(s[pos]);
        dfs(s, pos + 1);
        path.pop_back();
    }
};

526. 优美的排列

526. 优美的排列 - 力扣(LeetCode)

  • 这道题有两道工序要搞,一个就是先找到全排列,然后判断这个排列是否满足“优美”条件
  • 先画决策树,如下图:
class Solution 
{
public:
    bool check[16];
    int ret;
    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; //恢复现场
            }
        }
    }
};

51. N皇后

51. N 皇后 - 力扣(LeetCode)

国际象棋中皇后的攻击机制如上面所说,n皇后问题,就是要我们在一个 n * n 大小的棋盘内放皇后棋子,返回所有符合 n皇后 机制的棋盘,如示例图和示例1,下面来分析下这道题:

  • 这道题的难点就是 “剪枝 + 代码能力”,这道题重点就是要搞一个策略,来判断一个位置能不能放皇后,有两种策略:
  • 策略一:就是每一个格子来考虑能不能放皇后,判断完成后再去下一个格子搞,这样遍历完所有的格子即可,虽然这种方法时间复杂度是常数,但是代码非常不好写很麻烦,不推荐用
  • 策略二:我们每一次我考虑一行的皇后应该放在哪里,如下图:

所以,现在最主要的问题就是:如何“剪枝” ?

  • 剪枝的目的就是考虑当前这个位置能否放上皇后
  • 方法一:无脑循环,就是假设当前位置有皇后,然后对各个方向直线上判断有没有其它的皇后,这种可以解决,但是时间复杂度太高,需要4个循环,为Log(4n * 2的n次方),但是这道题能过;以这个思路我们还可以优化一下,以上图为例,我们每一行只放一个皇后,所以可以少一次循环
  • 方法二:类似哈希表的策略,这个在五子棋这种类似棋盘的问题都可以用;我们可以搞三个数组,bool row[n] 表示行,bool col[n] 表示列,如下图:
  • 然后行列是这样搞的,那么搞下斜对角线的数组,这个需要一点数学知识,如下图:
  • 然后负对角线也是相同的道理,这里不再赘述,公式为 y + x = b,就是在数组的 y + x 的位置判断是 true 还是 false 即可
class Solution 
{
public:
    int N;
    vector<vector<string>> ret;
    vector<string> path;
    bool checkCol[20], checkDig1[20], checkDig2[20];
    vector<vector<string>> solveNQueens(int n) 
    {
        N = n;
        //先初始化path,相当于初始化棋盘
        path.resize(N);
        for(int i = 0; i < n; i++)
        {
            path[i].append(N, '.');
        }
        dfs(0); //仅需传入一个参数,表示要从哪一行开始去枚举
        return ret;
    }

    void dfs(int row)
    {
        if(row == N) //我一行一行去填,填到越界了,说明已经把 path 里面合法的皇后填完了
        {
            ret.push_back(path);
            return;
        }
        for(int col = 0; col < N; col++) //尝试在这一行放皇后
        {
            //当列,正对角线,负对角线都没有皇后时,就可以放,由于是以行枚举的,所以不必判断行
            if(!checkCol[col] && !checkDig1[row - col + N] && !checkDig2[row + col])
            {
                path[row][col] = 'Q';
                checkCol[col] = true;
                checkDig1[row - col + N] = true;
                checkDig2[row + col] = true;
                dfs(row + 1);

                //恢复现场
                path[row][col] = '.';
                checkCol[col] = false;
                checkDig1[row - col + N] = false;
                checkDig2[row + col] = false;
            }
        }
    }
};

36. 有效的数独

36. 有效的数独 - 力扣(LeetCode)

这道题其实不是回溯题,可以说是哈希表那一类题;数独这道题最重要的其实也是剪枝的操作,而只要把这道题搞懂了,再搞下面的解数独那道题就会很简单,下面来分析下这道题:

  • 数独就是:每一行,每一列以及每个3x3的小方格都不能出现重复的数,题目要求就是给我们一个已经填了部分数的数独,要我们判断这是不是一个有效的数独
  • 我们先搞定行跟列的要求,可以学习“N皇后”的经验,搞两个 bool 类型的数组 row[9][10],以这个数组为例,row[2][4] 就是表示“第二行是否出现了4这个数组”,为true就是出现了,否则为false;列同理。就是 col[9][10],所以 col[7][9] 就表示第7列是否存在9这个数
  •  然后就是搞定每一个 3x3 的小方格了,如果要暴力的话,就是用两个for循环暴力去找;但是我们也可以搞一个哈希表,把每个 3x3 的小方格看成一个整体,那么一个数独就是有9个小方格,所以数组就是 bool grid[3][3][10],前面两个数就是表示方格的位置,而第三个数就是表示这个方格有没有这个数
  • 而且这个方格的位置都很好搞,要想知道一个 1x1 的小小方格在哪个 3x3 的小方格里,只要把这个 1x1 的方格的位置都 “/3” 即可,就能快速知道它在哪一个 3x3 方格里
  • 上面的方法就是典型的用“空间换时间”的方法
class Solution 
{
public:
    bool row[9][10];
    bool col[9][10];
    bool grid[3][3][10];

    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;
                    else
                        row[i][num] = col[j][num] = grid[i / 3][j / 3][num] = true;
                }
            }
        }
        return true;
    }
};

37. 解数独

37. 解数独 - 力扣(LeetCode)

数独的规则这里不再赘述,而有了上一道题的经验之后,这道题虽然标的是困难,但是难度每那么大了,下面来分析下这道题:

  • 依旧是依次遍历这个二维数组,遇到一个空格就往里面填数,依次填 1-9 的数,然后决策树就有9个分支(碍于篇幅问题,这里就不画决策树了哈),然后就是判断填在这里的9个数合不合法,判断方式也很简单,就是上一题的那3个数组,当不合法时,直接返回不再往下递归
class Solution 
{
public:
    bool row[9][10];
    bool col[9][10];
    bool grid[3][3][10];
    void solveSudoku(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';
                    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 num = 1; num <= 9; num++)
                    {
                        if(!row[i][num] && !col[j][num] && !grid[i / 3][j / 3][num])
                        {
                            board[i][j] = num + '0';
                            row[i][num] = col[j][num] = grid[i / 3][j / 3][num] = true;
                            if(dfs(board) == true) return true;
                            //填下一个数时一定要恢复现场
                            board[i][j] = '.';
                            row[i][num] = col[j][num] = grid[i / 3][j / 3][num] = false;
                        }
                    }
                    //当1-9全部填完之后依旧没有返回true,说明这种情况不行
                    return false;
                }
            }
        }
        return true;
    }
};

79. 单词搜索

79. 单词搜索 - 力扣(LeetCode)

给我们一个矩阵,然后矩阵有一些字母,然后再给我们一个单词,要我们设计一个算法判断该单词的字母在矩阵中是否连续存在,并且和单词的顺序一样,如示例,下面来分析下这道题:

  • 如下图:
  • 根据上面的图片中的坐标,我们就可以开始画决策树了,如下图:
  • 仔细看就可以发现,我们只需要对上面树做一次深度优先遍历即可
  • 然后就是函数体的设计,就是给我一个位置,我要去匹配周围节点的值,所以函数体头设计就是bool dfs(board, i, j, s, pos),表示我要在[i, j]这个位置上下左右去匹配s的pos这个位置的字符存不存在,然后就是返回值,失败了就去试另一条路
  • 然后就是回溯和剪枝,回溯就是这条路匹配失败往回走的那一段,然后剪枝同理

细节:二维矩阵搜索中,经常要注意的细节

  •  不能走重复的路。假设题目给的数组是 A A C D,然后s = A A A A,如果按照上面的逻辑,然后到第二个A时,是上下左右去找的,那么就会找到前面的A,这样就会出问题,所以我们要规避这样的情况
  • 解决方法①:就是建立一个和原矩阵同等规模大小的矩阵visit[][],这个矩阵是bool类型的,当一个位置用过时,把visit的对应位置设置成true表示这个位置已经用过了,所以每次往周围遍历时,要先判断下这个数组
  • 但是很多人认为上面的方法可能会浪费空间,所以我们有了另一个解决方法
  • 解决方法②:直接修改原矩阵的值。就是匹配了一个字母之后,先记录这个字母方便回溯,然后把这个字母修改成 ' . ',这种方法在笔试中可以用,但是在面试中得问下面试官,就是原矩阵允不允许修改
  • 但是这道题可以用方法②,但是如果某些题的搜索过程很复杂的话,恢复现场也就会很麻烦,这种情况下还是老老实实用方法①吧
class Solution 
{
public:
    bool vis[7][7];
    int m, n; 
    bool exist(vector<vector<char>>& board, string word) 
    {
        m = board.size(), n = board[0].size();
        for(int i = 0;i < m; i++)
        {
            for(int j = 0; j < n; j++)
            {
                if(board[i][j] == word[0]) //先找第一个字母
                {
                    vis[i][j] = true;
                    if(dfs(board, i, j, word, 1) == true) return true; //从[i][j]位置上下左右匹配word[1]位置的字符
                    vis[i][j] = false;
                }
            }
        }
        return false;
    }

    int dx[4] = {0, 0, 1, -1}; //搞两个数组,数组中存的是x轴和y轴的偏移量(方法二)
    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; //当要匹配的位置和word大小一样了,那么就表示已经找到了一种情况

        //写法一:上下左右去找,可以搞4个函数,不推荐
        //写法二:用向量的方式,定义上下左右四个位置,这个方法是我们在搜索问题中经常用的,推荐
        for(int k = 0; k < 4; k++)
        {
            int x = i + dx[k], y = j + dy[k]; //循环会执行四次
            if((x >= 0 && y >= 0) && (x < m && y < n) && (!vis[x][y] && board[x][y] == word[pos])) //当坐标没有越界,vis矩阵中该元素还没使用过并且能找到和word对应字母匹配
            {
                vis[x][y] = true;
                if(dfs(board, x, y, word, pos + 1) == true) return true;
                vis[x][y] = false; //恢复现场
            }
        }
        return false;
    }
};

1219. 黄金矿工

1219. 黄金矿工 - 力扣(LeetCode)

这个题目和我们玩的那个黄金矿工还是有区别的。

这道题和上一道单词搜索很像,题目给我们一个二维数组,然后我们任意值不为0的位置出发,可以往上下左右四个方向遍历,每一次遍历将该位置的数相加,然后要我们找到相加的数的最大值,要求是每次遍历的位置必须是连续的,不能遍历值为0的位置,每个位置只能遍历一次,下面来分析下这道题:

  • 这道题最基本的遍历路线和上面单词搜索很相似的,如下图:
  • 其余的步骤比如说函数设计细节处理啥的,和上面单词搜索是一样的,这里不再赘述
class Solution 
{
    bool vis[16][16];
    int dx[4] = {0, 0, 1, -1};
    int dy[4] = {1, -1, 0, 0};
    int m, n;
    int path, ret;
public:
    int getMaximumGold(vector<vector<int>>& grid) 
    {
        m = grid.size(), n = grid[0].size();
        for(int i = 0; i < m; i++)
        {
            for(int j = 0; j < n; j++)
            {
                if(grid[i][j])
                {
                    vis[i][j] = true;
                    path = grid[i][j];
                    dfs(grid, i, j);
                    vis[i][j] = false;
                }
            }
        }
        return ret;
    }

    void dfs(vector<vector<int>>& g, int i, int j)
    {
        ret = max(ret, path);
        for(int k = 0; k < 4; k++)
        {
            int x = i + dx[k], y = j + dy[k];
            if((x >= 0 && y >= 0) && (x < m && y < n) && (!vis[x][y] && g[x][y] != 0))
            {
                vis[x][y] = true;
                path += g[x][y];
                dfs(g, x, y);
                path -= g[x][y];
                vis[x][y] = false;
            }
        }
    }
};

980. 不同路径 Ⅲ

980. 不同路径 III - 力扣(LeetCode)

这道题的前面两个题的“不同路径Ⅰ”和 “不同路径Ⅱ”推荐用动态规划来解决,但是这道题不建议用dp,这道题建议用爆搜

题目给我们一个网格,网格里有很多数字,不同的数字有不同的效果,要我们返回从起始位置到结束位置的所有路径的数量,但是有一个要求,就是必须要把所有的0的位置都走一次,而且每个0只能走一次,下面来分析下这道题:

  •  
class Solution 
{
    bool vis[21][21];
    int dx[4] = {1, -1, 0, 0};
    int dy[4] = {0, 0, 1, -1};
    int m, n, step;
    int beginX, beginY;
    int ret;
public:
    int uniquePathsIII(vector<vector<int>>& grid) 
    {
        m = grid.size(), n = grid[0].size();
        for(int i = 0; i < m; i++)
        {
            for(int j = 0; j < n; j++)
            {
                if(grid[i][j] == 0) 
                {
                    step++;
                }
                else if(grid[i][j] == 1)
                {
                    beginX = i;
                    beginY = j;
                    step += 2; //加上开始和结束的步数
                }
            }
        }
        vis[beginX][beginY] = true;
        dfs(grid, beginX, beginY, 1); //从起始位置开始遍历
        return ret;
    }
    void dfs(vector<vector<int>>& g, int i, int j, int count)
    {
        if(g[i][j] == 2)
        {
            if(count == step) 
                ret++;
            return;
        }
        for(int k = 0; k < 4; k++)
        {
            int x = i + dx[k], y = j + dy[k];
            if((x >= 0 && y >= 0) && (x < m && y < n) && (!vis[x][y] && g[x][y] != -1))
            {
                vis[x][y] = true;
                dfs(g, x, y, count + 1);
                vis[x][y] = false;
            }
        }
    }
};
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值