
《算法设计与分析》在讲解贪心基础的时候,将贪心的特性归纳为两点,最优子结构与贪心选择性质。
最优子结构:从(1~8中,取最小值)

贪心选择性质:(状态3是在状态2推导出来的,与状态1无关。这也是贪心与动归的核心区别)

大概明白概念就行,更多的,还是在题目中锻炼出来的。
一、分发饼干 -- 尽量满足大胃口
二、摆动序列 -- 状态跟踪-很帅😉
三、最大子数组和 -- 巧妙避开上一个状态的影响
四、跳跃游戏 -- 及时止损,增大收益
五、跳跃游戏II -- 尽量增加覆盖范围
六、K次取反后最大化的数组和 -- 最大化收益,最小化损失
七、加油站 -- 通过排除失败情况,快速排除不可能的起点。
八、分发糖果 -- 两次单向遍历,处理了每个节点与[左][右]两邻居的约束关系😇
九、根据身高重建队列 -- 同样是两段策略
十、用最少数量的箭引爆气球 -- 区间问题
十一、无重叠区间 -- 区间问题
十二、划分字母区间 -- 记录最后出现的值
十三、单调递增的数字 -- 从右往左,局部调整,之后数字置位9
十四、监控二叉树 -- 递推&贪心
根据多次重刷经验(3、5、7、8、9、11、12、13、14,这几题,是略有挑战的题😉)
假设你是一位很棒的家长,想要给你的孩子们一些小饼干。但是,每个孩子最多只能给一块饼干。 对每个孩子 i,都有一个胃口值 g[i],这是能让孩子们满足胃口的饼干的最小尺寸;并且每块饼干 j,都有一个尺寸 s[j] 。如果 s[j] >= g[i],我们可以将这个饼干 j 分配给孩子 i ,这个孩子会得到满足。你的目标是尽可能满足越多数量的孩子,并输出这个最大数值。 示例 1:
示例 2:
提示:
class Solution {
// 核心思想是用尽可能大的饼干满足尽可能大的胃口,从而最大化能满足的孩子数量。
public:
int findContentChildren(vector<int>& g, vector<int>& s) {
sort(g.begin(),g.end());
sort(s.begin(),s.end());
int cnt = 0;
int l =g.size()-1, r = s.size()-1;
while(l>=0&&r>=0){
if(g[l]<=s[r]){
r--,l--;
cnt++;
}else{
l--;
}
}
return cnt;
}
};如果连续数字之间的差严格地在正数和负数之间交替,则数字序列称为摆动序列。第一个差(如果存在的话)可能是正数或负数。少于两个元素的序列也是摆动序列。 例如, [1,7,4,9,2,5] 是一个摆动序列,因为差值 (6,-3,5,-7,3) 是正负交替出现的。相反, [1,4,7,2,5] 和 [1,7,4,5,5] 不是摆动序列,第一个序列是因为它的前两个差值都是正数,第二个序列是因为它的最后一个差值为零。 给定一个整数序列,返回作为摆动序列的最长子序列的长度。 通过从原始序列中删除一些(也可以不删除)元素来获得子序列,剩下的元素保持其原始顺序。 示例 1:
示例 2:
示例 3:
// 没有见过这么帅的贪心策略,“状态跟踪” // 记录前一个差值的方向(上升 / 下降),并动态更新当前最长长度
class Solution {
public:
int wiggleMaxLength(vector<int>& nums) {
if (nums.size() <= 1) return nums.size();
int up = 1, down = 1; // 分别记录以当前元素结尾的上升/下降摆动序列长度
for (int i = 1; i < nums.size(); ++i) {
int diff = nums[i] - nums[i-1];
if (diff > 0) up = down + 1; // 当前是上升,只能由前一个下降的序列延长
else if (diff < 0) down = up + 1; // 当前是下降,只能由前一个上升的序列延长
// 差值为0时,up和down保持不变(不影响结果)
}
return max(up, down);
}
};给定一个整数数组 nums ,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。 示例:
class Solution {
// 设计还需巧妙,巧妙的避开,上一个状态对自身的影响
public:
int maxSubArray(vector<int>& nums) {
int sum = nums[0];
int maxN = nums[0];
for(int i=1; i<nums.size(); ++i){
sum = sum<0?0:sum; // 必须及时更新这个状态
sum+=nums[i];
maxN = max(maxN,sum);
}
return maxN;
}
};给定一个非负整数数组,你最初位于数组的第一个位置。 数组中的每个元素代表你在该位置可以跳跃的最大长度。 判断你是否能够到达最后一个位置。 示例 1:
示例 2:
// 贪心策略:及时止损,只保留对后续有增益的子数组前缀。
class Solution {
public:
bool canJump(vector<int>& nums) {
// 有一种,情况,经常会被忽略掉,就是最后一位是0的话,依旧会被跳过。
int maxFlag = nums[0];
for(int i=0; i<nums.size()-1; ++i){
if(maxFlag==0&&maxFlag==nums[i]) return false;
maxFlag = maxFlag<nums[i]?nums[i]:maxFlag;
maxFlag--;
}
return true;
}
};给定一个非负整数数组,你最初位于数组的第一个位置。 数组中的每个元素代表你在该位置可以跳跃的最大长度。 你的目标是使用最少的跳跃次数到达数组的最后一个位置。 示例:
说明: 假设你总是可以到达数组的最后一个位置。
// 贪心策略:每次跳跃时,选择覆盖范围最大的位置,从而减少后续需要的跳跃次数
class Solution {
// 总是对这种特殊位置,判断不清晰,本题就是错在了这种特殊位置上
public:
int jump(vector<int>& nums) {
if(nums.size()<=1) return 0;
// 不断更新,能够抵达的最远距离
int cur_n = nums[0],max_n=nums[0];
int cnt = 0; // 次数
for(int i=0; i<nums.size()-1; ++i){ // 来,让我爽一把
max_n = max(max_n,i+nums[i]);
if(cur_n==i){ // 到了不得不蹦的位置
cnt++;
cur_n=max_n;
}
}
return cnt+1;
}
};给定一个整数数组 A,我们只能用以下方法修改该数组:我们选择某个索引 i 并将 A[i] 替换为 -A[i],然后总共重复这个过程 K 次。(我们可以多次选择同一个索引 i。) 以这种方式修改数组后,返回数组可能的最大和。 示例 1:
示例 2:
示例 3:
提示:
// 贪心策略:优先对绝对值大的负数取反(最大化增益),若所有负数处理完仍有剩余操作次数,则对绝对值最小的数(无论正负)进行奇数次取反(最小化损失)。 最大化收益,最小化损失
class Solution {
// 这道题目的贪心,好帅啊,可以重复练习几次,可以记录下来的呢。
static bool cmp(int a,int b){
return abs(a)<abs(b);
}
public:
int largestSumAfterKNegations(vector<int>& nums, int k) {
sort(nums.begin(),nums.end(), cmp);
int sum = 0;
for(int i=nums.size()-1; i>=0; --i){
if(nums[i]<0&&k!=0){
k--;
sum-=nums[i];
}else sum+=nums[i];
}
if(k>0&&k%2!=0){ // k还剩余
sum-=2*abs(nums[0]);
}
return sum;
}
};在一条环路上有 N 个加油站,其中第 i 个加油站有汽油 gas[i] 升。 你有一辆油箱容量无限的的汽车,从第 i 个加油站开往第 i+1 个加油站需要消耗汽油 cost[i] 升。你从其中的一个加油站出发,开始时油箱为空。 如果你可以绕环路行驶一周,则返回出发时加油站的编号,否则返回 -1。 说明:
示例 1: 输入:
输出: 3 解释:
示例 2: 输入:
// 贪心策略:通过排除失败情况,快速排除不可能的起点。
class Solution {
// 通过排除局部的失败情况,快速排除不可能的起点。
public:
int canCompleteCircuit(vector<int>& gas, vector<int>& cost) {
vector<int> diff(gas.size());
int sum = 0;
for(int i=0; i<gas.size(); ++i){
diff[i] = gas[i]-cost[i];
sum += diff[i];
}
if(sum<0) return -1;
int cnt = 0;
int cur = 0;
for(int i=0; i<diff.size(); ++i){
if(cur+diff[i]<0){
cnt = i+1;
cur = 0;
}else cur += diff[i];
}
return cnt;
}
};老师想给孩子们分发糖果,有 N 个孩子站成了一条直线,老师会根据每个孩子的表现,预先给他们评分。 你需要按照以下要求,帮助老师给这些孩子分发糖果:
那么这样下来,老师至少需要准备多少颗糖果呢? 示例 1:
示例 2:
// 贪心策略:通过,两次单向遍历,处理了每个节点与[左][右]两邻居的约束关系,从而求出局部最优解,进而得出全局最优
class Solution {
public:
int candy(vector<int>& ratings) {
int len = ratings.size();
vector<int> randy(len,0);
// 从左向右 来一次
randy[0]=1;
for(int i=1; i<len; ++i){
if(ratings[i]>ratings[i-1]) randy[i]=randy[i-1]+1;
else randy[i]=1;
}
// 从右向左 来一次 被用来修补修补
for(int i=len-2; i>=0; --i){
if(ratings[i]>ratings[i+1]&&randy[i]<=randy[i+1]) randy[i] = randy[i+1]+1;
}
int sum = 0;
for(int i : randy) sum+=i;
return sum;
}
};假设有打乱顺序的一群人站成一个队列,数组 people 表示队列中一些人的属性(不一定按顺序)。每个 people[i] = [hi, ki] 表示第 i 个人的身高为 hi ,前面 正好 有 ki 个身高大于或等于 hi 的人。 请你重新构造并返回输入数组 people 所表示的队列。返回的队列应该格式化为数组 queue ,其中 queue[j] = [hj, kj] 是队列中第 j 个人的属性(queue[0] 是排在队列前面的人)。 示例 1:
示例 2:
提示:
题目数据确保队列可以被重建
// 贪心策略:两端策略,首先看左端,然后看右端。每次只是单 一 一 方面。 多方面一起,只会让人迷糊。
class Solution {
// 我觉得,这是一道非常酷的题,非常有意思的呢
// 非常有记录的必要
static bool cmp(vector<int> a1, vector<int> a2){
if(a1[0]==a2[0]) return a1[1]<a2[1]; // 从小到大排序
return a1[0]>a2[0]; // 从大到小排序
}
public:
vector<vector<int>> reconstructQueue(vector<vector<int>>& people) {
sort(people.begin(),people.end(),cmp);
vector<vector<int>> res;
for(int i=0; i<people.size(); ++i){
if(people[i][1]==0) res.insert(res.begin()+people[i][1],people[i]);
else res.insert(res.begin()+people[i][1],people[i]);
}
return res;
}
};在二维空间中有许多球形的气球。对于每个气球,提供的输入是水平方向上,气球直径的开始和结束坐标。由于它是水平的,所以纵坐标并不重要,因此只要知道开始和结束的横坐标就足够了。开始坐标总是小于结束坐标。 一支弓箭可以沿着 x 轴从不同点完全垂直地射出。在坐标 x 处射出一支箭,若有一个气球的直径的开始和结束坐标为 xstart,xend, 且满足 xstart ≤ x ≤ xend,则该气球会被引爆。可以射出的弓箭的数量没有限制。 弓箭一旦被射出之后,可以无限地前进。我们想找到使得所有气球全部被引爆,所需的弓箭的最小数量。 给你一个数组 points ,其中 points [i] = [xstart,xend] ,返回引爆所有气球所必须射出的最小弓箭数。 示例 1:
示例 2:
示例 3:
示例 4:
示例 5:
提示:
// 贪心策略:区间问题
class Solution {
// 本题考查,对重叠区间的处理方式,先遍历,然后设置区间
static bool cmp(vector<int> p1,vector<int> p2){
return p1[0]<p2[0]; // 从小到大
}
public:
int findMinArrowShots(vector<vector<int>>& points) {
sort(points.begin(),points.end(),cmp);
int cnt = 0;
int l=-1,r=-1; // 左端点,有端点
for(int i=0; i<points.size(); ++i){
int cur_l = points[i][0];
int cur_r = points[i][1];
if((cur_l>=l&&cur_l<=r)||(cur_r<=r&&cur_r>=l)){
l = max(cur_l,l);
r = min(cur_r,r);
}else{
cnt++;
l = cur_l;
r = cur_r;
}
}
return cnt;
}
};给定一个区间的集合,找到需要移除区间的最小数量,使剩余区间互不重叠。 注意: 可以认为区间的终点总是大于它的起点。 区间 [1,2] 和 [2,3] 的边界相互“接触”,但没有相互重叠。 示例 1:
示例 2:
示例 3:
// 贪心策略:优先保留结束时间早的区间,为后续区间留出更多空间,从而减少后续重叠的可能性
class Solution {
// 本题考查,对重叠区间的处理方式,先遍历,然后设置区间
static bool cmp(vector<int> p1,vector<int> p2){
return p1[0]<p2[0]; // 从小到大
}
public:
int findMinArrowShots(vector<vector<int>>& points) {
sort(points.begin(),points.end(),cmp);
int cnt = 0;
int l=-1,r=-1; // 左端点,有端点
for(int i=0; i<points.size(); ++i){
int cur_l = points[i][0];
int cur_r = points[i][1];
if((cur_l>=l&&cur_l<=r)||(cur_r<=r&&cur_r>=l)){
l = max(cur_l,l);
r = min(cur_r,r);
}else{
cnt++;
l = cur_l;
r = cur_r;
}
}
return cnt;
}
};字符串 S 由小写字母组成。我们要把这个字符串划分为尽可能多的片段,同一字母最多出现在一个片段中。返回一个表示每个字符串片段的长度的列表。 示例:
提示:
// 贪心策略:记录最后出现的值
class Solution {
// 题目读错,结果就很抽象
public:
vector<int> partitionLabels(string s) {
vector<int> pos(s.size(),0);
vector<int> hash(26,0); // 跟这个无关
// 记录字符序
for(int i=s.size()-1; i>=0; --i){ // 字符序
int ch = s[i]-'a'; // 转化
hash[ch] = max(hash[ch],i);
pos[i] = hash[ch];
}
vector<int> res;
int cnt = 0;
int maxn = pos[0];
for(int i=0; i<s.size(); ++i){ // 开始分割字符
cnt++; // 个数
maxn = max(maxn,pos[i]); // 最早能结束的位置
if(i==maxn){
res.push_back(cnt); // 存储结果
cnt = 0; // 置零
}
}
if(cnt!=0) res.push_back(cnt);
return res;
}
};给定一个非负整数 N,找出小于或等于 N 的最大的整数,同时这个整数需要满足其各个位数上的数字是单调递增。 (当且仅当每个相邻位数上的数字 x 和 y 满足 x <= y 时,我们称这个整数是单调递增的。) 示例 1:
示例 2:
示例 3:
说明: N 是在 [0, 10^9] 范围内的一个整数。
// 贪心策略:从右往左,局部调整,之后数字置位9
class Solution {
// 从右往左寻找递减点,局部调整高位,确保寻找到最大的单调递增数
public:
int monotoneIncreasingDigits(int n) {
string str = to_string(n);
int flag = str.size()+2;
for(int i=str.size()-1; i>0; --i){
if(str[i-1]>str[i]){
flag = i;
str[i-1]--;
}
}
for(int i=flag; i<str.size(); ++i){
str[i]='9';
}
return stoi(str);
}
};给定一个二叉树,我们在树的节点上安装摄像头。 节点上的每个摄影头都可以监视其父对象、自身及其直接子对象。 计算监控树的所有节点所需的最小摄像头数量。 示例 1:

示例 2:

提示:
// 贪心策略:递推
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
// 这道,二叉树,贪心,运用递推公式,运用的非常完美。
public:
int cnt = 0;
// 什么都无0 覆盖1 照相机2
int dfs(TreeNode* root){
// 覆盖
if(root == nullptr) return 1; // 表示被覆盖
int l_num = dfs(root->left);
int r_num = dfs(root->right);
if(l_num==0 || r_num==0){ // 子节点,未被照到
cnt++;
return 2; // 放置照相机
}
if(l_num==2 || r_num==2){ // 字节点,有照相机
return 1; // 于是该节点就被覆盖了
}
return 0; // 表示该节点,没人照顾。
}
int minCameraCover(TreeNode* root) {
cnt = 0;
if(dfs(root)==0) cnt++; // 如果根节点,未被覆盖
return cnt;
}
};