LeetCode---386周赛

题目列表

3046. 分割数组

3047. 求交集区域内的最大正方形面积

3048. 标记所有下标的最早秒数 I

3049. 标记所有下标的最早秒数 II

一、分割数组

这题简单的思维题,要想将数组分为两个数组,且分出的两个数组中数字不会重复,很显然一个数字出现次数最多两次,代码如下

class Solution {
public:
    bool isPossibleToSplit(vector<int>& nums) {
        unordered_map<int,int>mp;
        for(auto x:nums)
            if(++mp[x]>2)
                return false;
        return true;
    }
};

二、求交集区域内的最大正方形面积

直接暴力枚举出所有两个矩阵的交集的正方形面积,求解出最大值,代码如下

class Solution {
public:
    long long largestSquareArea(vector<vector<int>>& bLeft, vector<vector<int>>& tRight) {
        int n=bLeft.size(),w=0;
        for(int i=1;i<n;i++){
            for(int j=i-1;j>=0;j--){
                //挑选四个直线,围成交集区域
                int x_l=max(bLeft[i][0],bLeft[j][0]);
                int x_r=min(tRight[i][0],tRight[j][0]);
                int y_top=min(tRight[i][1],tRight[j][1]);
                int y_bottom=max(bLeft[i][1],bLeft[j][1]);
                if(x_l<x_r&&y_top>y_bottom){//确保会有交集
                    w=max(w,min(x_r-x_l,y_top-y_bottom));
                }
            }    
        }
        return 1LL*w*w;
    }
};

三、标记所有下标的最早秒数I

题目问最早秒数,我们正常来说都会想到贪心 / 从小到大枚举验证,其中贪心,大家可以去试着想想,因为需要从左往右遍历时间,而我们不知道后面changeIndices[]的情况,所以就不能去决定这一步去做什么操作会比较好,也就很难去贪心。

那么我们来看看枚举验证行不行?一旦数据不好,估计得验证O(n)次,大概率会超时,所以我们要降低时间复杂度,怎么做?--- 二分

1、是否满足二分条件,即单调性?

根据题目所给的条件,我们知道时间越多,我们越有可能将nums[i]减为零,越有可能标记所有下标,即秒数越多越能满足条件,符合单调性,可以二分

2、如何验证是否能在k秒内标记所有下标,即bool check(int k)函数如何写?(如果下面的内容不理解,可以先看看下面加粗的内容)

首先,由于changeIndices[]是不可预知的,即标记操作是不可控的,所以我们优先考虑什么时候标记下标的问题,根据贪心,我们肯定是越晚标记下标越好,这样会有更多的时间将nums[i]减为零,所以我们要知道每个下标的最晚标记时间

然后在来考虑是否能在下标 i 的最晚标记时间之前,将nums[i]减为零,这个就很简单了,我们只要维护一个cnt来记录到目前为止有多少时间,然后在到达某个最晚标记时间时,如果cnt>=nums[i],cnt = cnt - nums[i],否则直接返回false,如果所有下标都能被标记就返回true (很显然check函数的时间复杂度为O(n),所以暴力枚举会超时,需要二分)

如果大家不是很理解,可以将这题转换成考试来看,即一共有n门课程,nums[i]表示第 i 门课程的复习天数,changeIndices[i]表示第 i 天进行考试的课程,问复习完并考完所有课程的最少天数是多少?相信经历了这么多年考试的你们,会更容易理解复习和考试的关系(doge)

代码如下

/*
将问题转换成一共有n门课程,第i门课程的复习时间为nums[i]天
changeIndices[i]课程的考试时间为第i天
复习并考完所有课程的最小时间

由于考试时间是固定的,我们需要优先考虑考试时间
=> 考试时间越靠后,就会有更加充分的时间用来复习
=> 具有单调性
=> 可以二分

check函数如何去判断是否能复习考完所有的考试?
1、贪心,我们把每门课程的考试时间尽可能往后拖延  last[]记录每门课程的最迟考试时间
2、如果考试数目<=课程数,return false; // 可以优化的点
   否则我们从前往后遍历天数,优先复习考试时间近的科目,看能否在考试之前完成复习
*/

