[学习报告]《LeetCode零基础指南》(第六讲)贪心

野生的小小风256 2022-04-21 20:52:21

目录

 

前言

解题报告

1913. 两个数对之间的最大乘积差

976. 三角形的最大周长

561. 数组拆分 I

881. 救生艇

324. 摆动排序 II

455. 分发饼干

1827. 最少操作使数组递增

945. 使数组唯一的最小增量

611. 有效三角形的个数


前言

今日是九日集训打卡的第六天,今天学习的内容是贪心算法,也就局部最优解的算法。

解题报告

1913. 两个数对之间的最大乘积差

两个数对 (a, b) 和 (c, d) 之间的 乘积差 定义为 (a * b) - (c * d) 。

例如,(5, 6) 和 (2, 7) 之间的乘积差是 (5 * 6) - (2 * 7) = 16 。
给你一个整数数组 nums ,选出四个 不同的 下标 w、x、y 和 z ,使数对 (nums[w], nums[x]) 和 (nums[y], nums[z]) 之间的 乘积差 取到 最大值 。

返回以这种方式取得的乘积差中的 最大值 。

int cmp(const void*a,const void*b){
    return *(int*)a-*(int*)b;
}
int maxProductDifference(int* nums, int numsSize){
    qsort(nums,numsSize,sizeof(int),cmp);
    return (nums[numsSize-1]*nums[numsSize-2]-nums[0]*nums[1]); //贪心
}

需要返回最大乘积差值直接将最大两数相乘减去最小两项即可,直接排序后取最后两项和前两项即可。

976. 三角形的最大周长

给定由一些正数(代表长度)组成的数组 nums ,返回 由其中三个长度组成的、面积不为零的三角形的最大周长 。如果不能形成任何面积不为零的三角形,返回 0

int max(int a,int b){                                   //可以使得代码简洁
    return a>b?a:b;
}
int cmp(const void*a,const void*b){
    return *(int*)a-*(int*)b;
}
int largestPerimeter(int* nums, int numsSize){
    qsort(nums,numsSize,sizeof(int),cmp);               //这里的排序很有必要,详见题解
    int ans=0;
    for(int i=2;i<numsSize;i++){
        if((nums[i-2]+nums[i-1])>nums[i]){              //满足两边之和大于第三边面积才不为0
            ans=max(ans,nums[i]+nums[i-1]+nums[i-2]);   //通过循环与比较使得ans为最大值
        }
    }
    return ans;
}

排序的作用即为实现贪心,详见 [学习报告]《LeetCode零基础指南》(第五讲)排序(qsort)

561. 数组拆分 I

给定长度为 2n 的整数数组 nums ,你的任务是将这些数分成 n 对, 例如 (a1, b1), (a2, b2), ..., (an, bn) ,使得从 1 到 n 的 min(ai, bi) 总和最大。

返回该 最大总和 。

int cmp(const void*a,const void*b){
    return *(int*)a-*(int*)b;
}
int arrayPairSum(int* nums, int numsSize){
    int ans=0;
    qsort(nums,numsSize,sizeof(int),cmp);         //排序
    for(int i=0;i<numsSize;i+=2){                 //排序后每一组中偶数项都最小
        ans+=nums[i];                            
    }
    return ans;
}

首先对数组进行排序,当排序完成之后,按照题目所分的分法进行分对之后,总是小于 的值,也就是每一组的 min(a,b)都取 ,而 元素占据的是排序后数组的偶数下标部分,直接全部都取出加和后返回即可。

881. 救生艇

给定数组 people 。people[i]表示第 i 个人的体重 ,船的数量不限,每艘船可以承载的最大重量为 limit。

每艘船最多可同时载两人,但条件是这些人的重量之和最多为 limit。

返回 承载所有人所需的最小船数 。

int cmp(const void *a, const void *b) {
    return *(int *)a - *(int *)b;
}

int numRescueBoats(int* people, int peopleSize, int limit){
    int i;
    int l = 0, r = peopleSize-1;                     //这里的l和r实际上指的是最轻的人和最重的人
    int ans = 0;
    qsort(people, peopleSize, sizeof(int), cmp);     //按从小到大排序
    while(l <= r) {                                  //人全被送走之前
        if(l == r) {
            ++ans; break;                            // 当l=r时也就只剩下一个人了,坐一艘然后直接跳出循环即可
        } else if(people[l] + people[r] > limit) {   // 当最重和最轻都超过限制时,最重就只能自己坐一艘了
            ++ans, r--;                              //所以这时候把最重的用一艘船送走最重的变成下一个
        } else                                       //这种情况指的是最重和最轻两个人可以坐同一艘的情况
            ++ans, ++l, --r;                         //所以这个时候只需要一艘可以送走最轻和最重两个人
    }
    return ans;
}

