代码随想录 刷题记录-16 贪心算法(1)贪心理论基础及习题

news2024/9/22 23:35:07

一、理论基础

什么是贪心

贪心的本质是选择每一阶段的局部最优,从而达到全局最优

贪心的套路(什么时候用贪心)

贪心算法并没有固定的套路

所以唯一的难点就是如何通过局部最优,推出整体最优。

靠自己手动模拟,如果模拟可行,就可以试一试贪心策略,如果不可行,可能需要动态规划。

如何验证可不可以用贪心算法呢?

最好用的策略就是举反例,如果想不到反例,那么就试一试贪心吧

刷题或者面试的时候,手动模拟一下感觉可以局部最优推出整体最优,而且想不到反例,那么就试一试贪心

那么刷题的时候什么时候真的需要数学推导呢?

例如这道题目:链表:环找到了,那入口呢? ,这道题不用数学推导一下,就找不出环的起始位置,想试一下就不知道怎么试,这种题目确实需要数学简单推导一下。

贪心一般解题步骤

贪心算法一般分为如下四步:

  • 将问题分解为若干个子问题
  • 找出适合的贪心策略
  • 求解每一个子问题的最优解
  • 将局部最优解堆叠成全局最优解

这个四步其实过于理论化了,我们平时在做贪心类的题目 很难去按照这四步去思考,真是有点“鸡肋”。

做题的时候,只要想清楚 局部最优 是什么,如果推导出全局最优,其实就够了。

二、习题

1.455.分发饼干

这里的局部最优就是大饼干喂给胃口大的,充分利用饼干尺寸喂饱一个,全局最优就是喂饱尽可能多的小孩

可以尝试使用贪心策略,先将饼干数组和小孩数组排序。

然后从后向前遍历小孩数组,用大饼干优先满足胃口大的,并统计满足小孩数量。

这个例子可以看出饼干 9 只有喂给胃口为 7 的小孩,这样才是整体最优解,并想不出反例,那么就可以撸代码了。

class Solution {
    public int findContentChildren(int[] g, int[] s) {
        Arrays.sort(g);
        Arrays.sort(s);
        int i = g.length-1 , j = s.length-1;
        int cnt = 0;
        while(j >= 0 && i>=0){
            if(s[j] >= g[i]){
                cnt++;
                j--;
            }
            i--;
        }
        return cnt;
    }
}

其他思路

也可以换一个思路,小饼干先喂饱小胃口

class Solution {
public:
    int findContentChildren(vector<int>& g, vector<int>& s) {
        sort(g.begin(),g.end());
        sort(s.begin(),s.end());
        int index = 0;
        for(int i = 0; i < s.size(); i++) { // 饼干
            if(index < g.size() && g[index] <= s[i]){ // 胃口
                index++;
            }
        }
        return index;
    }
};

总结

思考的过程:想清楚局部最优,想清楚全局最优,感觉局部最优是可以推出全局最优,并想不出反例,那么就试一试贪心。

2.376. 摆动序列

思路

思路 1(贪心解法)

本题要求通过从原始序列中删除一些(也可以不删除)元素来获得子序列,剩下的元素保持其原始顺序。

要求删除元素使其达到最大摆动序列,应该删除什么元素呢?

用示例二来举例,如图所示:

局部最优:删除单调坡度上的节点(不包括单调坡度两端的节点),那么这个坡度就可以有两个局部峰值

整体最优:整个序列有最多的局部峰值,从而达到最长摆动序列

局部最优推出全局最优,并举不出反例,那么试试贪心。

实际操作上,其实连删除的操作都不用做,因为题目要求的是最长摆动子序列的长度,所以只需要统计数组的峰值数量就可以了(相当于是删除单一坡度上的节点,然后统计长度)

这就是贪心所贪的地方,让峰值尽可能的保持峰值,然后删除单一坡度上的节点

在计算是否有峰值的时候,大家知道遍历的下标 i ,计算 prediff(nums[i] - nums[i-1]) 和 curdiff(nums[i+1] - nums[i]),如果prediff < 0 && curdiff > 0 或者 prediff > 0 && curdiff < 0 此时就有波动就需要统计。

