LeetCode算法练习——深度优先搜索 DFS(2)

更多干货就在我的个人博客 BlackBlog.tech 欢迎关注!
也可以关注我的csdn博客:黑哥的博客
谢谢大家!

我们继续LeetCode之旅.
做了一段时间的LeetCode,感觉还是不错的。算法很基础,没有特别难的(至少我看在做的),很适合考试,面试,就业之前的训练。对提升基本功很有帮助,我觉得如果有时间的话,每天都做上几道,日积月累,代码能力必然有提高。
这一篇继续进行DFS的练习,预计还会有十道题。

给个目录:
LeetCode109 有序链表转换二叉搜索树
LeetCode332 重新安排行程
LeetCode337 打家劫舍 III
LeetCode394 字符串解码
LeetCode417 太平洋大西洋水流问题
LeetCode473 火柴拼正方形
LeetCode494 目标和
LeetCode491 递增子序列
LeetCode515 在每个树行中找最大值
LeetCode513 找树左下角的值

LeetCode109 有序链表转换二叉搜索树

题目

给定一个单链表,其中的元素按升序排序,将其转换为高度平衡的二叉搜索树。

本题中,一个高度平衡二叉树是指一个二叉树每个节点 的左右两个子树的高度差的绝对值不超过 1。

示例:

给定的有序链表: [-10, -3, 0, 5, 9],

一个可能的答案是:[0, -3, 9, -10, null, 5], 它可以表示下面这个高度平衡二叉搜索树:

      0
     / \
   -3   9
   /   /
 -10  5

C++代码

class Solution {
public:
    bool isValidBST(TreeNode* root) {
        return dfs(root,LONG_MAX,LONG_MIN);
    }

    bool dfs(TreeNode* root, long max,long min){
        if(!root) return true;
        if(root->val<=min || root->val>=max) return false;
        else return (dfs(root->left,root->val,min) && dfs(root->right,max,root->val));
    }
};

体会

这个题利用了二叉检索树自身的性质,左边节点小于根节点,右边节点大于根节点。初始化时带入系统最大值和最小值,在递归过程中换成它们自己的节点值,用long代替int就是为了包括int的边界条件。
如果这棵树遍历到了叶节点,则返回true。如果在遍历的过程中出现了当前节点大于等于父节点(左子树)或小于等于父节点(右子树)则返回false。对结果做&&运算。返回最终的结果。

LeetCode332 重新安排行程

题目

给定一个机票的字符串二维数组 [from, to],子数组中的两个成员分别表示飞机出发和降落的机场地点,对该行程进行重新规划排序。所有这些机票都属于一个从JFK(肯尼迪国际机场)出发的先生,所以该行程必须从 JFK 出发。

说明:

如果存在多种有效的行程,你可以按字符自然排序返回最小的行程组合。例如,行程 [“JFK”, “LGA”] 与 [“JFK”, “LGB”] 相比就更小,排序更靠前
所有的机场都用三个大写字母表示(机场代码)。
假定所有机票至少存在一种合理的行程

示例 1:
tickets = [["MUC", "LHR"], ["JFK", "MUC"], ["SFO", "SJC"], ["LHR", "SFO"]]
返回["JFK", "MUC", "LHR", "SFO", "SJC"]

示例 2:
tickets = [["JFK","SFO"],["JFK","ATL"],["SFO","ATL"],["ATL","JFK"],["ATL","SFO"]]
返回["JFK","ATL","JFK","SFO","ATL","SFO"]
另一种有效的行程是 ["JFK","SFO","ATL","JFK","ATL","SFO"]。但是它自然排序更大更靠后。

C++代码

class Solution {
public:
    unordered_map<string,multiset<string>> hash; //用来存储起点与终点的信息
    vector<string> res; //最终的路径结果
    vector<string> findItinerary(vector<pair<string, string>> tickets) {
        if(tickets.size()==0) return {}; //如果输入为空 返回空
        for (int i=0;i<tickets.size();i++){
            hash[tickets[i].first].insert(tickets[i].second); //将ticket转化为hash
        }
        dfs("JFK"); //dfs
        reverse(res.begin(), res.end()); //因为最终得到的结果是一个反序列的 我们要返回来
        return res;

    }
    void dfs(string from){
        while(hash[from].size()>0){ //如果当前起点还有终点没有去过的话
            string tem = *hash[from].begin(); //记录下当前起点去往的第一个终点
            hash[from].erase(hash[from].begin()); //将第一个终点从hash中删除
            dfs(tem); //继续dfs
        }
        res.push_back(from); //将当前的出发点加入res
    }
};

体会

