代码随想录1刷—贪心算法篇(二)

  • [452. 用最少数量的箭引爆气球](https://leetcode.cn/problems/minimum-number-of-arrows-to-burst-balloons/)
  • [435. 无重叠区间](https://leetcode.cn/problems/non-overlapping-intervals/)
  • [763. 划分字母区间](https://leetcode.cn/problems/partition-labels/)
    • 方法一
    • 方法二
  • [56. 合并区间](https://leetcode.cn/problems/merge-intervals/)
    • 拓展:lambda表达式
  • [738. 单调递增的数字](https://leetcode.cn/problems/monotone-increasing-digits/)
    • 暴力解法
    • 贪心算法
  • [714. 买卖股票的最佳时机含手续费](https://leetcode.cn/problems/best-time-to-buy-and-sell-stock-with-transaction-fee/)
    • 贪心算法
    • 动态规划
      • 优化
  • [968. 监控二叉树](https://leetcode.cn/problems/binary-tree-cameras/)
    • 情况分析

452. 用最少数量的箭引爆气球

局部最优:当气球出现重叠,一起射,所用弓箭最少。全局最优:把所有气球射爆所用弓箭最少。

如果真实的模拟射气球的过程,应该射一个,气球数组就remove一个元素,这样最直观,毕竟气球被射了。

但仔细思考一下就发现:为了让气球尽可能的重叠,需要对数组进行排序。如果把气球排序之后,从前到后遍历气球,被射过的气球仅仅跳过就行了,没有必要让气球数组remote气球,只要记录一下箭的数量就可以了。

如果气球重叠了,重叠气球中右边边界的最小值 之前的区间一定需要一个弓箭

注意题目中说的是:满足 xstart ≤ x ≤ xend,则该气球会被引爆。那么说明两个气球挨在一起不重叠也可以一起射爆,、所以代码中 if (points[i][0] > points[i - 1][1]) 不能是>=

class Solution {private:static bool cmp(const vector<int>& a, const vector<int>& b){return a[0] < b[0];}
public:int findMinArrowShots(vector<vector<int>>& points) {if(points.size() == 0)return 0;sort(points.begin(), points.end(), cmp);int result = 1;for(int i = 1; i < points.size(); i++){if(points[i][0] > points[i-1][1]){   //气球i和i-1不挨着result++;}else{points[i][1] = min(points[i-1][1],points[i][1]);    //更新重叠气球的最小右边界}}return result;}
};

435. 无重叠区间

按照右边界排序,就要从左向右遍历,因为右边界越小越好,只要右边界越小,留给下一个区间的空间就越大,所以从左向右遍历,优先选右边界小的。

按照左边界排序,就要从右向左遍历,因为左边界数值越大越好(越靠右),这样就给前一个区间的空间就越大,所以可以从右向左遍历。

一些同学做这道题目可能真的去模拟去重复区间的行为,这是比较麻烦的,还要去删除区间。题目只是要求移除区间的个数,没有必要去真实的模拟删除区间!按照右边界排序,从左向右记录非交叉区间的个数。最后用区间总数减去非交叉区间的个数就是需要移除的区间个数了。此时问题就是要求非交叉区间的最大个数。

class Solution {private:static bool cmp(vector<int>& a,vector<int>& b){return a[1] < b[1]; //按右边界从小到大排序}
public:int eraseOverlapIntervals(vector<vector<int>>& intervals) {if(intervals.size() == 0)return 0;sort(intervals.begin(),intervals.end(),cmp);int count = 1;  //记录非交叉区间的个数int end = intervals[0][1];  //记录区间的分割点for(int i = 1;i< intervals.size();i++){if(end <= intervals[i][0]){end = intervals[i][1];count++;}}return intervals.size() - count;}
};
//用452的思路也是一样的,弓箭的数量就相当于是非交叉区间的数量,只要把弓箭那道题目代码里射爆气球的判断条件加个等号(认为[0,1][1,2]不是相邻区间),然后用总区间数减去弓箭数量 就是要移除的区间数量了class Solution {public:// 按照区间右边界排序static bool cmp (const vector<int>& a, const vector<int>& b) {return a[1] < b[1];}int eraseOverlapIntervals(vector<vector<int>>& intervals) {if (intervals.size() == 0) return 0;sort(intervals.begin(), intervals.end(), cmp);int result = 1; // points 不为空至少需要一支箭for (int i = 1; i < intervals.size(); i++) {if (intervals[i][0] >= intervals[i - 1][1]) {result++; // 需要一支箭}else {  // 气球i和气球i-1挨着intervals[i][1] = min(intervals[i - 1][1], intervals[i][1]); // 更新重叠气球最小右边界}}return intervals.size() - result;}
};

763. 划分字母区间

方法一

在遍历的过程中相当于是要找每一个字母的边界,如果找到之前遍历过的所有字母的最远边界,说明这个边界就是分割点了。此时前面出现过所有字母,最远也就到这个边界了。

可以分为如下两步:

  • 统计每一个字符最后出现的位置
  • 从头遍历字符,并更新字符的最远出现下标,如果找到字符最远出现位置下标和当前下标相等了,则找到了分割点

class Solution {public:vector<int> partitionLabels(string s) {int hash[26] = {0};//hash每个格子代表一个字母//注意,申请hash[26]那数组的下标就是0-25,a对应下标0的位置//为了防止数组越界 一般会多开一格作为保护 所以开hash[27]比较合适for(int i = 0;i < s.size();i++){hash[s[i]-'a'] = i;}vector<int> result;int left = 0,right = 0;for(int i = 0;i < s.size();i++){right = max(right,hash[s[i]-'a']);if(i == right){result.push_back(right-left+1);//返回一个表示每个字符串片段的长度的列表left = i+1;}}return result;}
};

方法二

提供一种与 452.用最少数量的箭引爆气球 、435.无重叠区间 相同的思路。

统计字符串中所有字符的起始和结束位置,记录这些区间(实际上也就是 435.无重叠区间 题目里的输入),将区间按左边界从小到大排序,找到边界将区间划分成组,互不重叠。找到的边界就是答案。

class Solution {public:static bool cmp(vector<int> &a, vector<int> &b) {return a[0] < b[0];}// 记录每个字母出现的区间vector<vector<int>> countLabels(string s) {vector<vector<int>> hash(26, vector<int>(2, INT_MIN));vector<vector<int>> hash_filter;for (int i = 0; i < s.size(); ++i) {if (hash[s[i] - 'a'][0] == INT_MIN) {hash[s[i] - 'a'][0] = i;}hash[s[i] - 'a'][1] = i;}// 去除字符串中未出现的字母所占用区间for (int i = 0; i < hash.size(); ++i) {if (hash[i][0] != INT_MIN) {hash_filter.push_back(hash[i]);}}return hash_filter;}vector<int> partitionLabels(string s) {vector<int> res;// 这一步得到的 hash 即为无重叠区间题意中的输入样例格式:区间列表// 只不过现在我们要求的是区间分割点vector<vector<int>> hash = countLabels(s);// 按照左边界从小到大排序sort(hash.begin(), hash.end(), cmp);// 记录最大右边界int rightBoard = hash[0][1];int leftBoard = 0;for (int i = 1; i < hash.size(); ++i) {// 由于字符串一定能分割,因此,// 一旦下一区间左边界大于当前右边界,即可认为出现分割点if (hash[i][0] > rightBoard) {res.push_back(rightBoard - leftBoard + 1);leftBoard = hash[i][0];}rightBoard = max(rightBoard, hash[i][1]);}// 最右端res.push_back(rightBoard - leftBoard + 1);return res;}
};

56. 合并区间

按照左边界排序,排序之后局部最优:每次合并都取最大的右边界,这样就可以合并更多的区间了;

整体最优:合并所有重叠的区间。

class Solution {public:// 按照区间左边界从小到大排序static bool cmp (const vector<int>& a, const vector<int>& b) {return a[0] < b[0];}vector<vector<int>> merge(vector<vector<int>>& intervals) {vector<vector<int>> result;if (intervals.size() == 0) return result;sort(intervals.begin(), intervals.end(), cmp);bool flag = false; // 标记最后一个区间有没有合并int length = intervals.size();for (int i = 1; i < length; i++) {int start = intervals[i - 1][0];    // 初始为i-1区间的左边界int end = intervals[i - 1][1];      // 初始i-1区间的右边界while (i < length && intervals[i][0] <= end) { // 合并区间end = max(end, intervals[i][1]);    // 不断更新右区间if (i == length - 1) flag = true;   // 最后一个区间也合并了i++;                                // 继续合并下一个区间//因为此处需要继续合并后面的 用了i++ 所以while里边界条件一定要处理好//不写i < length会越界访问}result.push_back({start, end});}// 如果最后一个区间没有合并,将其加入resultif (flag == false) {result.push_back({intervals[length - 1][0], intervals[length - 1][1]});}return result;}
};
//简洁版~class Solution {public:// 按照区间左边界从小到大排序static bool cmp (const vector<int>& a, const vector<int>& b) {return a[0] < b[0];}vector<vector<int>> merge(vector<vector<int>>& intervals) {vector<vector<int>> result;if (intervals.size() == 0) return result;sort(intervals.begin(), intervals.end(), cmp);result.push_back(intervals[0]);for (int i = 1; i < intervals.size(); i++) {if (result.back()[1] >= intervals[i][0]) { // 合并区间result.back()[1] = max(result.back()[1], intervals[i][1]);} else {result.push_back(intervals[i]);}}return result;}
};

拓展:lambda表达式

lambda 表达式定义了一个匿名函数,并且可以捕获一定范围内的变量。lambda 表达式的语法形式可简单归纳为: [ capture ] ( params ) opt -> ret { body; };

其中 capture 是捕获列表,params 是参数表,opt 是函数选项,ret 是返回值类型, body 是函数体。另外,lambda 表达式在没有参数列表时,参数列表是可以省略的。

[](const vector<int>& a, const vector<int>& b){return a[0] < b[0];}//表示的就是:static bool cmp (const vector<int>& a, const vector<int>& b) {return a[0] < b[0];}

738. 单调递增的数字

暴力解法

直接从n到0一个个试,试出最大的合法数字。

class Solution {private:bool checkNum(int num) {int max = 10;while (num) {int t = num % 10;if (max >= t) max = t;  //测是不是每一位都大于后一位,然后更新else return false;      //不大于则不合法,直接返回falsenum = num / 10;}return true;}
public:int monotoneIncreasingDigits(int N) {for (int i = N; i > 0; i--) {if (checkNum(i)) return i;  //从n到0一个个试是不是合法……}return 0;}
};

贪心算法

局部最优:遇到strNum[i - 1] > strNum[i]的情况,让strNum[i - 1]--,然后strNum[i]给为9,可以保证这两位变成最大单调递增整数全局最优:得到小于等于N的最大单调递增的整数

数字:332,从前向后遍历的话,那么就把变成了329,此时2又小于了第一位的3了,真正的结果应该是299。所以从前后向遍历会改变已经遍历过的结果!那么从后向前遍历,就可以重复利用上次比较得出的结果了,从后向前遍历332的数值变化为:332 -> 329 -> 299

class Solution {public:int monotoneIncreasingDigits(int n) {string strNumber = to_string(n);    //将int类型数字转换为字符串数组~int flag = strNumber.size();    //flag用于标记9的赋值从哪一位开始进行//设置为这个默认值,为了防止第二个for循环在flag没有被赋值的情况下执行for(int i = strNumber.size() - 1; i > 0; i--){if(strNumber[i - 1] > strNumber[i]){  //相邻位数上的数字违背单调递增flag = i;   //标记9的赋值从哪一位开始进行strNumber[i - 1]--;   //赋值的前一位进行-1操作。}}for(int i = flag; i < strNumber.size(); i++){strNumber[i] = '9';}//如果没有出现相邻位数上的数字违背单调递增的原则,则在第一个循环内不做操作,不进入第二个循环,直接返回原数字return stoi(strNumber); //注意返回值要恢复为int类型 所以字符串转int stoi函数}
};

714. 买卖股票的最佳时机含手续费

贪心算法

在 122.买卖股票的最佳时机II 中使用贪心策略不用关心具体什么时候买卖,只要收集每天的正利润,最后就是最大利润了。而本题有了手续费,就关系到什么时候买卖了,因为计算所获得利润,需要考虑到出现买卖利润不足以手续费的情况。

如果使用贪心策略,就是最低值买,最高值(算上手续费还盈利)就卖。

  • 买入日期:其实很好想,遇到更低点就记录一下。
  • 卖出日期:当前价格大于(最低价格+手续费),就可以收获利润,卖出日期就是连续收获利润区间里的最后一天。

所以在做收获利润操作的时候其实有三种情况:

  • 情况一:收获利润的这一天并不是收获利润区间里的最后一天(不是真正的卖出,相当于持有股票),所以后面要继续收获利润。
  • 情况二:前一天是收获利润区间里的最后一天(相当于真正的卖出了),今天要重新记录最小价格了。
  • 情况三:不作操作,保持原有状态(买入,卖出,不买不卖)
class Solution {public:int maxProfit(vector<int>& prices, int fee) {int result = 0;int minPrice = prices[0];   //记录初始最低价格for(int i = 1;i < prices.size();i++){//情况2 买入if(prices[i] < minPrice){minPrice = prices[i];}//情况3 不买不卖if(prices[i] >= minPrice && prices[i] <= minPrice + fee){continue;}//算利润if(prices[i] > minPrice + fee){result += prices[i] - minPrice - fee;minPrice = prices[i] - fee; //情况1 //比如[1,3,7,5,10,3] fee=3情况下://先算7-1-3=3,然后更新当天的最低价格,和第二天的最低价格进行比较//如果第二天低则是情况2(这一天是真的卖出了,最低价格会被更新为第二天的价格,也就是第8行)//但7-3=4<5属于今天低的情况,今天低则是情况1//意味着7这一天并不是本次购入股票的利润最高点,所以更新7这天的价格-减去手续费 = 4//这样就相当于当天没有进行卖出,因为后续卖出的result利润中 10-4-3= 3而不再是10-7-3//也就是说实际上手续费只算了一次,第二次扣除部分已经和最低价格的更新进行了抵消,也就相当于是最后一天才卖的了}}return result;}
};

动态规划

相对于 122.买卖股票的最佳时机II 的动态规划解法中,只需要在计算卖出操作的时候减去手续费就可以了。

class Solution {public:int maxProfit(vector<int>& prices, int fee) {//dp[i][0]  表示第 i 天交易完后手里没有股票的最大利润//dp[i][1]  表示第 i 天交易完后手里持有股票的最大利润int n = prices.size();vector<vector<int>> dp(n,vector<int>(2,0));dp[0][0] = 0;dp[0][1] = - prices[0];for(int i = 1; i < n; i++){dp[i][0] = max(dp[i - 1][0], dp[i - 1][1] + prices[i] - fee);// 前一天没有,今天也没买 or 前一天有 今天卖了 扣了手续费dp[i][1] = max(dp[i - 1][1], dp[i - 1][0] - prices[i]);// 前一天持有 or 前一天没有,今天购入}return dp[n - 1][0];}
};//时间复杂度:O(n)
//空间复杂度:O(n)
优化

当然可以对空间进行优化,不申请dp,因为当前状态只是依赖前一个状态,直接定义int变量进行更新即可。

class Solution {public:int maxProfit(vector<int>& prices, int fee) {//表示第 i 天交易完后手里没有股票的最大利润 saleStock //表示第 i 天交易完后手里持有股票的最大利润 holdStock int n = prices.size();int saleStock = 0;int holdStock = - prices[0];for(int i = 1; i < n; i++){int temp = saleStock;saleStock = max(saleStock, holdStock + prices[i] - fee);// 前一天没有,今天也没买 or 前一天有 今天卖了 扣了手续费holdStock = max(holdStock, temp - prices[i]);// 前一天持有 or 前一天没有,今天购入}return saleStock;}
};//时间复杂度:O(n)
//空间复杂度:优化至O(1)

968. 监控二叉树

从下往上看,局部最优:让叶子节点的父节点安摄像头,所用摄像头最少,整体最优:全部摄像头数量所用最少!

从低到上,先给叶子节点父节点放个摄像头,然后隔两个节点放一个摄像头,直至到二叉树头结点。

可以使用后序遍历也就是左右中的顺序,这样就可以在回溯的过程中从下到上进行推导了。

每个节点可能的几种状态分别由数字来表示:

那么空节点怎么办?

情况分析

  • 情况1:左右节点都有覆盖,此时中间节点应该为无覆盖的状态。
  • 情况2:左右节点至少有一个无覆盖,则中间节点(父节点)放摄像头。
  • 情况3:左右节点至少有一个有摄像头,那么其父节点就是被覆盖的状态。
  • 情况4:处理完,递归结束后,头结点没有覆盖,放摄像头。
class Solution {private:int result;int traversal(TreeNode* cur){if(cur == NULL) return 2;   //空结点是有覆盖状态int left = traversal(cur->left);int right = traversal(cur->right);if(left == 2 && right == 2) return 0;    // 情况1else if(left == 0 || right == 0){ // 情况2result++;return 1;} else return 2;    // 情况3  if(left == 1 || right == 1) }
public:int minCameraCover(TreeNode* root) {result = 0;if(traversal(root) == 0){   // 情况4result++;}return result;}
};

代码随想录1刷—贪心算法篇(二)相关推荐

  1. 代码随想录1刷—单调栈篇

    代码随想录1刷-单调栈篇 什么时候想到单调栈? 单调栈的原理? 单调栈工作过程? [739. 每日温度](https://leetcode.cn/problems/daily-temperatures ...

  2. 代码随想录一刷个人记录

    代码随想录一刷个人记录 2022/7/14(leetcode) 2022/7/15(leetcode) 2022/7/17[字符串] 2022/7/18[字符串] 2022/7/20[双指针] 202 ...

  3. 代码随想录 一刷总结(完结)

    一.数组 5 1. 基础知识 连续空间 相同类型元素. 注意java中二维数据是一些不连续的一维数组空间. 易读不易删除. 2. 典型解法 (1) 二分法 适用范围 有序.无重复元素. 时间复杂度O( ...

  4. iVX低代码平台系列详解 -- 概述篇(二)

    写在前面 ivx动手尝试电梯:ivx在线编辑器 iVX系列教程持续更新中 上篇文章可看:iVX低代码平台系列详解 – 概述篇(一) ivx目录 写在前面 一.iVX优势 1.快速学习 2.快速开发 3 ...

  5. LeetCode 741. 摘樱桃___贪心算法篇__失败

    741. 摘樱桃 一个N x N的网格(grid) 代表了一块樱桃地,每个格子由以下三种数字的一种来表示: 0 表示这个格子是空的,所以你可以穿过它. 1 表示这个格子里装着一个樱桃,你可以摘到樱桃然 ...

  6. 算法篇 - 二叉搜索树

    前言 在前端的工作当中,二叉搜索树不怎么常见,虽然没有快排.冒泡.去重.二分.希尔等算法常见,但是它的作用,在某些特定的场景下,是非常重要的. 目前es6的使用场景比较多,所以我准备能用es6的地方就 ...

  7. 通俗易懂:贪心算法(二):区间问题 (力扣435无重叠区间)

    看完本文,可以顺便解决leetcode以下题目: 435.无重叠区间(中等) 一.通俗易懂的 贪心算法 |思想 (重复一次~~~) 贪心算法就是采用贪心的策略,保证每一次的操作都是局部最优的,从而使得 ...

  8. 贪心算法 背包问题代码 c语言,用贪心算法求解普通背包问题的C++代码

    用贪心算法求解普通背包问题的C++代码 2019年3月6日 125次阅读 来源: 贪心算法 #include #define  M  100 void display(int &n,doubl ...

  9. 算法之组合数学及其算法篇(二) ----- 鸽巢原理

    鸽巢原理 前言 鸽巢原理 运用1 运用二 运用三 鸽巢原理的推广 推论 运用一 运用二 鸽巢原理在几何上的作用 鸽巢原理对于数学的证明 前言 鸽巢原理又称抽屉原理或鞋盒原理,这个原理最早是由狄利克雷( ...

最新文章

  1. 查询服务器上几张显卡命令
  2. 国内首个手机AR实景驾驶导航上线,还能识别车辆行人,搜狗地图出品
  3. 面向初级 Web 开发人员的 Tomcat
  4. php 函数有命名空间吗_一篇弄懂PHP命名空间及use的使用
  5. 安装指定版本的minikube
  6. 【动态规划BFS】相遇
  7. SQL Server 2008 R2:error 26 开启远程连接详解
  8. 华为ipd项目管理流程_开发一个产品,就是开发一门新的生意IPD落地直播问答精选...
  9. 除了html 还有什么页面,HTML页面布局有哪些?
  10. 【协同任务】基于matlab蚁群算法多组群UAV协同任务路径规划【含Matlab源码 1578期】
  11. 浅谈Java垃圾回收
  12. Android Intent FLAG标识
  13. R语言使用lm函数构建线性回归模型、线性回归模型的诊断、使用influence.measures函数识别有影响力的观察值
  14. 计算机二级office查分数,3月份office计算机二级内网查分90,写下一点经验给后来人...
  15. pg8168改mac命令_Realtek 8168网卡改MAC地址教程
  16. hardfault常见原因_keil遇到hardfault时原因的查找
  17. 丸子小程序打通视频号,变现更便捷~
  18. C++ STL容器详解
  19. TAElectronic-车辆电子电器模型库
  20. iptables 查看客户端流量情况

热门文章

  1. angular2/4 使用[innerHTML]时样式不生效
  2. 常见算法及其时间复杂度总结
  3. 做项目很累?那是你的“姿势”不对!
  4. 小米2及其他手机无法连接mac
  5. 【7月比赛合集】31场可报名的「创新应用」、「数据分析」和「程序设计」大奖赛,任君挑选!
  6. matlab mcc 参数,matlab中mcc编译器参数的含义
  7. c#计算机语言常见题(使用switch语句将所有题连接)
  8. oracle测试报告模板,Oracle与集算器对照测试报告
  9. radius认证服务器无响应,关于radius认证和portal认证服务器的一点问题
  10. 2021年安全员-A证模拟考试系统及安全员-A证实操考试视频