这是我们思考本题的一个大体思路,但本题要考虑三种情况:

  1. 情况一:上下坡中有平坡
  2. 情况二:数组首尾两端
  3. 情况三:单调坡中有平坡
情况一:上下坡中有平坡

在图中,当 i 指向第一个 2 的时候,prediff > 0 && curdiff = 0 ,当 i 指向最后一个 2 的时候 prediff = 0 && curdiff < 0

如果我们采用,删左面三个 2 的规则,那么 当 prediff = 0 && curdiff < 0 也要记录一个峰值,因为他是把之前相同的元素都删掉留下的峰值。

所以我们记录峰值的条件应该是: (preDiff <= 0 && curDiff > 0) || (preDiff >= 0 && curDiff < 0),为什么这里允许 prediff == 0 ,就是为了 上面我说的这种情况。

情况二:数组首尾两端

可以写死,就是 如果只有两个元素,且元素不同,那么结果为 2。

不写死的话,可以假设,数组最前面还有一个数字,那这个数字应该是什么呢?

之前我们在 讨论 情况一:相同数字连续 的时候, prediff = 0 ,curdiff < 0 或者 >0 也记为波谷。

那么为了规则统一,针对序列[2,5],可以假设为[2,2,5],这样它就有坡度了即 preDiff = 0,如图:

针对以上情形,result 初始为 1(默认最右面有一个峰值),此时 curDiff > 0 && preDiff <= 0,那么 result++(计算了左面的峰值),最后得到的 result 就是 2(峰值个数为 2 即摆动序列长度为 2)

经过以上分析后,我们可以写出如下代码:

// 版本一
class Solution {
public:
    int wiggleMaxLength(vector<int>& nums) {
        if (nums.size() <= 1) return nums.size();
        int curDiff = 0; // 当前一对差值
        int preDiff = 0; // 前一对差值
        int result = 1;  // 记录峰值个数,序列默认序列最右边有一个峰值
        for (int i = 0; i < nums.size() - 1; i++) {
            curDiff = nums[i + 1] - nums[i];
            // 出现峰值
            if ((preDiff <= 0 && curDiff > 0) || (preDiff >= 0 && curDiff < 0)) {
                result++;
            }
            preDiff = curDiff;
        }
        return result;
    }
};
情况三:单调坡度有平坡

在版本一中,我们忽略了一种情况,即 如果在一个单调坡度上有平坡,例如[1,2,2,2,3,4],如图:

单调中的平坡 不能算峰值(即摆动)。

之所以版本一会出问题,是因为我们实时更新了 prediff。

那么我们应该什么时候更新 prediff 呢?

我们只需要在 这个坡度 摆动变化的时候,更新 prediff 就行,这样 prediff 在 单调区间有平坡的时候 就不会发生变化,造成我们的误判。

代码如下:

public int wiggleMaxLength(int[] nums) {
        int preDiff = 0;
        int curDiff = 0; 
        int result = 1;
        for(int i = 0 ; i < nums.length - 1 ; i++){
            curDiff = nums[i+1] - nums[i];
            if((preDiff >= 0 && curDiff < 0) || (preDiff <=0 && curDiff > 0)){
                result++;
                preDiff = curDiff;
            }
        }
        return result;
    }

3.53. 最大子序和

思路

暴力解法

两层for循环

贪心解法

贪心贪的是哪里呢?

如果 -2 1 在一起,计算起点的时候,一定是从 1 开始计算,因为负数只会拉低总和,这就是贪心贪的地方!

局部最优:当前“连续和”为负数的时候立刻放弃,从下一个元素重新计算“连续和”,因为负数加上下一个元素 “连续和”只会越来越小。

全局最优:选取最大“连续和”

局部最优的情况下,并记录最大的“连续和”,可以推出全局最优

从代码角度上来讲:遍历 nums,从头开始用 count 累积,如果 count 一旦加上 nums[i]变为负数,那么就应该从 nums[i+1]开始从 0 累积 count 了,因为已经变为负数的 count,只会拖累总和。

这相当于是暴力解法中的不断调整最大子序和区间的起始位置

区间的终止位置,其实就是如果 count 取到最大值,及时记录。