题目看起来并没有什么难度。DFS+回溯,但我觉得有坑。。。
第一次写,写的这个代码,所有样例测试完全正确。

class Solution {
private:
    vector<string> res;
public:
    vector<string> findItinerary(vector<pair<string, string>> tickets) {
        vector<string> path;
        vector<int> mark;
        for(int i=0;i<tickets.size();i++)
            mark.push_back(0);
        path.push_back("JFK");
        dfs(tickets,mark,path,"JFK");
        tickets.clear();
        path.clear();
        mark.clear();
        return res;
    }
    void dfs(vector<pair<string,string>> tickets, vector<int> mark, vector<string> path, string from){
        int flag = true;
        for(int i=0;i<tickets.size();i++){
            if(mark[i]==0) flag=false;
        }
        if(flag == true){
            res = path;
            return;
        }
        for(int i=0;i<tickets.size();i++){
            if(tickets[i].first==from && mark[i]==0){
                path.push_back(tickets[i].second);
                mark[i] = 1;
                dfs(tickets,mark,path,tickets[i].second);
                mark[i]=0;
                path.pop_back();
            }
        }
        return;
    }
};

可是,每次提交都会报一个错误,本地调试也没有问题。就很迷。
换了思路重新写。
各位有兴趣麻烦帮我检查一下代码。

另外,此题少给出了一个条件。就是默认输出最长的路径,路径长度相同时,再以字典序列输出。

LeetCode337 打家劫舍 III

题目

小偷又发现一个新的可行窃的地点。 这个地区只有一个入口,称为“根”。 除了根部之外,每栋房子有且只有一个父房子。 一番侦察之后,聪明的小偷意识到“这个地方的所有房屋形成了一棵二叉树”。 如果两个直接相连的房子在同一天晚上被打劫,房屋将自动报警。

在不触动警报的情况下,计算小偷一晚能盗取的最高金额。

示例 1:

     3
    / \
   2   3
    \   \ 
     3   1

能盗取的最高金额 = 3 + 3 + 1 = 7.

示例 2:

     3
    / \
   4   5
  / \   \ 
 1   3   1

能盗取的最高金额 = 4 + 5 = 9.

C++代码

class Solution {
public:
    //偷 对第一个根节点 采取偷与不偷的方式 最后求最大值
    int rob(TreeNode* root) {
        if(!root) return 0;
        return std::max(robRoot(root),notRobRoot(root));
    }
    //偷根节点的情况 就不能偷两个子节点
    int robRoot(TreeNode *root){
        if(!root) return 0;
        return notRobRoot(root->left) +notRobRoot(root->right)+root->val;
    }
    //不偷根节点的情况 可以偷两个子节点 但不一定就要偷
    int notRobRoot(TreeNode *root){
        if(!root) return 0;
        return rob(root->left) + rob(root->right);
    }
};

体会

这个题属于思路很清晰就很简单的题。
首先我们来分析题目,题目中说如果两个节点连在一起,那么偷东西就会被发现。所有只要我们偷东西的节点不连在一起就没有问题。连在一起的情况只有根节点与子节点。
对于一个根节点分为偷与不偷两种情况,如果偷的话,那么他的子节点就不能偷;如果不偷的话,可以偷他的子节点,也可以不偷,根据偷到的金额去做判断。对于每个根节点,我们要把这两周情况都考虑上,代码如上。

LeetCode394 字符串解码

题目

给定一个经过编码的字符串,返回它解码后的字符串。

编码规则为: k[encoded_string],表示其中方括号内部的 encoded_string 正好重复 k 次。注意 k 保证为正整数。

你可以认为输入字符串总是有效的;输入字符串中没有额外的空格,且输入的方括号总是符合格式要求的。

此外,你可以认为原始数据不包含数字,所有的数字只表示重复的次数 k ,例如不会出现像 3a 或 2[4] 的输入。

示例:

s = "3[a]2[bc]", 返回 "aaabcbc".
s = "3[a2[c]]", 返回 "accaccacc".
s = "2[abc]3[cd]ef", 返回 "abcabccdcdcdef".

C++代码

class Solution {
public:
    string decodeString(string s) {
        dfs(s);
        return s;
    }

