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

可惜,差 7 分钟就可以AK了,这周的题目其实算简单的。自己对二叉树的还不够熟悉,在第三题卡了挺久的,主要是二叉树递归还带返回值,这类型的题目做的不多,经验还是太少。排名:301 / 1659。

第一题:模拟 + 排序。

第二题:贪心。

第三题:二叉树处理。

第四题:动态规划 DP。

详细题解如下。


1.方阵中战斗力最弱的 K 行(The K Weakest Rows In A Matrix)

          AC代码(C++)

2. 数组大小减半(Reduce Array Size to The Half)

          AC代码(C++)

3.分裂二叉树的最大乘积(Maximum Product of Splitted Binary Tree)

          AC代码(C++)

4.跳跃游戏 V(Jump Game V)

          AC代码(C++)


LeetCode第174场周赛地址:

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


1.方阵中战斗力最弱的 K 行(The K Weakest Rows In A Matrix)

题目链接

https://leetcode-cn.com/problems/the-k-weakest-rows-in-a-matrix/

题意

给你一个大小为 m * n 的方阵 mat,方阵由若干军人和平民组成,分别用 0 和 1 表示。

请你返回方阵中战斗力最弱的 k 行的索引,按从最弱到最强排序。

如果第 i 行的军人数量少于第 j 行,或者两行军人数量相同但 i 小于 j,那么我们认为第 i 行的战斗力比第 j 行弱。

军人 总是 排在一行中的靠前位置,也就是说 1 总是出现在 0 之前。

示例 1:

输入:mat = 
[[1,1,0,0,0],
 [1,1,1,1,0],
 [1,0,0,0,0],
 [1,1,0,0,0],
 [1,1,1,1,1]], 
k = 3
输出:[2,0,3]
解释:
每行中的军人数目:
行 0 -> 2 
行 1 -> 4 
行 2 -> 1 
行 3 -> 2 
行 4 -> 5 
从最弱到最强对这些行排序后得到 [2,0,3,1,4]

示例 2:

输入:mat = 
[[1,0,0,0],
 [1,1,1,1],
 [1,0,0,0],
 [1,0,0,0]], 
k = 2
输出:[0,2]
解释: 
每行中的军人数目:
行 0 -> 1 
行 1 -> 4 
行 2 -> 1 
行 3 -> 1 
从最弱到最强对这些行排序后得到 [0,2,3,1]

提示:

  • m == mat.length
  • n == mat[i].length
  • 2 <= n, m <= 100
  • 1 <= k <= m
  • matrix[i][j] 不是 0 就是 1

 

解题思路

根据题目意思,就是统计每一行的 1 的个数,然后按照一个排序要求 : 按照 1 的个数升序排序,如果个数相同,则按 行的下标 进行升序排序。

因此,我们可以用一个二维数组,每一行记录两个值 : 行的下标 + 对应行 1 的个数。

然后对这个二维数组进行排序,重写cmp : 先按照第二列升序,如果第二列数相同,则按第一列升序。

然后取出这个二维数组的前 k 行的第一列数,作为最后答案。

因此,这道题,只要按照题目进行模拟,然后重写cmp进行排序即可。

 

AC代码(C++)

bool cmp(vector<int> a, vector<int> b)
{
    if(a[1] != b[1])
        return a[1] < b[1];
    else
        return a[0] < b[0];
}

class Solution {
public:
    vector<int> kWeakestRows(vector<vector<int>>& mat, int k) {
        vector<vector<int> > temp;
        int n = mat.size(), m = mat[0].size();
        for(int i = 0;i < n; ++i)
        {
            int cnt = 0;
            for(int j = 0;j < m; ++j)
            {
                if(mat[i][j] == 0)
                    break;
                cnt++;
            }
            temp.push_back({i, cnt});
        }
        sort(temp.begin(), temp.end(), cmp);
        
        vector<int> ans;
        
        for(int i = 0;i < k; ++i)
        {
            ans.push_back(temp[i][0]);
        }
        
        return ans;
    }
};

 


2. 数组大小减半(Reduce Array Size to The Half)

题目链接

https://leetcode-cn.com/problems/reduce-array-size-to-the-half/

题意

给你一个整数数组 arr。你可以从中选出一个整数集合,并删除这些整数在数组中的每次出现。

返回 至少 能删除数组中的一半整数的整数集合的最小大小。

示例 1:

输入:arr = [3,3,3,3,5,5,5,2,2,7]
输出:2
解释:选择 {3,7} 使得结果数组为 [5,5,5,2,2]、长度为 5(原数组长度的一半)。
大小为 2 的可行集合有 {3,5},{3,2},{5,2}。
选择 {2,7} 是不可行的,它的结果数组为 [3,3,3,3,5,5,5],新数组长度大于原数组的二分之一。

示例 2:

输入:arr = [7,7,7,7,7,7]
输出:1
解释:我们只能选择集合 {7},结果数组为空。

示例 3:

输入:arr = [1,9]
输出:1

提示:

  • 1 <= arr.length <= 10^5
  • arr.length 为偶数
  • 1 <= arr[i] <= 10^5