代码如下:

class Solution {
    public int maxSubArray(int[] nums) {
        int max = Integer.MIN_VALUE;
        int tmp = 0;
        for(int i = 0 ; i < nums.length ; i++){
            tmp += nums[i];
            max = Math.max(tmp, max);
            if(tmp < 0) tmp = 0;
        }
        return max;
    }
}

4.122.买卖股票的最佳时机 II

思路

本题首先要清楚两点:

  • 只有一只股票!
  • 当前只有买股票或者卖股票的操作

想获得利润至少要两天为一个交易单元。

贪心算法

贪心策略:最高利润是可以分解的,只要股票在上涨,就可以用今日减昨日的差价做利润累加,直到最高点。这样只要收集所有的正利润即可。

局部最优:收集每天的正利润,全局最优:求得最大利润

5.122.买卖股票的最佳时机 II

贪心的思路,局部最优:当前可移动距离尽可能多走,如果还没到终点,步数再加一。整体最优:一步尽可能多走,从而达到最少步数。

思路虽然是这样,但在写代码的时候还不能真的能跳多远就跳多远,那样就不知道下一步最远能跳到哪里了。

所以真正解题的时候,要从覆盖范围出发,不管怎么跳,覆盖范围内一定是可以跳到的,以最小的步数增加覆盖范围,覆盖范围一旦覆盖了终点,得到的就是最少步数!

这里需要统计两个覆盖范围,当前这一步的最大覆盖和下一步最大覆盖

如果移动下标达到了当前这一步的最大覆盖最远距离了,还没有到终点的话,那么就必须再走一步来增加覆盖范围,直到覆盖范围覆盖了终点。

如图:

方法一

class Solution {
    public int jump(int[] nums) {
        //nowCover是当前可以处于的位置
        //nextCover 是跳一次后可以处于的位置
        int res = 0;
        int nowCover = 0;
        int nextCover = 0;
        if(nums.length == 1) return 0;
        for(int i = 0 ; i < nums.length ; i++){
            if(nowCover >= nums.length - 1) break;
            nextCover = Math.max(nextCover,nums[i]+i);
            if(i == nowCover){//遍历到当前所处的边界了
                res++;
                nowCover = nextCover;
            } 
        }
        return res;
    }
}

方法二

让移动下标最大只能移动到 nums.size - 2 的地方

如果移动下标等于当前覆盖最大距离下标, 需要再走一步(即 ans++),因为最后一步一定是可以到的终点。(题目假设总是可以到达数组的最后一个位置),如图:

  • 如果移动下标不等于当前覆盖最大距离下标,说明当前覆盖最远距离就可以直接达到终点了,不需要再走一步。如图:

6.1005.K次取反后最大化的数组和

思路

贪心的思路,局部最优:让绝对值大的负数变为正数,当前数值达到最大,整体最优:整个数组和达到最大。

局部最优可以推出全局最优。

如果将负数都转变为正数了,K依然大于0,此时的问题是一个有序正整数序列,如何转变K次正负,让 数组和 达到最大。

贪心:局部最优:只找数值最小的正整数进行反转,当前数值和可以达到最大(例如正整数数组{5, 3, 1},反转1 得到-1 比 反转5得到的-5 大多了),全局最优:整个 数组和 达到最大。

代码如下:

class Solution {
    public int largestSumAfterKNegations(int[] nums, int k) {
        int sum = 0;
        while(k-- > 0){
            Arrays.sort(nums);
            nums[0] = - nums[0];
        }
        for(int i = 0 ; i<nums.length ; i++){
            sum += nums[i];
        }
        return sum;
    }
}

上面的时间复杂度为O(k*n*logn),改进如下:

class Solution {
    public int largestSumAfterKNegations(int[] nums, int k) {
        Arrays.sort(nums);
        int sum = 0;
        //先处理负数
        for(int i = 0 ; i < nums.length && nums[i] < 0 && k > 0 ; i++,k--){
            nums[i] = -nums[i];
        }
        //再判断k是否用尽,没用尽则对最小正数进行翻转
        if(k % 2 == 1){
            Arrays.sort(nums);
            nums[0] = -nums[0];
        }
        for(int t : nums){
            sum += t;
        }
        return sum;
    }
}

