文章目录
贪心算法
- 贪心的本质是选择每一阶段的局部最优,从而达到全局最优。例如,从家去学校,有三条路可以走,要花时间最少,要怎么走?肯定走路程最短的。
- 贪心算法没有固定的模板和套路,唯一的难点就是如何通过局部最优,推出整体最优。
- 贪心算法最好用的策略就是举反例,如果想不到反例,那就试一试贪心。贪心算法有时候也是常识性推导,所以会认为本应该就这么做。
- 贪心算法的套路就是常识性推导加上举反例。
- 贪心算法解题思路:只要想清楚局部最优是什么,如果推导出全局最优,就够了。类比于回溯/递归,想清楚递归遍历时,要做什么,什么时候停止,就可以了。
- 贪心算法题目
- 简单:455.分发饼干、1005.K次取反后最大化的数组和、860.柠檬水找零
- 中等
- 序列问题:376.摆动序列、738.单调递增的数字
- 股票问题:122.买卖股票的最佳时机II、714.买卖股票的最佳时机含手续费
- 两个维度权衡问题:135.分发糖果、406.根据身高重建队列
- 有点难
- 区间问题:55.跳跃游戏、45.跳跃游戏Ⅱ、452.用最少数量的箭引爆气球、435.无重叠区间、763.划分字母区间、56.合并区间
- 其他:53.最大子序和、134.加油站、968.监控二叉树
455.分发饼干
思路 步骤
为了满足更多的小孩,避免饼干浪费,那么需要充分利用饼干尺寸喂饱孩子,全局最优就是喂饱尽可能多的小孩
思路1: 大饼干优先满足胃口大的孩子;
思路2: 用尽量小的饼干满足胃口小的孩子
做法1: 排序、child自减(相当于双指针)、贪心。先将饼干数组和小孩数组排序,然后从后往前遍历小孩数组,用大饼干优先满足胃口大的孩子,并统计满足小孩数量。
做法2: 排序、child自加(相当于双指针)、贪心。先将饼干数组和小孩数组排序,然后从前往后遍历饼干,再遍历的胃口,用尽量小的饼干满足胃口小的孩子,并统计满足小孩数量。
做法3: 排序、双指针、贪心。先将饼干数组和小孩数组排序,然后同时遍历饼干和胃口,用尽量小的饼干满足胃口小的孩子,统计满足小孩和使用过饼干的数量。
代码
- 做法1:排序 index自减 贪心,大饼干优先满足胃口大的孩子
class Solution {
public:
int findContentChildren(vector<int>& g, vector<int>& s) {
//排序
sort(g.begin(), g.end());
sort(s.begin(), s.end());
//cookie自减+for循环(相当于双指针)
int cookie = s.size()-1;//饼干数组下标,记录哪块饼干被使用过
int children = 0;//被满足的孩子
//先遍历孩子 再从后遍历饼干 相当于大饼干满足胃口大的孩子
for(int i=g.size()-1; i>=0; i--)
{
if(cookie>=0 && g[i] <= s[cookie])//优先用大饼干满足胃口大的孩子
{
children++;//满足的孩子
cookie--;//满足的话 这块饼干用过了
}
}
return children;
}
};
在这里,不能先遍历饼干,再遍历胃口。因为for循环的i 是固定移动的,而if里面的下标 cookie是符合条件cookie>=0 && g[i] <= s[cookie]
才移动的。如果for控制饼干, if控制胃口,会出现如下情况 :
if的cookie指向 胃口10, for的i指向饼干9,因为饼干9满足不了 胃口10,所以i 向前移动,而cookie走不到g[i] <= s[cookie] 的逻辑,所以cookie不会移动。i 持续向前移动,最后所有的饼干都匹配不上,一定是for控制胃口,if控制饼干。
- 做法2:排序 index自增 贪心,用尽量小的饼干满足胃口小的孩子
class Solution {
public:
int findContentChildren(vector<int>& g, vector<int>& s)
{
//排序
sort(g.begin(), g.end());
sort(s.begin(), s.end());
int child = 0;
for(int i=0; i<s.size(); i++)
{
if(child < g.size() && g[child] <= s[i]) child++;
}
return child;
}
};
- 做法3: 排序 双指针 贪心,用尽量小的饼干满足胃口小的孩子
class Solution {
public:
int findContentChildren(vector<int>& g, vector<int>& s)
{
//排序
sort(g.begin(), g.end());
sort(s.begin(), s.end());
int child = 0;
int cookie = 0;
for(;child<g.size() && cookie<s.size();)
{
if(g[child] <= s[cookie]) child++;//当前饼干可以满足当前孩子的需求,孩子数量+1
cookie++;//满足,饼干被使用过了;不满足,饼干被抛弃
}
return child;
}
};
376. 摆动序列
贪心算法 思路 分析 代码
1. 思路: 贪心算法,统计相邻数差值异号的个数,即峰值数,最大摆动序列长度=峰值数+1,注意平坡情况的考虑。
2. 要求删除元素使其达到最大摆动序列:
让峰值尽可能的保持峰值,删除单调坡度上的节点(不包括单调坡度两端的节点),该坡度就有两个局部峰值。让整个序列有最多的局部峰值,从而达到最长摆动序列,最后统计序列长度。
3. 实际操作:
- 可以不删除节点,统计数组的峰值数量即可。
- 计算峰值时,遍历数组,三个相邻的数,前两者差值与后两者差值异号,说明有峰值。
- prediff=nums[i] - nums[i-1],curdiff=nums[i+1] - nums[i]
prediff < 0 && curdiff > 0
或者prediff > 0 && curdiff < 0
,统计峰值数,最大摆动序列长度=峰值数+1- 考虑三种异常情况,上下坡中有平坡、单调坡中有平坡、数组首尾两端
4. 三种异常情况:
-
记录峰值条件:
prediff < 0 && curdiff > 0
或者prediff > 0 && curdiff < 0
-
上下坡中有平坡:
一共有四个2,删除左边三个2或者右边三个2都可以,统一删除左边三个2,
i指向第一个2时,prediff > 0 && curdiff = 0 ;
i指向第二个2时,prediff = 0 && curdiff = 0 ;
i指向第三个2时,prediff = 0 && curdiff = 0 ;
i指向第四个2时,prediff = 0 && curdiff < 0,要记录该峰值,删除前三个2后剩下的峰值。因此,该情况下,记录峰值条件是(preDiff <= 0 && curDiff > 0) || (preDiff >= 0 && curDiff < 0)
-
单调坡中有平坡:
如果单调坡中有平坡,是不能算峰值的,只有当坡度摆动变化时,才可以计算峰值数。例如,下图要删除前两个2,有两种更新prediff情况-
如果坡度变化后,即出现峰值后再更新prediff,第一个2和最后一个2都会统计为峰值
if ((preDiff <= 0 && curDiff > 0) || (preDiff >= 0 && curDiff < 0)) result++; preDiff = curDiff;
-
如果坡度变化时,即出现峰值才更新prediff,只有最后一个2统计为峰值
if ((preDiff <= 0 && curDiff > 0) || (preDiff >= 0 && curDiff < 0)) { result++; preDiff = curDiff; }
-
- 数组首尾两端:
题目仅有一个元素或者含两个不等元素的序列也视作摆动序列,表明数组只有两个不同的元素,摆动序列长度为2。但通过统计差值来计算峰值个数,至少需要三个数字,因此需要考虑数组最左和最右的特殊情况,两种处理方法,- 单独处理,如果只有两个元素,且元素不同,那么摆动序列长度为2,result=2;
- 结合峰值判断条件统计,如果数组只有两个元素,如序列[2,5],可以假设为[2,2,5],那么坡度preDiff = 0。因此,可以假设数组最右边有一个峰值,即result初始值为1。当
curDiff > 0 && preDiff <= 0
满足时,result++,计算了左边的峰值,最后result=2,摆动序列长度为2。
5. 代码:
class Solution {
public:
//贪心算法
int wiggleMaxLength(vector<int>& nums) {
if(nums.size() <= 1) return nums.size();//空数组或单元素数组
int prediff = 0;//前两个元素差值
int curdiff = 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;//更新prediff
}
}
return result;
}
};
动态规划 思路 步骤 代码
1. 思路: 动态规划,根据当前数组计算山峰山谷数
2. 确定dp数组及其下标含义:
如果nums[i]>nums[i-1],设dp状态为dp[i][0],表示前i个数中,第i个数nums[i]作为山峰的摆动子序列的最长长度;
如果nums[i]<nums[i-1],设dp状态为dp[i][1],表示前i个数中,第i个数nums[i]作为山谷的摆动子序列的最长长度
3. 转移方程:
前i个数,0 < j < i,
如果满足nums[j] < nums[i]
,则dp[i][0] = max(dp[i][0], dp[j][1] + 1)
,表示将nums[i]接到前面某个山谷后面,作为山峰
如果满足nums[j] > nums[i]
,则dp[i][1] = max(dp[i][1], dp[j][0] + 1)
,表示将nums[i]接到前面某个山峰后面,作为山谷
4. dp数组初始化: 一个数可以接到前面的某个数后面,也可以以自身为子序列的起点,所以初始状态为:dp[0][0] = dp[0][1] = 1
5. 代码
class Solution {
public:
//动态规划
//确定dp数组及其下标含义
int dp[1005][2];//1.创建dp数组 第一维大小 0 <= nums[i] <= 1000
int wiggleMaxLength(vector<int>& nums)
{
memset(dp, 0, sizeof(dp));//2.将数组的每个元素初始化为0
dp[0][0] = dp[0][1] = 1;//3.dp数组初始化
//遍历dp数组
for(int i=1; i<nums.size(); i++)
{
dp[i][0] = dp[i][1] = 1;//前i个数的dp数组初始化
for(int j=0; j<i; j++)//遍历前i个数的dp数组
{
if(nums[j] < nums[i]) dp[i][0] = max(dp[i][0], dp[j][1]+1);//nums[i]接到前面某个山谷后面,作为山峰
}
for(int j=0; j<i; j++)
{
if(nums[j] > nums[i]) dp[i][1] = max(dp[i][1], dp[j][0]+1);//nums[i]接到前面某个山峰后面,作为山谷
}
}
return max(dp[nums.size()-1][0], dp[nums.size()-1][1]);
}
};
53. 最大子序和
暴力解法 双层for循环
思路: 第一层for设置起始位置,第二层for循环遍历数组寻找最大值
代码: 力扣可以过用例,但是会超时
class Solution {
public:
//暴力解法
int maxSubArray(vector<int>& nums) {
int result = INT32_MIN;
int sum = 0;//用来记录每一次遍历连续子数组的和
for(int i=0; i<nums.size(); i++)//决定每次连续子数组的遍历起点
{
sum = 0;//每次连续子数组求和前清零
for(int j=i; j<nums.size(); j++)
{
sum += nums[j];//求和
result = sum > result ? sum : result;//判断上次子数组和大还是当前子数组和大
}
}
return result;
}
};
贪心算法 思路 分析 代码
思路:
- 如果当前当前元素的连续和是负数就放弃,从下一个元素重新计算连续和并记录,最后取最大的连续和。
- 相当于是暴力解法中的不断调整最大子序和区间的起始位置,如果count取到最大值了,用result记录最大子序和区间和,也算是调整了区间终止位置。
- 贪心算法就是在count为正数时,开始一个区间的统计,也就是图中红色起始位置。
代码:
class Solution {
public:
int maxSubArray(vector<int>& nums)
{
int result = INT32_MIN;//最小负数
int sum = 0;//用来记录每一次遍历连续子数组的和
for(int i=0; i<nums.size(); i++)
{
sum += nums[i];
//记录最大连续和
if(sum > result) result = sum;
//连续和为负数就放弃 下一个元素开始求连续和
if(sum <= 0) sum = 0;
}
return result;
}
};
注意点:
- 如果输入用例都是负数,结果是什么?
- 是遇到负数就选择起始位置,还是连续和为负选择起始位置?
动态规划 思路 步骤 代码
思路及步骤:
- 确定dp数组(dp table)以及下标的含义:dp[i]:以nums[i]为结尾的最大连续子序列和
- 确定递推公式:dp[i]只有两个方向可以推出来,一是dp[i - 1] + nums[i],即nums[i]加入当前连续子序列和;二是nums[i],即从头开始计算当前连续子序列和。最后dp[i]一定是取最大的,所以
dp[i] = max(dp[i - 1] + nums[i], nums[i]);
- dp数组初始化:dp[i]是依赖于dp[i - 1]的状态,dp[0]是递推公式的基础。根据dp[i]的定义,dp[0]应为nums[0],即
dp[0] = nums[0]
- 确定遍历顺序:递推公式中,dp[i]依赖于dp[i - 1]的状态,需要从前向后遍历。
代码:
class Solution {
public:
//动态规划
int maxSubArray(vector<int>& nums)
{
if(nums.size()==0) return 0;//剔除空序列
vector<int> dp(nums.size(), 0);//dp数组定义 nums[i]结尾的子序列和
dp[0] = nums[0];//dp数组初始化
int result = dp[0];
for(int i=1; i<nums.size(); i++)
{
dp[i] = max(dp[i-1]+nums[i], nums[i]);//推导
if(dp[i] > result) result = dp[i];//记录最大连续和dp[i]
}
return result;
}
};
122.买卖股票的最佳时机II
贪心算法 思路 代码
思路:
- 把利润分解为以天为单位,而不是整体去考虑,那么根据prices,每天的利润序列:(prices[i] - prices[i - 1])…(prices[1] - prices[0])。
- 局部最优:收集每天的正利润,收集正利润的区间,也就是股票买卖的区间;全局最优:只需要关注最终利润,不需要记录区间,求得最大利润
- 贪心算法就是只收集正利润,最后求得最大利润
代码:
class Solution {
public:
//贪心算法
int maxProfit(vector<int>& prices) {
int result = 0;
for(int i=1; i<prices.size(); i++)//i从1开始,第二天才有利润
{
//累加每天的正利润,最后求的最大利润
result += max(prices[i]-prices[i-1], 0);
}
return result;
}
};
动态规划 思路 步骤 代码
思路及步骤:
- 确定dp数组(dp table)以及下标的含义:
- dp[i][0]第i天持有股票后的最多现金
- dp[i][1]第i天不持有股票所得最多现金
- 确定递推公式:
-
如果第i天持有股票即dp[i][0], 则有:
- 第i-1天就持有股票,保持现状,所得现金=昨天持有股票的所得现金,即dp[i - 1][0]
- 第i天买入股票,所得现金=昨天不持有股票的所得现金 - 今天的股票价格 即dp[i - 1][1] - prices[i]
-
如果第i天不持有股票即dp[i][1], 则有:
- 第i-1天就不持有股票,保持现状,所得现金=昨天不持有股票的所得现金 即dp[i - 1][1]
- 第i天卖出股票,所得现金=按照今天股票价格卖出后所得现金,即prices[i] + dp[i - 1][0]
-
dp[i][0],第i天持股票所剩最多现金 = max(第i-1天持股票所剩现金, 第i-1天持现金-买第i天的股票),即
dp[i][0] = max(dp[i - 1][0], dp[i - 1][1] - prices[i]);
-
dp[i][1],第i天持有最多现金 = max(第i-1天持有的最多现金,第i-1天持有股票的最多现金+第i天卖出股票)。即
dp[i][1] = max(dp[i - 1][1], dp[i - 1][0] + prices[i]);
- dp数组初始化:
- dp[0][0]表示最初持有的股票prices[0],即
dp[0][0] -= prices[0]
- dp[0][1]表示最初持有现金,即
dp[0][1]=0
- 确定遍历顺序:递推公式中,dp[i][0]、dp[i][1]依赖于dp[i - 1][0]、dp[i - 1][1]的状态,需要从前向后遍历。
代码:
class Solution {
public:
//动态规划
int maxProfit(vector<int>& prices)
{
// dp[i][1]第i天持有的最多现金
// dp[i][0]第i天持有股票后的最多现金
int n = prices.size();
vector<vector<int>> dp(n, vector<int>(2, 0));
dp[0][0] -= prices[0];//持股票
dp[0][1] = 0;//持现金
for(int i=1; i<n; i++)
{
// 第i天持股票所剩最多现金 = max(第i-1天持股票所剩现金, 第i-1天持现金-买第i天的股票)
dp[i][0] = max(dp[i-1][0], dp[i-1][1] - prices[i]);//第i天买了股票
// 第i天持有最多现金 = max(第i-1天持有的最多现金,第i-1天持有股票的最多现金+第i天卖出股票)
dp[i][1] = max(dp[i-1][1], dp[i-1][0] + prices[i]);//第i天卖了股票
}
return max(dp[n-1][0], dp[n-1][1]);
}
};
1005.K次取反后最大化的数组和
贪心算法 思路 步骤 代码
思路:
- 序列有正有负,贪心思路,局部最优是让绝对值大的负数变为正数,当前数值达到最大;整体最优是整个数组和达到最大
- 此时序列只有正数,且k大于0,另一个贪心思路,局部最优是只找数值最小的正整数进行反转,当前数值和可以达到最大;全局最优是整个数组和达到最大。注意整个过程
步骤:
- 第一步:将数组按照绝对值大小从大到小排序
- 第二步:从前向后遍历,遇到负数将其变为正数,同时K–
- 第三步:如果K还大于0,那么反复转变数值最小的元素,将K用完
- 第四步:求和
代码:
class Solution {
//绝对值从大到小 排序
static bool campare(int a, int b)
{
return abs(a) > abs(b);
}
public:
int largestSumAfterKNegations(vector<int>& nums, int k) {
//1. 排序
sort(nums.begin(), nums.end(), campare);
//2. 遇到负数就变号 k也要统计
for(int i=0; i<nums.size(); i++)
{
if(nums[i]<0 && k>0)
{
nums[i] = -nums[i];
k--;
}
}
//3. 此时都是正序列 且k仍大于0,数值最小的数变号
//k为偶数,偶数次反转后还是正数,不用删减,
//k为奇数,再处理一次,取数值最小的数变号
if(k%2 == 1) nums[nums.size()-1] = -nums[nums.size()-1];
//4.求和
int result = 0;
for(int n : nums) result += n;
return result;
}
};