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

又是一周掉分之旅,我发现,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


1.统计有序矩阵中的负数(Count Negative Numbers In A Sorted Matrix)

题目链接

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),最多遍历一次所有列,同时一次所有行。

 

AC代码(C++)

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;
    }
};

 


2. 最后 K 个数的乘积(Product of The Last K Numbers)

题目链接

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

 

 

AC代码(C++)

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

 


3.最多可以参加的会议数目(Maximum Number of Events that Can Be Attended)

题目链接

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

 

 

解题分析

拿到题目,想到就是利用贪心的做法

首先,先对会议进行排序 : 按照开始时间进行升序排序,如果开始时间一样,那么就按照结束时间排序。

那么我们对天数进行贪心,对于某一天,我们随便找一个,正在开会同时还没有参与的会议,并且为了使得会议时间尽量延长,我们先参与 快要结束的会议(已经结束的会议就不考虑了),然后再考虑下一天。

我们要记录,已经开会(也就是枚举的时间,大于了会议的开始时间)的会议,为了方便我们找到要更快结束的,即找到已经开始的会议中,结束时间最小的。那么这个会议记录是动态的(可能有新开的要进入,已经结束的,要去掉),在一个动态的记录中,找最小值,所以可以考虑使用 : 优先队列

  • 按照会议开始日期对 events 排序,使用默认排序算法就可以
  • 再使用一个优先队列,根据日期,将今天开始的会议全都加入到优先队列中(也就是优先队列中的,是已经开始会议的结束时间)
  • 优先队列的排序以会议的结束日期为标准,保证越接近结束日期的越靠前(按结束时间从小到大排序)
  • 每一天如果优先队列里还有会议,就去参加一个(同时把这个会议去掉)
  • 新的一天,先把优先队列里已经过期的会议清除掉(也就是结束时间 < 新的时间,那就是会议已经结束了)

 

AC代码(C++)

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;
    }
};

 


4.多次求和构造目标数组(Maximum Students Taking Exam)

题目链接

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。

 

AC代码(C++)

#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;
    }
};

 

你可能感兴趣的:(LeetCode刷题记录及题解,#,LeetCode比赛)