解题思路

根据题目,我们要通过删除原本数组中的某几种数字(比如我们删除 1,那么原数组中,所有的 1 都被删除),使得原本数组剩余的长度要小于原本数组的一半。

这个时候不要受到 示例 1 解释的影响,示例 1 的解释有误导性。

根据我们的分析,我们想要最少的删除 ans 种数字,那就是我们要优先删除,出现次数多的,因为这样子,我们花费 1 的代价,可以尽可能多的删除原数组的长度。

因此对于 示例 1,其实我们删除 数字 3 和 5,因为这两个数字出现的次数多。

因此,我们使用贪心,依此删除出现次数多的数字,直到数组的长度,变为原来的一半以下。

1)先统计每个数出现的次数,根据数据范围大小,我们可以用 cnt[ i ]表示 数字 i 出现的次数

2)对 cnt 数组进行 降序排序

3)依此从出现次数多的删除,直到数组剩余的长度要小于原本数组的一半。

 

AC代码(C++)

class Solution {
public:
    int minSetSize(vector<int>& arr) {
        vector<int> cnt(100010, 0);
        for(auto a : arr)
            cnt[a]++;

        // 降序排序
        sort(cnt.begin(), cnt.end());
        reverse(cnt.begin(), cnt.end());
        
        // 至少要删除这么多长度
        int sum = arr.size();
        sum = (sum + 1) / 2;

        // 从出现次数多的数字开始删除
        int ans = 0;
        for(int i = 0;i < 100010; ++i)
        {
            if(sum <= 0) break;
            
            ans++;
            sum -= cnt[i];
        }
        return ans;
    }
};

 


3.分裂二叉树的最大乘积(Maximum Product of Splitted Binary Tree)

题目链接

https://leetcode-cn.com/problems/maximum-product-of-splitted-binary-tree/

题意

给你一棵二叉树,它的根为 root 。请你删除 1 条边,使二叉树分裂成两棵子树,且它们子树和的乘积尽可能大。

由于答案可能会很大,请你将结果对 10^9 + 7 取模后再返回。

示例 1:

示例有图,具体看链接

输入:root = [1,2,3,4,5,6]
输出:110
解释:删除红色的边,得到 2 棵子树,和分别为 11 和 10 。它们的乘积是 110 (11*10)

示例 2:

示例有图,具体看链接

输入:root = [1,null,2,3,4,null,null,5,6]
输出:90
解释:移除红色的边,得到 2 棵子树,和分别是 15 和 6 。它们的乘积为 90 (15*6)

提示:

  • 每棵树最多有 50000 个节点,且至少有 2 个节点。
  • 每个节点的值在 [1, 10000] 之间。

解题分析

首先先分析题目,我们切断 1 条边的时候,相当于就是得到了两棵树,那么如果我们切断 1 条边,能马上知道某一棵树的所有和,(知道原本总和的情况下),那么另一棵树就能知道其所有和,那么这两颗树的乘积也就知道。

所以我们可以枚举所有断的情况,如果枚举的时候,能马上知道某一棵树的所有和,就可以解决问题了。

那么如何马上知道某一棵树的所有和。

我们发现,当切断 1 条边的时候,对于这个 示例 1。就是上面这个图,对于节点 2,就是一个新树的根节点。那么如果这个节点上,有以下所有节点的和,那么就能知道这棵树的所有和。

所以我们要先进行预处理,将原本的树的节点值,变为 = 原本节点值 + 左子树所有节点值 + 右子树的。

这部分的预处理,也是利用 dfs,带返回值(也就是此时以此节点为根节点的所有值之和)

1)当遇到NULL 返回 0

2)要更新当前节点值,先要得到 左子树值 和 右子树值

3)更新当前节点值 = 原本节点值 + 左子树所有节点值 + 右子树的。

4)同时返回值为 更新的节点值

在最后,得到的返回值,也就是整棵树的所有和 sum

这样子之后,本来上面的图就变为了

此时再用 一次 dfs 枚举所有切断的可能性,然后根据切断的时候,下面的那个节点值就是其中一颗树的所有和,又根据第一次 dfs的结果sum,我们可以得到另一颗子树的所有和,那么也就知道乘积,然后更新此时的最大值。最后得到最大的乘积值。

 

根据数据范围,所有和 最多为 5e8,那么乘积的最大也就 25e16,那么用 long long 存储。最后得到的再对 10^9 + 7 取模后再返回。

 

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) {}
 * };
 */

#define LL long long
const LL MOD = 1e9 + 7;

class Solution {
public:
    // 记录乘积最大值
    LL ans = 0;

    LL dfsSum(TreeNode* root)
    {
        if(root == NULL) return 0;
        // 要更新当前节点值,先得到,左右子树的
        LL l = dfsSum(root->left);
        LL r = dfsSum(root->right);
        // 更新,由于是指针的,所以是直接修改了地址的值,那么root对应的值修改是实参修改
        root->val = root->val + l + r;  

        return root->val;
    }
    // 枚举所有切断可能
    void dfsCalVal(TreeNode* root, LL sum)
    {
        if(root == NULL) return;
        
        LL sum2 = root->val;
        LL sum1 = sum - sum2;
        LL v = sum1 * sum2;
        //cout << sum << " " << sum1 << " " << sum2 << " " << v << " " << ans << endl;
        if(v > ans)
            ans = v;
        
        dfsCalVal(root->left, sum);
        dfsCalVal(root->right, sum);
    }
    