总结参考:本周小结!(贪心算法系列二)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2071677.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

深度学习 回归问题

1. 梯度下降算法 深度学习中, 梯度下降算法是是一种很重要的算法. 梯度下降算法与求极值的方法非常类似, 其核心思想是求解 x ′ x x′, 使得 x ′ x x′ 在取 x ⋆ x^{\star} x⋆ 时, 可以使得 l o s s 函数 loss函数 loss函数 的值最小. 其中, 在求解 x ′ x x′ 的过…

罗德与施瓦茨RS、UPV 音频分析仪 250KHZ 双通道分析仪UPL

罗德与施瓦茨 UPV 音频分析仪的规格包括&#xff1a; 模拟 双通道分析仪&#xff1a;带宽高达 250 kHz 生成正弦波信号&#xff1a;单通道最高 185 kHz&#xff08;需要 B1&#xff09;和双通道最高 80 kHz FFT本底噪声&#xff1a;< -140dB 固有频率响应&#xff08;20 …

链动 2+1 模式小程序 AI 智能名片商城源码培训邀约策略研究

摘要&#xff1a;本文深入剖析链动 21 模式小程序 AI 智能名片商城源码的培训邀约策略&#xff0c;从该源码的价值出发&#xff0c;阐述邀约的重要性&#xff0c;并详细介绍具体的邀约策略&#xff0c;旨在为相关培训活动提供切实可行的指导&#xff0c;提高邀约成功率&#xf…

前端如何快速切换node版本:nvm

安装之前最好卸载计算机已经安装的node&#xff08;通过Windows菜单找到Node.js的卸载程序&#xff0c;运行卸载程序&#xff09;。下载nvm安装包&#xff1a;nvm安装地址。安装nvm&#xff0c;选择nvm安装根路径指定nodejs的安装路径打开命令行&#xff0c;输入nvm -v 可查看版…

Object.create的原型继承

