LeetCode第172场周赛(Weekly Contest 172)解题报告

回家傻一天。在家做的第一场周赛,早上起来懵逼了一上午,最后惨不忍睹。晚上在写题解的时候,重新看题,思路清晰,反应快速。然而....,哇,掉分了。

第一题:模拟。

第二题:模拟。

第三题:树的操作。

第四题:贪心 或者 动态规划 DP。

详细题解如下。


1.6 和 9 组成的最大数字(Maximum 69 Number)

          AC代码(C++)

2. 竖直打印单词(Print Words Vertically)

          AC代码(C++)

3.删除给定值的叶子节点(Delete Leaves With A Given Value)

          AC代码(C++)

4.灌溉花园的最少水龙头数目(Minimum Number of Taps to Open to Water A Garden)

          AC代码(方法一  贪心  C++)

          AC代码(方法二  DP  C++)


LeetCode第172场周赛地址:

https://leetcode-cn.com/contest/weekly-contest-172/


1.6 和 9 组成的最大数字(Maximum 69 Number)

题目链接

https://leetcode-cn.com/problems/maximum-69-number/

题意

给你一个仅由数字 6 和 9 组成的正整数 num

你最多只能翻转一位数字,将 6 变成 9,或者把 9 变成 6 。

请返回你可以得到的最大数字。

示例 1:

输入:num = 9669
输出:9969
解释:
改变第一位数字可以得到 6669 。
改变第二位数字可以得到 9969 。
改变第三位数字可以得到 9699 。
改变第四位数字可以得到 9666 。
其中最大的数字是 9969 。

示例 2:

输入:num = 9996
输出:9999
解释:将最后一位从 6 变到 9,其结果 9999 是最大的数。

示例 3:

输入:num = 9999
输出:9999
解释:无需改变就已经是最大的数字了。

提示:

  • 1 <= num <= 10^4
  • num 每一位上的数字都是 6 或者 9 。

解题思路

根据题意和数据范围,num的最大输入可能是 9999。

根据题意进行,我们从高位往下遍历,当判断出第一次出现某一位是 6时,就将 6 替换成 9,即可。

因此我们要模拟,从高位往低位,那么就是从 1000,100,10,最后到 1。

当我们出现这一位时 6 时,变为 9,相当于原来的 num,再加上, 3 * 对应位数(1000,或100,或10,或1)。

 

AC代码(C++)

class Solution {
public:
    int maximum69Number (int num) {
        int cur = num;
        for(int p = 1000; p >= 1; p /= 10)  // 从高位往低位
        {
            if(cur / p == 6)  // 出现是 6,
            {
                num += 3 * p;  // 变成 9 相当于是,加上 3* 对应位数的乘积
                break;
            }
            cur %= p;
        }
        return num;
    }
};

 


2. 竖直打印单词(Print Words Vertically)

题目链接

https://leetcode-cn.com/problems/print-words-vertically/

题意

给你一个字符串 s。请你按照单词在 s 中的出现顺序将它们全部竖直返回。
单词应该以字符串列表的形式返回,必要时用空格补位,但输出尾部的空格需要删除(不允许尾随空格)。
每个单词只能放在一列上,每一列中也只能有一个单词。

示例 1:

输入:s = "HOW ARE YOU"
输出:["HAY","ORO","WEU"]
解释:每个单词都应该竖直打印。 
 "HAY"
 "ORO"
 "WEU"

示例 2:

输入:s = "TO BE OR NOT TO BE"
输出:["TBONTB","OEROOE","   T"]
解释:题目允许使用空格补位,但不允许输出末尾出现空格。
"TBONTB"
"OEROOE"
"   T"

示例 3:

输入:s = "CONTEST IS COMING"
输出:["CIC","OSO","N M","T I","E N","S G","T"]

提示:

  • 1 <= s.length <= 200
  • s 仅含大写英文字母。
  • 题目数据保证两个单词之间只有一个空格。

解题思路

根据题目的意思,将句子中的各个单词分出来,然后将单词竖着放,然后每个单词的同一行上的字符,组成新的一个字符串。然后同一行的单词长度不够,就补 空格,但是如果是 空格 在末尾,就不用补。

比如,对于示例2,具体的分析如下

所以这道题,不难,主要是理解题意,然后按照题目意思进行模拟即可。

因此,主要分为以下几步:

1)先将输入的字符串句子,拆分成,各个单词。

2)求出各个单词的最大长度maxLen。根据上面示例2的分析,最后的字符串数组答案,有几个字符串,取决于,各个单词的最大长度。

3)接着,我们遍历同一行,也就是 index 从 0 到 maxLen - 1,那么多行。

     3.1)首先,分析,补空格是怎么来,就是当我们分析到某一行的时候,对于某个单词而言,已经超出它本身的长度,也就是已经没有字符了,那么就对这个单词而言,我们要取出这一行的字符,那就是给它补空格

     3.2)对于末尾不用补空格。我们为了判断这个空格到底最后成不成为输出字符串的一部分。我们从最后一个单词到第一个单词的 index 来取,只要没有出现过是有效字符,那么出现的空格,那就是末尾空格,不用加上。但如果已经出现过有效字符,比如示例2中,第三个输出字符串,出现过 T了,那么前面再出现空格,也是要加上去的,因为要组成新的字符串,那么前面要补空格。

 