详见注释。

324. 摆动排序 II

给你一个整数数组 nums,将它重新排列成 nums[0] < nums[1] > nums[2] < nums[3]... 的顺序。

你可以假设所有输入数组都可以得到满足题目要求的结果。

int cmp(const void *a, const void *b) {
    return *(int *)a - *(int *)b;
}
void wiggleSort(int* nums, int numsSize){
    int *ret=(int*)malloc(sizeof(int)*numsSize);
   
    for(int i=0;i<numsSize;i++){
        ret[i]=nums[i];
    } 
    qsort(ret,numsSize,sizeof(int),cmp);
    int r=numsSize-1;                               //末位元素下标
    for(int i=1;i<numsSize;i+=2){                   //先对奇数下标项填充
        nums[i]=ret[r--];                                       
    }
    for(int i=0;i<numsSize;i+=2){                   //再对偶数下标项填充
        nums[i]=ret[r--];
    }
}

这题没有需要返回数组,但是直接只对一个数组进行处理比较麻烦,因此将原数组复制了一份处理后往回填充。填充的思路如下:由题意可知,此函数的元素总数一定为偶数,于是我们先将数组排序后再分为两半来看,这样后半所有的元素都将会大于前半的元素,这下只需要把后面的那一半的元素填入前面的空隙即可,按下标来看就是填入后半的数字到奇数下标项,因此定义了 r 作为末位下标,每填一个数字之后就往前移动一格,在填完后半的数字之后刚好下标来到了前半,那就继续使用一样的办法把前半的数字填入偶数下标项。

455. 分发饼干

假设你是一位很棒的家长,想要给你的孩子们一些小饼干。但是,每个孩子最多只能给一块饼干。

对每个孩子 i,都有一个胃口值 g[i],这是能让孩子们满足胃口的饼干的最小尺寸;并且每块饼干 j,都有一个尺寸 s[j] 。如果 s[j] >= g[i],我们可以将这个饼干 j 分配给孩子 i ,这个孩子会得到满足。你的目标是尽可能满足越多数量的孩子,并输出这个最大数值。

int cmp(const void*a,const void*b){
    return *(int*)a-*(int*)b;
}
int findContentChildren(int* g, int gSize, int* s, int sSize){
    qsort(g,gSize,sizeof(int),cmp);                             
    qsort(s,sSize,sizeof(int),cmp);                             //先将两个数组排序
    int i=gSize-1,j=sSize-1;                                    //默认两个标记都在数组末位
    int ans=0;
    while(i>=0&&j>=0){                                          //当任意的数组越界时结束循环
        if(s[j]>=g[i]){                                         //如果s>=g,这时候满足条件,两个标记一起移动,ans+1
            ans++;
            j--;
            i--;
        }
        else{                                                   //如果不满足条件,说明这个小朋友已经满足不了了,看下一个
            i--;
        }
    }
    return ans;
}

此题既然需要最多满足条件的解,那么就需要排序了,首先对两个数组全都进行排序。然后分别在两个数组的末位元素留一个标记,随后开始循环进行饼干分配(一旦有哪个数组的下标先越界了就结束循环)。如果分量 s 大于食量 那么这个时候小朋友就被满足了,因此饼干减一,需要满足的小朋友数量减一。那么一旦当这个饼干不可以满足小朋友的时候怎么办呢?身为一个好家长,如果连目前最大的饼干都已经满足不了这个小朋友了,那这个小朋友就只能先不去管,去看下一个有可能被满足的小朋友了,因此只有食量 g 数组的下标移动。

1827. 最少操作使数组递增

给你一个整数数组 nums (下标从 0 开始)。每一次操作中,你可以选择数组中一个元素,并将它增加 1 。

比方说,如果 nums = [1,2,3] ,你可以选择增加 nums[1] 得到 nums = [1,3,3] 。
请你返回使 nums 严格递增 的 最少 操作次数。

我们称数组 nums 是 严格递增的 ,当它满足对于所有的 0 <= i < nums.length - 1 都有 nums[i] < nums[i+1] 。一个长度为 1 的数组是严格递增的一种特殊情况。

int minOperations(int* nums, int numsSize){
    if(numsSize==1){
        return 0;
    }
    int ans=0;
    for(int i=1;i<numsSize;i++){
        while(nums[i]<=nums[i-1]){
            nums[i]++;
            ans++;
        }
    }
    return ans;
}