● 首先我们来从这种方法来创建一个和之前一样计算年龄的方法 const PersonProto {cacleAge() {console.log(2038 - birthYear);} };const zhangsan Object.create(PersonProto); console.log(zhangsan);● 发现确实可以实现原型继承的特性 const PersonProto {cacleAge()…

odoo17 group col 属性

odoo17 group col 属性 以前版本&#xff0c;col4,在17中不能用了&#xff0c;或者方法变了 <record id"hetong.addfj_wizard" model"ir.ui.view"><field name"name">合同附件</field><field name"model">het…

免费的大模型插件llm.nvim

llm.nvim&#xff08;https://github.com/StubbornVegeta/llm.nvim&#xff09;是一款基于cloudflare的免费大模型插件&#xff0c;你可以像使用ChatGPT一样和它进行对话 在使用这款插件之前&#xff0c;你需要注册cloudflare&#xff0c;获取你的account和API key。你可以在这…

RCE - - 无字母数字远程命令执行

题目源码 <?php if(isset($_GET[code])){$code $_GET[code];if(strlen($code)>35){die("Long.");}if(preg_match("/[A-Za-z0-9_$]/",$code)){die("NO.");}eval($code); }else{highlight_file(__FILE__); } 分析 这道题 code 接 get 传…

【Qt】常用控件QProgressBar

常用控件QProgressBar 使用QProgressBar表示一个进度条&#xff01;&#xff01;&#xff01; QProgressBar的核心属性 属性说明 minimum 进度条最⼩值 maximum 进度条最⼤值 value 进度条当前值 alignment ⽂本在进度条中的对⻬⽅式. Qt::AlignLeft : 左对⻬Qt::Alig…

AJAX(4)——XMLHttpRequest

XMLHttpRequest 定义&#xff1a;XMLHttpRequest(XHR)对象用于与服务器交互。通过XMLHttpRequest可以在不刷新页面的情况下请求特定URL&#xff0c;获取数据。这允许网页在不影响用于操作的情况下&#xff0c;更新页面的局部内容。XMLHttpRequest在AJAX编程中被大量使用 关系…

第6章 B+树索引

目录 6.1 没有索引的查找 6.1.1 在一个页中的查找 6.1.2 在很多页中查找 6.2 索引 6.2.1 一个简单的索引方案 6.2.2 InnoDB中的索引方案 6.2.2.1 聚簇索引 6.2.2.2 二级索引 6.2.2.3 联合索引 6.2.3 InnoDB的B树索引的注意事项 6.2.3.1 根页面万年不动窝 6.2.3.2 内节…

MYSQL————数据库的约束

1.约束类型 1.not null&#xff1a;指示某列不能存储null值 2.unique&#xff1a;保证某列的每行必须有唯一值 3.default&#xff1a;规定没有给列赋值时的默认值 4.primary key&#xff1a;not null和unique的结合。确保某列&#xff08;或两个或多个列的结合&#xff09;有唯…

qtcreator的vim模式下commit快捷键ctrl+g,ctrl+c没有反应的问题

首先开启vim后&#xff0c;CtrlG&#xff0c;CtrlC无法用 解决&#xff1a; 工具 -> 选项->FakeVim 转到Ex Command Mapping 搜索Commit 底栏Regular expression 输入commit &#xff08;理论上可以是随意的单词&#xff09; 设置好后&#xff0c;以后要运行&#x…

vue+uniapp

#vue支持的语法&#xff0c;基本上可以做uniapp中所使用&#xff08;指绝大部分&#xff09; #知识点&#xff1a;插值表达式&#xff0c;响应式&#xff0c;指令&#xff0c;事件&#xff0c;指令修饰符 #拥有一些案例&#xff0c;补充&#xff0c;以及说明了如何在vscode运…

如何在 Android 智能手机上恢复已删除的图片

面对现实&#xff0c;从手机图库中丢失照片总是令人不安的&#xff0c;无论您是无意中删除了它们&#xff0c;还是甚至出于冲动而生气。但是&#xff0c;我们在这里告诉您&#xff0c;与大多数人的看法相反&#xff0c;从画廊中删除图像并不会使它们不可挽回地丢失。以下是一些…

【MySQL进阶之路】内外链接

目录 内连接 外连接 左外连接 右外连接 个人主页&#xff1a;东洛的克莱斯韦克-CSDN博客 内连接 内连接实际上就是利用where子句对两种表形成的笛卡儿积进行筛选 select 字段 from 表1 inner join 表2 on 连接条件 and 其他条件&#xff1b; 外连接 外连接分为左外连接和…

【Java】—— 数组元素的查找:顺序查找与二分查找

目录 1、顺序查找 2、二分查找 1、顺序查找 在Java编程中&#xff0c;我们经常需要查找数组中某个元素的下标。有时&#xff0c;我们需要找到该元素第一次出现的位置&#xff0c;而有时则需要找到最后一次出现的位置。在本文中&#xff0c;我们将重点介绍如何查找元素第一次出…

AI依赖的隐患:技术能力退化、安全风险与社会不平等的未来

现代科技的浪潮中&#xff0c;ChatGPT等人工智能工具已经成为我们工作和生活的得力助手。然而&#xff0c;当这种便利变成了依赖&#xff0c;潜在的风险开始显现。过度依赖AI不仅可能导致技术能力的严重退化&#xff0c;还可能加剧信息安全问题和社会不平等。让我们深度剖析这三…

智慧社区信息系统建设:数据可视化与原型设计的力量

在数字化浪潮的推动下&#xff0c;智慧社区作为城市治理现代化的重要一环&#xff0c;正以前所未有的速度改变着我们的生活方式。智慧社区信息系统&#xff0c;作为支撑这一变革的核心&#xff0c;不仅要求高效的数据处理能力&#xff0c;还需具备直观的数据展示与强大的用户交…

zdppy+vue3+onlyoffice文档管理系统实战 20240823上课笔记 zdppy_cache框架的低代码实现

遗留问题 1、封装API2、有账号密码3、查询所有有效的具体数据&#xff0c;也就是缓存的所有字段 封装查询所有有效具体数据的方法 基本封装 def get_all(self, is_activeTrue, limit100000):"""遍历数据库中所有的key&#xff0c;默认查询所有没过期的:para…