又是一周掉分之旅,我发现,LeetCode周赛的数据好水,所以有的时候,实在没思路,先暴力解决试试(即使分析出时间复杂度会超时),比如第二题和第三题都可以暴力通过,GG思密达。
这周主要使用了数据结构中的:优先队列。即当我们动态改变一个数组的值,同时每一次都想知道其中的最大值(或者最小值),那么可以考虑使用 优先队列。其存取的时间复杂度是 O(logN),N 是优先队列中的个数。
第一题:暴力枚举 或者 二分查找。
第二题:前缀和(乘积)。
第三题:贪心。
第四题:思维。
详细题解如下。
1.统计有序矩阵中的负数(Count Negative Numbers In A Sorted Matrix)
AC代码(C++)
2. 最后 K 个数的乘积(Product of The Last K Numbers)
AC代码(C++)
3.最多可以参加的会议数目(Maximum Number of Events that Can Be Attended)
AC代码(C++)
4.多次求和构造目标数组(Maximum Students Taking Exam)
AC代码(C++)
LeetCode第176场周赛地址:
https://leetcode-cn.com/contest/weekly-contest-176
https://leetcode-cn.com/problems/count-negative-numbers-in-a-sorted-matrix/
给你一个
m * n
的矩阵grid
,矩阵中的元素无论是按行还是按列,都以非递增顺序排列。请你统计并返回
grid
中 负数 的数目。示例 1:
输入:grid = [[4,3,2,-1],[3,2,1,-1],[1,1,-1,-2],[-1,-1,-2,-3]] 输出:8 解释:矩阵中共有 8 个负数。
示例 2:
输入:grid = [[3,2],[1,0]] 输出:0
提示:
m == grid.length
n == grid[i].length
1 <= m, n <= 100
-100 <= grid[i][j] <= 100
根据数据范围,我们可以直接暴力枚举二维数组中的每一个数,时间复杂度为 O(N*M)。
优化的做法:二分查找,从数组的右上角(这样子,该值的左边都比它大,下边都比它小),也就有了二分查找。从而我们可以找到每一列中,从第几行开始,就都是负数,这样子的时间复杂度为 O(M + N),最多遍历一次所有列,同时一次所有行。
class Solution {
public:
int countNegatives(vector>& grid) {
int cnt = 0;
int n = grid.size(), m = grid[0].size();
for (int i = 0; i < n; ++i) {
for (int j = 0;j < m; ++j) {
if(grid[i][j] < 0)
++cnt;
}
}
return cnt;
}
};
https://leetcode-cn.com/problems/product-of-the-last-k-numbers/
请你实现一个「数字乘积类」ProductOfNumbers,要求支持下述两种方法:
1. add(int num)
- 将数字 num 添加到当前数字列表的最后面。
2. getProduct(int k)
- 返回当前数字列表中,最后 k 个数字的乘积。
- 你可以假设当前列表中始终 至少 包含 k 个数字。
- 题目数据保证:任何时候,任一连续数字序列的乘积都在 32-bit 整数范围内,不会溢出。
示例 1:
输入: ["ProductOfNumbers","add","add","add","add","add","getProduct","getProduct","getProduct","add","getProduct"] [[],[3],[0],[2],[5],[4],[2],[3],[4],[8],[2]] 输出: [null,null,null,null,null,null,20,40,0,null,32] 解释: ProductOfNumbers productOfNumbers = new ProductOfNumbers(); productOfNumbers.add(3); // [3] productOfNumbers.add(0); // [3,0] productOfNumbers.add(2); // [3,0,2] productOfNumbers.add(5); // [3,0,2,5] productOfNumbers.add(4); // [3,0,2,5,4] productOfNumbers.getProduct(2); // 返回 20 。最后 2 个数字的乘积是 5 * 4 = 20 productOfNumbers.getProduct(3); // 返回 40 。最后 3 个数字的乘积是 2 * 5 * 4 = 40 productOfNumbers.getProduct(4); // 返回 0 。最后 4 个数字的乘积是 0 * 2 * 5 * 4 = 0 productOfNumbers.add(8); // [3,0,2,5,4,8] productOfNumbers.getProduct(2); // 返回 32 。最后 2 个数字的乘积是 4 * 8 = 32
提示:
add
和getProduct
两种操作加起来总共不会超过40000
次。0 <= num <= 100
1 <= k <= 40000
从数据范围分析,如果直接每一次暴力求 k 项乘积,应该是会超时的(但实际上,用C++暴力,可以过,不知道为啥子。。。)除非在暴力的时候,进行分析优化(根据分析,乘积的结果应该是 int 大小,那也就是说,这个数最大为 2^31 - 1,由根据num 的范围,0 和 1的多少,其实不影响,k 的乘积(因为乘上 0 就是 0 ,乘上 1 还是本身 ),所以如果从 2 开始,k 个数相乘中,最多32个,就会超要求,所以其实,只要我们统计了 0 的个数,统计连续 1 的个数存在一起,那么对于连续 k 个相乘,最多也就是 64个左右 (也就是非0非1的数之间,插入非0或非1 ))。
而这道题,其实利用了前缀和(乘积)的性质,我们如果记录了 mutil[ i ] 为 前面 i 个数字的乘积,那么对于后 k 个数的结果就是 mutil[ n ] / mutil[ n - k ],也就是我们可以在 O(1) 时间内,求出后 k 个数乘积结果。
但是这里有一个问题,add 数字的 num 可能为 0,我们知道对于出现 0了,会使得乘积为 0。
那我们这里,当遇到 0 时,我们认为当前乘积为 1 (这样子,对于后面的乘积,不受影响(因为前面时 0 了,所以后面的和前面没关系了,重新算)),同时记录最后一个 0 出现的位置
当我们计算 后 k 个数的时候,我们判断,后 k 个数的位置,有没有出现 0 的位置,如果出现,那么直接返回 0。否则还是用 mutil[ n ] / mutil[ n - k ]。
举个例子
[2 3 0 4 2 3]
乘积结果 -> [2 3 1 4 8 24]
当我们要求后三个数的时候,那就是 24 / 1 = 24 (因为要除原本 0 位置的乘积,所以当我们输入 0 ,记录的是 1 )
当我们求后 4 个数的时候,由于 后 4 个数包括了 0 ,那就是返回 0
class ProductOfNumbers {
public:
vector mutil;
int len;
int isZero;
ProductOfNumbers() {
// 初始化,用len记录乘积数组中的个数,同时由于前缀和的性质,我们一般多一位在前面(因为相除的时候,下标要 - 1,所以为了不越界,多一个值)
mutil.clear();
mutil.push_back(1);
len = 1;
isZero = 0; // 记录最后一个 0 出现的位置
}
void add(int num) {
if(num == 0) {
isZero = len;
mutil.push_back(1);
}
else
mutil.push_back(mutil[len - 1] * num); // 如果不是 0,当前值 = 上一个值 * num
++len;
}
int getProduct(int k) {
if(isZero >= len - k) return 0; // 判断出,后 k 个数的下标比 0 出现的下标小,也就是后 k 个数包括 0.
return mutil[len - 1] / mutil[len - k - 1];
}
};
/**
* Your ProductOfNumbers object will be instantiated and called as such:
* ProductOfNumbers* obj = new ProductOfNumbers();
* obj->add(num);
* int param_2 = obj->getProduct(k);
*/
https://leetcode-cn.com/problems/maximum-number-of-events-that-can-be-attended/
给你一个数组 events,其中 events[i] = [startDayi, endDayi] ,表示会议 i 开始于 startDayi ,结束于 endDayi 。
你可以在满足 startDayi <= d <= endDayi 中的任意一天 d 参加会议 i 。注意,一天只能参加一个会议。
请你返回你可以参加的 最大 会议数目。
示例 1:
输入:events = [[1,2],[2,3],[3,4]] 输出:3 解释:你可以参加所有的三个会议。 安排会议的一种方案如上图。 第 1 天参加第一个会议。 第 2 天参加第二个会议。 第 3 天参加第三个会议。
示例 2:
输入:events= [[1,2],[2,3],[3,4],[1,2]] 输出:4
提示:
1 <= events.length <= 10^5
events[i].length == 2
1 <= events[i][0] <= events[i][1] <= 10^5
拿到题目,想到就是利用贪心的做法
首先,先对会议进行排序 : 按照开始时间进行升序排序,如果开始时间一样,那么就按照结束时间排序。
那么我们对天数进行贪心,对于某一天,我们随便找一个,正在开会同时还没有参与的会议,并且为了使得会议时间尽量延长,我们先参与 快要结束的会议(已经结束的会议就不考虑了),然后再考虑下一天。
我们要记录,已经开会(也就是枚举的时间,大于了会议的开始时间)的会议,为了方便我们找到要更快结束的,即找到已经开始的会议中,结束时间最小的。那么这个会议记录是动态的(可能有新开的要进入,已经结束的,要去掉),在一个动态的记录中,找最小值,所以可以考虑使用 : 优先队列。
class Solution {
public:
int maxEvents(vector>& events) {
priority_queue, greater > pq; // 优先队列,升序排序
sort(events.begin(), events.end()); // 对events的开始时间进行排序
int n = events.size();
int day = 0;
int ans = 0;
int len = 0; 用于判断events的是不是已经结束了
while (len < n || !pq.empty()) {
++day; // 新的一天
while(!pq.empty() && pq.top() < day) // 先把已经结束的去掉,因为结束时间小的在队头
{
pq.pop();
}
while(len < n && events[len][0] == day) { // 同时把新的一天,要开始的新会议加入队列中
pq.push(events[len][1]);
++len;
}
if(!pq.empty()) { // 如果队列不为空,说明对于这一天而言,有这些会议都在开,那么就找快要结束的一个会议参加
pq.pop();
++ans;
}
}
return ans;
}
};
https://leetcode-cn.com/problems/maximum-students-taking-exam/
给你一个整数数组 target 。一开始,你有一个数组 A ,它的所有元素均为 1 ,你可以执行以下操作:
- 令 x 为你数组里所有元素的和
- 选择满足 0 <= i < target.size 的任意下标 i ,并让 A 数组里下标为 i 处的值为 x 。
- 你可以重复该过程任意次
如果能从 A 开始构造出目标数组 target ,请你返回 True ,否则返回 False 。
示例 1:
输入:target = [9,3,5] 输出:true 解释:从 [1, 1, 1] 开始 [1, 1, 1], 和为 3 ,选择下标 1 [1, 3, 1], 和为 5, 选择下标 2 [1, 3, 5], 和为 9, 选择下标 0 [9, 3, 5] 完成
示例 2:
输入:target = [1,1,1,2] 输出:false 解释:不可能从 [1,1,1,1] 出发构造目标数组。
示例 3:
输入:target = [8,5] 输出:true
提示:
N == target.length
1 <= target.length <= 5 * 10^4
1 <= target[i] <= 10^9
数学思维题,首先先要分析题目,我们如果要正的推,即从 (1, 1, 1...,1)推到相应的 target,很难,因为有很多的可能性。那么就要考虑,逆着推
比如,对于示例 1 和 示例 3
[9,3,5] -> [1, 3, 5] -> [1, 3, 1] -> [1, 1, 1]
[8,5] -> [3, 5] -> [3, 2] -> [1, 2] -> [1, 1]
也就是,我们要得到数组中的最大值(记作mx),利用 最大值 - 其他值之和,替换掉最大值,这样子才能一步步接近 1。记数组的长度为 n。
那么有几个问题要解决
1)因为最大值要替换,所以我们要动态得到数组中的最大值,那就考虑,优先队列 (要求从大到小排序)
2)有了最大值,我们怎么知道其他值之和?我们可以先求出,数组的所有和(记作 sum),那么 其他值之和 = 所有和 - 最大值。同时,当我们更新最大值替换成 最大值 - 其他值之和 (记作 temp),那么此时的数组和更新为 sum = sum - mx + temp。(因为最大值被替换成 temp了)
3)什么时候返回false,什么时候返回true?想要为 true,那么只有当所有数组中的元素为 1,也就是 sum == n(n 是数组长度)。如果不满足,那就一直做上面和1)和2)。那什么时候为false,也就是当我们计算出要把 mx 替换成 temp 时,计算出来的 temp <= 0 (因为数组中的数一定是 >= 1,说明比 1 小的数,说明就不对了),因此,当我们计算出 temp < 1,那么也就是替换后的数,不应该出现在数组中,也就是无法回到 全是 1 的数组,那就返回 false。
#define LL long long
class Solution {
public:
bool isPossible(vector& target) {
int n = target.size();
priority_queue, less > pq; // 利用优先队列找到最大值 O(logN) (降序排序)
LL sum = 0;
LL mx;
for(auto t : target){
sum += t;
pq.push(t); // 把所有数,放到队列中
}
while(sum > n) {
mx = pq.top(); // 取出最大数
pq.pop();
LL temp = sum - mx; // 其他数的和
if(mx <= temp) return false; // 如果其他数和 >= 最大值,那么最大值 - 其他数和 <= 0
temp = mx - temp; // 要替换的数,也就是 最大值 - 其他数之和
sum = sum - mx + temp; // 更新数组所有数之和
pq.push(temp); // 对于队列而言,我们把最大数去掉了,要替换成temp,那就是加入temp
}
return true;
}
};