class Solution {
public:
    int earliestSecondToMarkIndices(vector<int>& nums, vector<int>& changeIndices) {
        int n=nums.size(),m=changeIndices.size();
        auto check=[&](int k)->bool{
            vector<int>last(n,-1); // 记录下标i的最晚标记时间,即最晚考试时间
            for(int i=0;i<k;i++)
                last[changeIndices[i]-1]=i;
            for(auto x:last)
                if(x<0) // 表示有的下标没有被标记的时间,即有的课程没有考试
                    return false;
            int cnt = 0;
            for(int i=0;i<k;i++){
                int idx = changeIndices[i] - 1;
                if(last[idx]==i){//表示下标idx到了最后的被标记的时间,即课程到了考试的最后截止时间
                    cnt-=nums[idx];
                    if(cnt<0) return false;//没有足够的时间将nums[idx]置为0,即没有足够的时间复习课程
                }else{
                    cnt++;
                }
            }
            return true;
        };
        int l=1,r=m;
        while(l<=r){
            int mid = l+(r-l)/2;
            if(check(mid)) r=mid-1;
            else l=mid+1;
        }
        return l>m?-1:l;
    }
};

四、标记所有下标的最早秒数II

有情提醒一下: 该题和第三题的题目并不一样。

在这题中,操作变得更加复杂了,但其实我们还是可以借鉴第三题的思路:

首先,这题依旧能够二分,因为时间越多,越有可能标记所有下标,但是check函数的思路不一样了,我们要优先考虑清零操作,因为它是不可控的(为了方便描述,这里将题目中的几个操作分别称为减一、清零、标记)。

【如果下面的内容看不太明白,依旧可以带入上一题说的考试模型,帮助你理解---减一操作:花费一天复习一门课,清空操作:花费一天速通一门课,标记:选择一天用来考试】

1)这里就要讨论一下:清零操作和减一操作什么时候用比较合适?

1、如果nums[i]用过减一操作,还需要用清零操作吗?没必要,因为如果能清零,就没必要在花多余的时间进行减一,可以将多出的时间给其他的nums[j]

2、如果nums[i]用过清零操作,也就不需要在进行减一操作了

结论:对于nums[i]要么执行清零操作,要么就执行减一操作,不能混用

2)根据贪心:我们肯定是能清零就尽量的去执行清零,让被清零的nums[i]有更多的时间被减为0

1、清零操作是越早越好还是越晚越好?肯定是越早越好,因为我们还需要有多余的时间去标记,所以我们需要从后往前遍历,去看是否有多余的时间去标记下标,所以我们要记录每个下标的最早清零时间

2、什么时候不需要用清零操作?

  • nums[i]==0时,不需要
  • nums[i]==1时,也不需要,因为减一操作也能做到清零,且可以在任意时间执行

除了上面的两种情况,还有一种特殊的情况,即用完清零操作之后就没时间进行标记了,这里我们不是只能进行对 i 下标进行nums[i]次减一操作,而是可以看之前进行清空操作的下标中nums[j]的最小值 是否 比nums[i]小,如果小,那么显然我们可以对 j 下标进行nums[j]次减一操作,同时nums[i]就会有时间进行清零和标记,这样的方案显然会更优----反悔贪心

我们从后往前遍历,同时维护用来标记/减一的时间cnt 和 需要减一和标记的总时间sum(都不包含进行清零操作的下标的标记时间)具体如何维护看下面的代码。

class Solution {
public:
    int earliestSecondToMarkIndices(vector<int>& nums, vector<int>& changeIndices) {
        int n=nums.size(),m=changeIndices.size();
        long long total = n;
        for(auto x:nums) total += x;            
        vector<int>first_d(n,-1);
        for(int i=m-1;i>=0;i--)
            first_d[changeIndices[i]-1]=i;
        auto check=[&](int k)->bool{
            priority_queue<int,vector<int>,greater<int>> q;
            int cnt = 0;// 减一复习并考试的课程的所有时间
            long long slow = total;//记录减一操作的nums[i]及其标记需要的时间,一开始默认全用减一操作
            for(int i=k-1;i>=0;i--){
                int idx=changeIndices[i]-1;
                if(nums[idx]<=1||i!=first_d[idx]){
                    cnt++;
                    continue;
                }
                if(cnt==0){
                    if(q.empty()||nums[idx]<=q.top()){//只能进行减一操作
                        cnt++;
                        continue;
                    }
                    slow += q.top()+1;
                    q.pop();
                    cnt += 2;
                }
                slow -= nums[idx]+1;
                cnt--;
                q.push(nums[idx]);
            }
            return cnt>=slow;
        };
        int l=1,r=m;
        while(l<=r){
            int mid=l+(r-l)/2;
            if(check(mid)) r=mid-1;
            else l=mid+1;
        }
        return l>m?-1:l;
    }
};
  • 12
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值