可惜,差 7 分钟就可以AK了,这周的题目其实算简单的。自己对二叉树的还不够熟悉,在第三题卡了挺久的,主要是二叉树递归还带返回值,这类型的题目做的不多,经验还是太少。排名:301 / 1659。
第一题:模拟 + 排序。
第二题:贪心。
第三题:二叉树处理。
第四题:动态规划 DP。
详细题解如下。
1.方阵中战斗力最弱的 K 行(The K Weakest Rows In A Matrix)
2. 数组大小减半(Reduce Array Size to The Half)
3.分裂二叉树的最大乘积(Maximum Product of Splitted Binary Tree)
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)状态转移方程 ,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;
}
};