    void dfs(string &s){
        int l=0,r=0; //方括号的左起点 右起点
        string num_s=""; //存储数字的部分
        int num_len =0; //数字的长度
        int num; //数字本身
        //遍历找方括号
        for(int i=0;i<s.length();i++){
            if(s[i]=='[') { //如果是左括号
                l = i; //记录位置
                num = atoi(num_s.data()); //将之前存储的num转换为数字
                num_len=num_s.size(); //得到数字的长度 用于后面裁切字符串
                num_s.clear(); //清空 方式影响后面内容
            }
            else if(s[i]==']') {
                r = i; //记录右括号位置
                break; //结束循环
            }
            else if(s[i]<='9' && s[i]>='0') num_s+=s[i]; //记录每一位的数字
        }
        if(l>=r) return ; //结束条件
        string newstr=""; //预备重复的部分字符串
        for(int i=0;i<num;i++)
            newstr+=s.substr(l+1,r-l-1); //重复字符串
        s = s.substr(0,l-num_len)+newstr+s.substr(r+1,s.length()-r-1); //制作新的字符串
        dfs(s);//递归调用
    }
};

体会

算是一道完全的模拟题。
我用的思路比较简单,每次都只处理最里面的一个[],将其中的内容重复。逐次递归调用,最后解析完所有的字符串。注意s = s.substr(0,l-num_len)+newstr+s.substr(r+1,s.length()-r-1);各个字符串的起始位置和裁切长度。

LeetCode417 太平洋大西洋水流问题

题目

给定一个 m x n 的非负整数矩阵来表示一片大陆上各个单元格的高度。“太平洋”处于大陆的左边界和上边界,而“大西洋”处于大陆的右边界和下边界。

规定水流只能按照上、下、左、右四个方向流动,且只能从高到低或者在同等高度上流动。

请找出那些水流既可以流动到“太平洋”,又能流动到“大西洋”的陆地单元的坐标。

提示:
输出坐标的顺序不重要
m 和 n 都小于150

示例:

给定下面的 5x5 矩阵:

  太平洋 ~   ~   ~   ~   ~ 
       ~  1   2   2   3  (5) *
       ~  3   2   3  (4) (4) *
       ~  2   4  (5)  3   1  *
       ~ (6) (7)  1   4   5  *
       ~ (5)  1   1   2   4  *
          *   *   *   *   * 大西洋

返回:

[[0, 4], [1, 3], [1, 4], [2, 2], [3, 0], [3, 1], [4, 0]] (上图中带括号的单元).

C++代码

class Solution {
public:
    vector<pair<int, int>> pacificAtlantic(vector<vector<int>>& matrix) {
        if (matrix.empty() || matrix[0].empty()) return {};
        int m = matrix.size(); //矩阵行数
        int n = matrix[0].size(); //矩阵列数
        vector<pair<int,int>> res; //最终结果
        vector<vector<bool>> pacific(m, vector<bool>(n, false)); //太平洋
        vector<vector<bool>> atlantic(m, vector<bool>(n, false)); //大西洋
        
        //遍历第一行,最底下一行
        for(int i=0;i<m;i++){ 
            dfs(matrix,pacific,i,0,matrix[i][0]);
            dfs(matrix,atlantic,i,n-1,matrix[i][n-1]);
        }
        
        //遍历第一列,最右边一列
        for(int i=0;i<n;i++){
            dfs(matrix,pacific,0,i,matrix[0][i]);
            dfs(matrix,atlantic,m-1,i,matrix[m-1][i]);
        }
        
        //将pacific与atlantic重合的部分 作为最终结果
        for(int i=0;i<m;i++){
            for(int j=0;j<n;j++){
                if(atlantic[i][j] && pacific[i][j])
                    res.push_back(make_pair(i,j));
            }
        }
        return res;
    }
    void dfs(vector<vector<int>>& matrix,vector<vector<bool>> &visited,int x,int y,int val){
        //越界 被访问 或者 当前值小于上一个值的都直接返回
        if (x<0 || x>matrix.size()-1 || y<0 || y>matrix[0].size()-1 || visited[x][y] || matrix[x][y] < val) return;
        //将该坐标设置为访问过
        visited[x][y] = true;
        dfs(matrix,visited,x+1,y,matrix[x][y]);
        dfs(matrix,visited,x-1,y,matrix[x][y]);
        dfs(matrix,visited,x,y+1,matrix[x][y]);
        dfs(matrix,visited,x,y-1,matrix[x][y]);
    }
};

体会

这个题第一眼看到觉得是一个很简单DFS搜索题。第一个想法肯定是对每一个点进行dfs,看他是否能够走到太平洋或者大西洋。显然这样,时间复杂度会很高。我们可以对其优化,每一次得到一条路径后,将路径上的所有点都记录下来,这样可以减少很多次dfs的过程,但优化效果不明显。
这个题,我们采用倒推法的思路。从大西洋、太平洋向内搜索,每次找比当前节点高的或者相同的节点,将这些节点设置为True。最终将大西洋中为True的,太平洋中也为True的节点作为最终的答案。