所以只要按照上面的分析,模拟就可以了

 

AC代码(C++)

class Solution {
public:
    vector<string> printVertically(string s) {
        
        // 拆分成各个单词
        vector<string> words;
        string str = "";
        for(int i = 0;i < s.size(); ++i)
        {
            if(s[i] == ' ')
            {
                words.push_back(str);
                str = "";
            }
            else
            {
                str += s[i];
            }
        }
        words.push_back(str);
        
        // 找出最大长度
        int maxLen = 0;
        for(auto w : words)
        {
            maxLen = max(maxLen, (int)w.size());
        }
        

        vector<string> ans;
        int index = 0;
        int n = words.size();
        
        // 遍历所有行, 0 到 maxLen - 1
        while(index < maxLen)
        {
            str = "";
            bool sp = false; 
            for(int i = n - 1; i >= 0; --i)
            {
                if(index >= (int)words[i].size())  // 出现空格,要判断这个空格加不加
                {
                    if(sp == true)  // 这个用于标记,出现这个空格前,是不是已经出现过有效字符了
                    {
                        str = " " + str;
                    }
                }
                else  // 出现有效字符,那就是组成新字符串的一部分,同时,表示,再出现空格,也要加进新字符串中
                {
                    str = words[i][index] + str;
                    sp = true;
                }
                    
            }
            index++;
            ans.push_back(str);
        }
        
        return ans;
    }
};

 


3.删除给定值的叶子节点(Delete Leaves With A Given Value)

题目链接

https://leetcode-cn.com/problems/delete-leaves-with-a-given-value/

题意

给你一棵以 root 为根的二叉树和一个整数 target ,请你删除所有值为 target 的 叶子节点 。

注意,一旦删除值为 target 的叶子节点,它的父节点就可能变成叶子节点;如果新叶子节点的值恰好也是 target ,那么这个节点也应该被删除。

也就是说,你需要重复此过程直到不能继续删除。

示例 1:

示例有图,具体看链接

输入:root = [1,2,3,2,null,2,4], target = 2
输出:[1,null,3,null,4]
解释:
上面左边的图中,绿色节点为叶子节点,且它们的值与 target 相同(同为 2 ),它们会被删除,得到中间的图。
有一个新的节点变成了叶子节点且它的值与 target 相同,所以将再次进行删除,从而得到最右边的图。

示例 2:

示例有图,具体看链接

输入:root = [1,3,3,3,2], target = 3
输出:[1,3,null,null,2]

提示:

  • 1 <= target <= 1000
  • 每一棵树最多有 3000 个节点。
  • 每一个节点值的范围是 [1, 1000] 。

解题分析

二叉树的操作,那么考虑用dfs(也就是递归)。

对于一个节点而言,我们要判断这个节点是不是符合这个要求,本来是先判断这个节点是不是叶节点(也就是左右节点都是NULL),但是因为这个节点即使不是叶节点,但是可能由于叶节点满足要求被删除了,那么此时父节点就变成了叶节点。

因此,在判断这个节点的时候,我们先要对其左右节点进行 dfs,然后再判断这个节点满步满足要求。

由于二叉树的操作是删除之类的,那么dfs的要有返回值,返回值也是二叉树的某个节点值。

 

dfs其实就是递归,相当于我们要分析子问题间的关系。那么对于这道题其实就是

1)我们要判断一个节点,满不满足要求,就是这个节点是叶节点,同时 val == target

2)但是即使这个节点是父节点,但是可能它的子节点都被删除了,就变成了叶节点。所以再进行操作 1)之前,我们先要对这个节点的左右节点进行 dfs。

 

AC代码(C++)

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode(int x) : val(x), left(NULL), right(NULL) {}
 * };
 */
class Solution {
public:
    
    TreeNode* dfs(TreeNode* root, int target)
    {
        if(root == NULL) return NULL;
        
        root->left = dfs(root->left, target);
        root->right = dfs(root->right, target);
        
        if(root->left==NULL && root->right==NULL && root->val==target) root = NULL;
        
        return root;
    }
    
    TreeNode* removeLeafNodes(TreeNode* root, int target) {
        return dfs(root, target);
    }
};

 


4.灌溉花园的最少水龙头数目(Minimum Number of Taps to Open to Water A Garden)

题目链接

https://leetcode-cn.com/problems/minimum-number-of-taps-to-open-to-water-a-garden/

题意

在 x 轴上有一个一维的花园。花园长度为 n,从点 0 开始,到点 n 结束。

花园里总共有 n + 1 个水龙头,分别位于 [0, 1, ..., n] 。