先来看看暴力的解法,把特殊情况(数组长度为1)去掉之后,直接开始循环,如果当前元素比前一个元素小,就一直循环加一到比前一个数大,然后同时操作次数同步加一。很暴力,但是效率低的离谱(过啦!!320ms击败了百分之9的人!),而其实稍微想一想也想的明白,其实只要把内部那个循环加一改一改,数字加几,操作数就加几就好了。

int minOperations(int* nums, int numsSize){
    if(numsSize==1){
        return 0;
    }
    int ans=0;
    for(int i=1;i<numsSize;i++){
        if(nums[i]<=nums[i-1]){
            ans=ans+(nums[i-1]-nums[i]+1);
            nums[i]=nums[i-1]+1;
        }
    }
    return ans;
}

效率超大幅提升。(320ms→8ms)

945. 使数组唯一的最小增量

给你一个整数数组 nums 。每次 move 操作将会选择任意一个满足 0 <= i < nums.length 的下标 i,并将 nums[i] 递增 1。

返回使 nums 中的每个值都变成唯一的所需要的最少操作次数。

int cmp(const void*a,const void*b){
    return *(int*)a-*(int*)b;
}
int minIncrementForUnique(int* nums, int numsSize){
    if(numsSize==1){
        return 0;
    }
    qsort(nums,numsSize,sizeof(int),cmp);
    int ans=0;
    for(int i=1;i<numsSize;i++){
         if(nums[i]<=nums[i-1]){
            ans=ans+(nums[i-1]-nums[i]+1);
            nums[i]=nums[i-1]+1;
            }
    }
    return ans;
}

与上题很类似,不过这题可以要先排序一下,这样在操作之后可以保证肯定没有重复。

611. 有效三角形的个数

给定一个包含非负整数的数组 nums ,返回其中可以组成三角形三条边的三元组个数。

int cmp(const void*a,const void*b){
    return *(int*)a-*(int*)b;
}
int triangleNumber(int* nums, int numsSize){
    qsort(nums,numsSize,sizeof(int),cmp);                //首先先对数组进行排序
    int i=0,j=1,k=numsSize-1;                            //三角形有三条边,这下需要三个变量来控制下标了,从两头逼近
    int ans=0;
    for(j=1;j<numsSize-1;j++){                           //j开始向右逼近
        for(i=0;i<j;i++){                                //i从0开始到j-1每一种情况列出
            while(i<j&&k>j){                             //防止元素重合
                 if(nums[k]>=nums[i]+nums[j]){           //找到满足三角形的k下标
                    k--;                                
                 }else{
                    ans=ans+(k-j);                       //从k开始到j+1的所有元素都满足
                    break;
                 }
             }
             k=numsSize-1;                               //重置k
        }
    }
    return ans;               
}

这是不贪心(1700+ms)的暴力解法,详细题解都在注释中写出。接下来看一种高效的写法:

int cmp(const void *a, const void *b) {
    return *(int *)a - *(int *)b;
}

int triangleNumber(int* nums, int numsSize){
    int i, j, k;
    int ans = 0;
    qsort(nums, numsSize, sizeof(int), cmp);       //首先进行排序
    for(i = 0; i < numsSize; ++i) {
        j = i + 1;
        k = j + 1;                                 //这样可以防止越界
        while(j < numsSize) {
            while(k < numsSize) {
                if(nums[i] + nums[j] <= nums[k]) {
                    break;                         // 一旦出现两边只和小于第三边就没必要继续了,后面只会更大
                }
                ++k;                               //在过大之前一直增加下标,这其中的元素都符合要求
            }
            ans += k-j-1;                          // 这是最后的k到j+1之间所有元素都满足
            ++j;                                   // 把j向k逼近
            if(k == j) k++;                        //防止元素重合
        }
        
    }    
    return ans;
}

这种写法与上面那种写法的区别很大,用 推进,一步步的将范围缩小,而且 和 是一起走的,所以复杂度只有O(n²)。而上面那种写法实际上只是一种枚举,和 k 各走各的,当 移动时也并没有缩小范围。复杂度是O(n³)。

...全文
45 回复 打赏 收藏 转发到动态 举报
写回复
用AI写文章
回复
切换为时间正序
请发表友善的回复…
发表回复

64,192

社区成员

发帖
与我相关
我的任务
社区描述
学习「 算法 」的捷径就是 「 题海战略 」,社区由「 夜深人静写算法 」作者创建,三年ACM经验,校集训队队长,亚洲区域赛金牌,世界总决赛选手。社区提供系统的训练,答疑解惑,面试经验,大厂内推等机会
社区管理员
  • 英雄哪里出来
  • 芝麻粒儿
  • Amy卜bo皮
加入社区
  • 近7日
  • 近30日
  • 至今
社区公告

QQ群:480072171

英雄算法交流 8 群

 

试试用AI创作助手写篇文章吧