LeetCode473 火柴拼正方形

题目

还记得童话《卖火柴的小女孩》吗?现在,你知道小女孩有多少根火柴,请找出一种能使用所有火柴拼成一个正方形的方法。不能折断火柴,可以把火柴连接起来,并且每根火柴都要用到。

输入为小女孩拥有火柴的数目,每根火柴用其长度表示。输出即为是否能用所有的火柴拼成正方形。

示例 1:

输入: [1,1,2,2,2]
输出: true

解释: 能拼成一个边长为2的正方形,每边两根火柴。

示例 2:

输入: [3,3,3,3,4]
输出: false

解释: 不能用所有火柴拼成一个正方形。

注意:

给定的火柴长度和在 0 到 10^9之间。
火柴数组的长度不超过15。

C++代码

class Solution {
public:
    bool makesquare(vector<int>& nums) {
        if(nums.size()<4 || nums.size()==0) return false; //如果数组的大小不满足4 就直接返回0
        int sum = accumulate(nums.begin(),nums.end(),0); //对整个数组进行求和
        int target = sum /4 ; //计算每一条边需要的长度
        vector<int> sums(4,0); //新建一个长度为4的数组,用来记录每一个边当前的长度
        sort(nums.rbegin(),nums.rend()); //对数组进行排序,先计算大的,这样可以优化不少效率
        bool res = dfs(nums,sums,0,target); // dfs
        return res;
    }

    bool dfs(vector<int>& nums,vector<int> sums, int pos, int target){
        if(pos>nums.size()-1){ //如果pos>nums.size()-1证明数组已经被遍历完,判断当前所有的边是否都满足要求
            return sums[0]==target && sums[1]==target && sums[2]==target && sums[3]==target;
        }
        //对每一条边进行尝试
        for(int i=0;i<4;i++){
            if(sums[i]+nums[pos]>target) continue; //如果i边 放入一个数字后,超过target则不放入,进入下一条边
            sums[i]+=nums[pos]; //i边 长度增加
            if(dfs(nums,sums,pos+1,target)) return true; //pos+1 继续dfs
            sums[i]-=nums[pos]; //回溯
        }
        return false;
    }
};

体会

这个题是一个比较标准DFS+回溯题。这问题可以理解为,一个数组如何分为四个相同的数。我们创建一个数组,长度为四,用来表明四条边当前的长度,每次遍历时将当前的长度与待放入长度相加后target进行比较。大于,则不放入,小于,则放入,不断dfs,每次pos+1。当pos超出范围时,证明所有的数字都被尝试过,返回当前的状态。

LeetCode494 目标和

题目

给定一个非负整数数组,a1, a2, …, an, 和一个目标数,S。现在你有两个符号 + 和 -。对于数组中的任意一个整数,你都可以从 + 或 -中选择一个符号添加在前面。

返回可以使最终数组和为目标数 S 的所有添加符号的方法数。

示例 1:

输入: nums: [1, 1, 1, 1, 1], S: 3
输出: 5
解释: 

-1+1+1+1+1 = 3
+1-1+1+1+1 = 3
+1+1-1+1+1 = 3
+1+1+1-1+1 = 3
+1+1+1+1-1 = 3

一共有5种方法让最终目标和为3。

注意:

数组的长度不会超过20,并且数组中的值全为正数。
初始的数组的和不会超过1000。
保证返回的最终结果为32位整数。

C++代码

class Solution {
public:
    int solution =0; //方案数
    int findTargetSumWays(vector<int>& nums, int S) {
        int sum = 0;
        dfs(nums,S,0,sum);
        return solution;
    }
    void dfs(vector<int>& nums,int S,int pos,int sum){
        //S表示最后结果,pos表示当前位置,sum表示目前总和
        if(pos>nums.size()-1) { 
            if(sum == S) solution+=1; //如果当前sum==S 方案数+1
            return ;
        }
        sum += nums[pos]; //先尝试+
        dfs(nums,S,pos+1,sum); //dfs
        sum -= nums[pos]; //回溯
        sum -= nums[pos]; //尝试-
        dfs(nums,S,pos+1,sum); //dfs
        sum += nums[pos]; //回溯
        return ;
    }
};

体会

一道非常清晰的DFS+回溯题,我觉得都可以做标准模版了。S表示最后结果,pos表示当前位置,sum表示目前总和,solution表示方案数。dfs中先尝试+,回溯后,尝试-。当sum == S时,方案数+1。最终返回方案数。

LeetCode491 递增子序列

题目

给定一个整型数组, 你的任务是找到所有该数组的递增子序列,递增子序列的长度至少是2。

示例:

输入: [4, 6, 7, 7]
输出: [[4, 6], [4, 7], [4, 6, 7], [4, 6, 7, 7], [6, 7], [6, 7, 7], [7,7], [4,7,7]]

说明:

给定数组的长度不会超过15。
数组中的整数范围是 [-100,100]。
给定数组中可能包含重复数字,相等的数字应该被视为递增的一种情况。

C++代码

class Solution {
public:
    set<vector<int>> res; //使用set 实现去除重复的功能
    vector<vector<int>> findSubsequences(vector<int>& nums) {
        vector<vector<int>> ans; //最终结果
        vector<int> path; //每一次遍历的路径
        //对数组中的每一个元素都要dfs
        for(int i=0;i<nums.size();i++){
            dfs(nums,path,i);
        }
        //将set中的数据转换到vector
        set<vector<int>>::iterator it; //迭代器
        for(it=res.begin();it!=res.end();it++){
            ans.push_back(*it);
        }
        return ans;
    }
    void dfs(vector<int> nums,vector<int> path,int pos){
        //如果path的大小为0,则证明这是第一个数据,直接放入
        if(path.size()==0) path.push_back(nums[pos]);
        //向后遍历,逐个添加
        for(int i=1;i<nums.size();i++) {
            //如果待添加的值小于path的最后一个值 不添加,如果 当前的位置大于数组长度 证明 nums已经遍历完 此时continue其实就是return了
            if(nums[pos+i]<path.back() || (pos+i>nums.size()-1)) continue;
            path.push_back(nums[pos+i]);//path放入新数据
            res.insert(path); //将path添加进set
            dfs(nums, path, pos + i); //对新的数据再进行dfs
            path.pop_back(); //回溯
        }
    }
};

体会

求一个序列中的所有递增子序列。首先区分一下子序列与子串,子序列中的数字不需要是连续的,子串的数字是连续的。因为数据中不能有重复,我们使用set来达到这一效果。dfs内部,path表示当前的路径,pos表示当前的位置。思路很清晰,我们从当前数据开始,对其后面的每一个数据做插入操作后,继续dfs,知道完成整个序列。注意回溯。

LeetCode515 在每个树行中找最大值

题目

您需要在二叉树的每一行中找到最大的值。

示例:

输入: 

          1
         / \
        3   2
       / \   \  
      5   3   9 

输出: [1, 3, 9]

C++代码

class Solution {
public:
    map<int,vector<int>> hash; //key为树的深度 vector存放该层的数据
    vector<int> largestValues(TreeNode* root) {
        dfs(root,0);
        vector<int> res;
        for(auto key : hash){
            res.push_back(*max_element(key.second.begin(),key.second.end())); //将每一行的最大值存在res中
        }
        return res;
    }
    
    void dfs(TreeNode* root,int depth){
        if(!root) return;
        hash[depth].push_back(root->val); //将当前节点存在hash中对应层的数组中
        if(root->left)  dfs(root->left,depth+1); //遍历左孩子
        if(root->right) dfs(root->right,depth+1); //遍历右孩子
    }
};

体会

思路很清晰,map的key表示第几层,value表示对应的节点。我们使用DFS的方法遍历整个树。遍历的过程中,记录下每一个节点的深度,将每一层的节点都存在map中对应数组中。最后将每一层最大的数据存储来即可。

LeetCode513 找树左下角的值

题目

给定一个二叉树,在树的最后一行找到最左边的值。

示例 1:

输入:

    2
   / \
  1   3

输出:
1

示例 2:

输入:

        1
       / \
      2   3
     /   / \
    4   5   6
       /
      7

输出:
7

C++代码

class Solution {
public:
    map<int,vector<int>> hash;
    int max_depth = INT_MIN; //初始化最大深度为一个极小值
    int findBottomLeftValue(TreeNode* root) {
        dfs(root,0); //从根节点开始dfs
        return hash[max_depth][0]; //将最底下一层最左边的元素返回出来
    }
    void dfs(TreeNode* root,int depth){
        if(!root) { // 如果是叶节点下面的节点 对最大深度作出修改
            if(depth-1>max_depth) max_depth=depth-1;
            return;
        }
        hash[depth].push_back(root->val); //将对应层的节点依次放入
        dfs(root->left,depth+1); //dfs左子树
        dfs(root->right,depth+1); //dfs右子树
    }
};

体会

这个题跟上一个题的思路完全一致,没有任何区别。唯一的不同就在于我们这一次不需要将一行中的最大值求出来,我们只需要返回最左边的那个值就可以了。

    原文作者:BlackBlog__
    原文地址: https://www.jianshu.com/p/167224d957d6
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