给你一个整数 n 和一个长度为 n + 1 的整数数组 ranges ,其中 ranges[i] (下标从 0 开始)表示:如果打开点 i 处的水龙头,可以灌溉的区域为 [i -  ranges[i], i + ranges[i]] 。

请你返回可以灌溉整个花园的 最少水龙头数目 。如果花园始终存在无法灌溉到的地方,请你返回 -1 。

示例 1:

输入:n = 5, ranges = [3,4,1,1,0,0]
输出:1
解释:
点 0 处的水龙头可以灌溉区间 [-3,3]
点 1 处的水龙头可以灌溉区间 [-3,5]
点 2 处的水龙头可以灌溉区间 [1,3]
点 3 处的水龙头可以灌溉区间 [2,4]
点 4 处的水龙头可以灌溉区间 [4,4]
点 5 处的水龙头可以灌溉区间 [5,5]
只需要打开点 1 处的水龙头即可灌溉整个花园 [0,5] 。

示例 2:

输入:n = 3, ranges = [0,0,0,0]
输出:-1
解释:即使打开所有水龙头,你也无法灌溉整个花园。

示例 3:

输入:n = 7, ranges = [1,2,1,0,2,1,0,1]
输出:3

示例 4:

输入:n = 8, ranges = [4,0,0,0,0,0,0,0,4]
输出:2

提示:

  • 1 <= n <= 10^4
  • ranges.length == n + 1
  • 0 <= ranges[i] <= 100

解题分析

方法一:贪心

对于喷洒的范围,我们利用贪心的方法,

首先,类似示例 1 的操作,我们先把各个水龙头的喷洒区间(左边最小值应该为 0,右边最大值最多为 n)。

然后,我们找到已经喷洒的边界为 pos,一开始的pos = 0。我们本来贪心的思想是,遍历所有区间,找到区间的左边点,是 <= pos,也就是至少这个范围的起始点已经可以可以覆盖之前已经覆盖的,那么新覆盖的范围,我们要找到最大的,那么就找这些满足的区间的右边点的最大值。这样子使得新覆盖的范围最大,那就是需要最少的水龙头,然后新的边界 pos更新。然后继续。直到所有的区间都遍历完了。(这个地方,为了一次遍历完,我们可以将区间进行升序排序,保证了所有区间的左边点是升序的,那么比较的时候,刚好就区间从前往后,一次遍历完)。

最后就判断,pos==n,如果是,那就输出,水龙头个数。否则输出-1。

方法二:动态规划DP

看到题目,最少数目,那么想到,能不能使用DP

三步走

1)设变量,dp[ i ] 表示,前面已经喷洒的范围,到 i 的需要最少的数目

2)状态转移。对于每一个区间,这个范围的 dp[ 区间最左边 ] = min(本身,dp[区间内的点] + 1),因为对于一个区间而言,区间最左边的,就是新的覆盖的范围,那么他可以是从 区间中的任意一个点,转移过来的。所以取的是最小值。

3)起始条件,因为是最小,所以所有的 dp初始值都设为INF。然后 dp[ 0 ] = 0,起点,这个时候,还没开始覆盖,所以需要 0 个。

最后输出判断,输出 dp[ n ],如果还是INF,说明无法覆盖完全部,输出 -1。否则,直接输出 dp[ n ]。

 

AC代码(方法一  贪心  C++)

class Solution {
public:
    int minTaps(int n, vector<int>& ranges) {
        vector<vector<int> > r(n + 1, vector<int> (2, 0));
        
        // 区间排序
        for(int i = 0;i < n + 1; ++i)
        {
            r[i][0] = max(i - ranges[i], 0);
            r[i][1] = min(i + ranges[i], n);
        }

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

        if(r[0][0] != 0) return -1;

        int ans = 0;
        int pos = 0;

        // 区间从头到尾遍历
        for(int i = 0;i < n + 1; ++i)
        {
            int temp = pos;
            // 找出所有满足条件的区间的左边点的最大值,也就是当前可以覆盖的最大范围,从而需要最少数目
            while(i < n + 1 && r[i][0] <= pos)
            {
                temp = max(temp, r[i][1]);
                ++i;
            }
            if(temp > pos)
            {
                --i;
                pos = temp;
                ans++;
            }
        }
        if(pos == n) return ans;
        else return -1;

    }
};

 

AC代码(方法二  DP  C++)

#define INF 0x3f3f3f3f
#define MAXN (int)(1e4 + 50)
class Solution {
public:
    
    int dp[MAXN];

    int minTaps(int n, vector<int>& ranges) {
        
        // 初始化
        for(int i = 0;i <= n;i++)
            dp[i] = INF;
        dp[0] = 0;

        for(int i = 0;i < n + 1; ++i)
        {
            int lf = max(i - ranges[i], 0);
            int rt = min(i + ranges[i], n);
            
            // 区间中所有的状态转移
            for(int j = lf; j < rt; ++j)
            {
                dp[rt] = min(dp[rt], dp[j] + 1);
            }
        }

        if(dp[n] == INF)
            return -1;
        else
            return dp[n];
    }
};

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值