    int maxProduct(TreeNode* root) {
        LL sum = dfsSum(root);

        dfsCalVal(root, sum);

        ans %= MOD;
        return (int)ans;
    }
};

 


4.跳跃游戏 V(Jump Game V)

题目链接

https://leetcode-cn.com/problems/jump-game-v/

题意

给你一个整数数组 arr 和一个整数 d 。每一步你可以从下标 i 跳到:

  • i + x ,其中 i + x < arr.length 且 0 < x <= d 。
  • i - x ,其中 i - x >= 0 且 0 < x <= d 。

除此以外,你从下标 i 跳到下标 j 需要满足:arr[i] > arr[j] 且 arr[i] > arr[k] ,其中下标 k 是所有 i 到 j 之间的数字(更正式的,min(i, j) < k < max(i, j))。

你可以选择数组的任意下标开始跳跃。请你返回你 最多 可以访问多少个下标。

请注意,任何时刻你都不能跳到数组的外面。

示例 1:

输入:arr = [6,4,14,6,8,13,9,7,10,6,12], d = 2
输出:4
解释:你可以从下标 10 出发,然后如上图依次经过 10 --> 8 --> 6 --> 7 。
注意,如果你从下标 6 开始,你只能跳到下标 7 处。你不能跳到下标 5 处因为 13 > 9 。你也不能跳到下标 4 处,因为下标 5 在下标 4 和 6 之间且 13 > 9 。
类似的,你不能从下标 3 处跳到下标 2 或者下标 1 处。

示例 2:

输入:arr = [3,3,3,3,3], d = 3
输出:1
解释:你可以从任意下标处开始且你永远无法跳到任何其他坐标。

示例 3:

输入:arr = [7,6,5,4,3,2,1], d = 1
输出:7
解释:从下标 0 处开始,你可以按照数值从大到小,访问所有的下标。

示例 4:

输入:arr = [7,1,7,1,7,1], d = 2
输出:2

提示:

  • 1 <= arr.length <= 1000
  • 1 <= arr[i] <= 10^5
  • 1 <= d <= arr.length

解题分析

根据题目意思,我们可以考虑,使用动态规划 DP。

1) 设变量,dp[ i ]表示,从 i 下标开始,总共最多可以跳的个数

2)初始化,一开始 都在自己的位置,那这个位置就算一个了,所以 dp[ all ] = 1

3)状态转移方程 dp[i] = max(dp[i], dp[cur] + 1),cur 表示所有可以跳的楼下标,也就是,我们要得到 i 这个楼的个数,那么就是,找出,能从 i 跳到的所有 cur中的最大值 再 + 1。

那么此时一定要注意,怎么更新 dp,按顺序或者反序?都不对

这道题,可以看成是,从高处往低处跳,最多能跳多少栋楼

所以我们要自下而上的更新 DP,那么就要从底楼,开始,这是因为,高楼可以跳到低楼,那么我们要更新高楼的情况,就需要低楼的情况。

因此按按楼,从低到高,更新 dp。

 

而关于从 i 怎么判断到 cur,几个条件。1)i 的左右 距离 d 的范围内。2)不能超出数组范围 [0, n-1]。3)从 i 到 cur,要求 arr[ i ] > arr[ cur ],如果只要出现 <=,那么后面的都跳不到了,因此要跳到,那么 i 是 [i, cur] 范围里的最高楼(根据题目要求)。

 

AC代码(C++)

#define MAXN 1010

vector<int> val;
int dp[MAXN], idx[MAXN];

bool cmp(int a, int b){
    return val[a] < val[b];  // 下标按照楼arr的高度排序
}

class Solution {
public:
    int maxJumps(vector<int>& arr, int d) {
        val = arr;
        int n = arr.size();
        // 把按照楼的从低到高排序的下标
        for(int i = 0;i < n; ++i) idx[i] = i;
        sort(idx, idx + n, cmp);  // 重写cmp
        //初始化
        for(int i = 0;i < n; ++i) dp[i] = 1;
        
        for(int i_ = 0;i_ < n;++i_)
        {
            int i = idx[i_];
            // 左右两边走
            for(int x = 1; x <= d; ++x)
            {
                int cur = i + x;
                if(cur >= n) break;
                if(arr[i] <= arr[cur]) break;
                
                dp[i] = max(dp[i], dp[cur] + 1);
            }
            for(int x = 1; x <= d; ++x)
            {
                int cur = i - x;
                if(cur < 0) break;
                if(arr[i] <= arr[cur]) break;
                
                dp[i] = max(dp[i], dp[cur] + 1);
            }
        }
        // 找出最大值,就是答案。
        int ans = 0;
        for(int i = 0;i < n; ++i)
            ans = max(ans, dp[i]);
        
        return ans;
    }
};

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值