1.贪心算法理论简介
贪心算法,又名贪婪法,是寻找最优解问题的常用方法,这种方法模式一般将求解过
程分成若干个步骤,但每个步骤都应用贪心原则,选取当前状态下最好/最优的选择
(局部最有利的选择),并以此希望最后堆叠出的结果也是最好/最优的解。
贪婪法的基本步骤:
步骤1:从某个初始解出发;
步骤2:采用迭代的过程,当可以向目标前进一步时,就根据局部最优策略,得到一
部分解,缩小问题规模;
步骤3:将所有解综合起来。
举一个例子:
例如,有一堆钞票,你可以拿走十张,如果想达到最大的金额,你要怎么拿?
指定每次拿最大的,最终结果就是拿走最大数额的钱。
每次拿最大的就是局部最优,最后拿走最大数额的钱就是推出全局最优。
2.题目
1.分发饼干
假设你是一位很棒的家长,想要给你的孩子们一些小饼干。但是,每个孩子最多只能给一块饼干。
对每个孩子 i,都有一个胃口值 g[i],这是能让孩子们满足胃口的饼干的最小尺寸;并且每块饼干 j,都有一个尺寸 s[j] 。如果 s[j] >= g[i],我们可以将这个饼干 j 分配给孩子 i ,这个孩子会得到满足。你的目标是尽可能满足越多数量的孩子,并输出这个最大数值。
示例 1:
输入: g = [1,2,3], s = [1,1]
输出: 1
解释:
你有三个孩子和两块小饼干,3个孩子的胃口值分别是:1,2,3。
虽然你有两块小饼干,由于他们的尺寸都是1,你只能让胃口值是1的孩子满足。
所以你应该输出1。
示例 2:
输入: g = [1,2], s = [1,2,3]
输出: 2
解释:
你有两个孩子和三块小饼干,2个孩子的胃口值分别是1,2。
你拥有的饼干数量和尺寸都足以让所有孩子满足。
所以你应该输出2.
分析
贪心:让每块饼干刚好满足一个孩子的胃口,能够最大可能的物尽其用
class Solution {
public:
int findContentChildren(vector<int>& g, vector<int>& s) {
vector<int> g_ = g;
int result = 0;
for (int i = 0; i < s.size(); i ++ ) {//给没块饼干尽量找一个刚好能满足的小孩,即浪费最小的优先满足
int pos = -1;
int tmp = INT_MAX;
for (int j = 0; j < g_.size(); j++) {
int a = s[i] - g_[j];
if (a >= 0 && a < tmp) {
tmp = a;
pos = j;
}
}
if (tmp != INT_MAX) {
result++;
g_.erase(g_.begin() + pos, g_.begin() + pos + 1);
}
}
return result;
}
};
2.摆动序列
如果连续数字之间的差严格地在正数和负数之间交替,则数字序列称为 摆动序列 。第一个差(如果存在的话)可能是正数或负数。仅有一个元素或者含两个不等元素的序列也视作摆动序列。
例如, [1, 7, 4, 9, 2, 5] 是一个 摆动序列 ,因为差值 (6, -3, 5, -7, 3) 是正负交替出现的。
相反,[1, 4, 7, 2, 5] 和 [1, 7, 4, 5, 5] 不是摆动序列,第一个序列是因为它的前两个差值都是正数,第二个序列是因为它的最后一个差值为零。
子序列 可以通过从原始序列中删除一些(也可以不删除)元素来获得,剩下的元素保持其原始顺序。
给你一个整数数组 nums ,返回 nums 中作为 摆动序列 的 最长子序列的长度 。
示例 1:
输入:nums = [1,7,4,9,2,5]
输出:6
解释:整个序列均为摆动序列,各元素之间的差值为 (6, -3, 5, -7, 3) 。
示例 2:
输入:nums = [1,17,5,10,13,15,10,5,16,8]
输出:7
解释:这个序列包含几个长度为 7 摆动序列。
其中一个是 [1, 17, 10, 13, 10, 16, 8] ,各元素之间的差值为 (16, -7, 3, -3, 6, -8) 。
示例 3:
输入:nums = [1,2,3,4,5,6,7,8,9]
输出:2
分析
最先想到的是动态规划
设dp(i)表示以第i个元素为结尾的摆动序列的最长子序列长度
flag[i]表示以第i个元素为结尾的摆动序列的最长子序列的最后一个差值,
则有:
对j:1到i-1:
如果flag(j)!=1&&flag(j)*(nums[i]-nums[j])<0) : dp[i]=max{dp[i], dp[j]+1},flag[i]=nums[i]-nums[j]
如果dp(j)==1代表以j结尾的摆动序列最大长度为1,此时flag(j)无意义,如果dp(i)<dp(j)+1,则dp[i]=dp[j], flag[i]=nums[i]-nums[j]
class Solution {
public:
int wiggleMaxLength(vector<int>& nums) {
if (nums.size()==1||(nums.size() == 2 && nums[0] == nums[1])) return 1;
if (nums.size() == 2 && nums[0] != nums[1])return 2;
vector<int> dp(nums.size(), 1);
vector<int> flag(nums.size());//从第二个元素开始
if(nums[1]!=nums[0])dp[1] = 2;
flag[1] = nums[1] - nums[0];
for (int i = 2; i < nums.size(); i++) {
for (int j = 1; j < i; j++) {
if (dp[j] == 1) {
if (nums[i] != nums[j] && dp[i] < 2) {
dp[i] = 2;
flag[i] = nums[i] - nums[j];
}
}
if(dp[j]!=1&&flag[j] * (nums[i] - nums[j]) < 0){
if (dp[j] + 1 > dp[i]) {
dp[i] = dp[j] + 1;
flag[i] = nums[i] - nums[j];
}
}
}
}
int result = 0;
for (int i = 0; i < nums.size(); i++) {
result = max(result, dp[i]);
}
return result;
}
};
贪心算法
![](https://i-blog.csdnimg.cn/blog_migrate/7978c772542960398d35290f7b2d7e75.png)
删除单调坡中间的节点
局部最优:删除单调坡度上的节点(不包括单调坡度两端的节点),那么这个坡度就可以有两个局部峰值。
整体最优:整个序列有最多的局部峰值,从而达到最长摆动序列。
局部最优推出全局最优
class Solution {
public:
int wiggleMaxLength(vector<int>& nums) {
//思路,找出连续递增和连续递减区间,然后删除中间元素
//即统计极值点的个数,极值点的特点:极值点分别减前/后两个值的差值异号
if (nums.size() == 1|| (nums.size() == 2 && nums[0] == nums[1])) return 1;
if (nums.size() == 2 && nums[0] != nums[1])return 2;
int result = 1;//最后一个元素
int pre_d=0, after_d;
for (int i = 0; i < nums.size()-1; i++) {
after_d = nums[i + 1] - nums[i];
if ((pre_d <= 0 && after_d > 0) || (pre_d >= 0 && after_d < 0)) {
result++;
pre_d=after_d;
}
}
return result;
}
};
3.最大子数组和
给你一个整数数组 nums ,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
子数组 是数组中的一个连续部分。
示例 1:
输入:nums = [-2,1,-3,4,-1,2,1,-5,4]
输出:6
解释:连续子数组 [4,-1,2,1] 的和最大,为 6 。
示例 2:
输入:nums = [1]
输出:1
示例 3:
输入:nums = [5,4,-1,7,8]
输出:23
分析
第一想到的动态规划
class Solution {
public:
int maxSubArray(vector<int>& nums) {
if (nums.size() == 1)return nums[0];
vector<int> dp(nums.size() + 1, 0);//dp[i]表示以第i个元素结尾的连续子数组的最大和
dp[1] = nums[0];
int result = dp[1];
for (int i = 2; i <= nums.size(); i++)
{
dp[i] = max(dp[i - 1] + nums[i - 1], nums[i - 1]);
result = max(result, dp[i]);
}
return result;
}
};
贪心算法
贪心贪的是哪里呢?
如果 -2 1 在一起,计算起点的时候,一定是从1开始计算,因为负数只会拉低总和,这就是贪心贪的地方!
局部最优:当前“连续和”为负数的时候立刻放弃,从下一个元素重新计算“连续和”,因为负数加上下一个元素 “连续和”只会越来越小。
全局最优:选取最大“连续和”
局部最优的情况下,并记录最大的“连续和”,可以推出全局最优。
从代码角度上来讲:遍历nums,从头开始用count累积,如果count一旦加上nums[i]变为负数,那么就应该从nums[i+1]开始从0累积count了,因为已经变为负数的count,只会拖累总和。
class Solution {
public:
int maxSubArray(vector<int>& nums) {
if (nums.size() == 1)return nums[0];
int count = nums[0];
int result = nums[0];
for (int i = 1; i < nums.size(); i++) {
if (count > 0) {
count += nums[i];
result = max(result, count);
}
else {
count = nums[i];
result = max(result, count);
}
cout << count << endl;
}
return result;
}
};
4.买卖股票的最佳时机 II
给你一个整数数组 prices ,其中 prices[i] 表示某支股票第 i 天的价格。
在每一天,你可以决定是否购买和/或出售股票。你在任何时候 最多 只能持有 一股 股票。你也可以先购买,然后在 同一天 出售。
返回 你能获得的 最大 利润 。
示例 1:
输入:prices = [7,1,5,3,6,4]
输出:7
解释:在第 2 天(股票价格 = 1)的时候买入,在第 3 天(股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5 - 1 = 4 。
随后,在第 4 天(股票价格 = 3)的时候买入,在第 5 天(股票价格 = 6)的时候卖出, 这笔交易所能获得利润 = 6 - 3 = 3 。
总利润为 4 + 3 = 7 。
示例 2:
输入:prices = [1,2,3,4,5]
输出:4
解释:在第 1 天(股票价格 = 1)的时候买入,在第 5 天 (股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5 - 1 = 4 。
总利润为 4 。
示例 3:
输入:prices = [7,6,4,3,1]
输出:0
解释:在这种情况下, 交易无法获得正利润,所以不参与交易可以获得最大利润,最大利润为 0 。
分析
先想到的还是动态规划
dp(i,0)表示第i天持有股票的最大收益,dp(i,1)表示第i天不持有股票的最大收益
则有:
dp(i,0)=max{dp(i-1,0), dp(i-1,1)-price[i]}
dp(i,1)=max{dp(i-1,1), dp(i-1,0)+price[i]}
class Solution {
public:
int maxProfit(vector<int>& prices) {
if (prices.size() == 0)return 0;
vector<vector<int>> dp(prices.size(), vector<int>(2));
dp[0][0] = -prices[0];//0表示持有,1表示不持有
dp[0][1] = 0;
for (int i = 1; i < prices.size(); i++) {
dp[i][0] = max(dp[i - 1][0], dp[i - 1][1] - prices[i]);
dp[i][1] = max(dp[i - 1][1], dp[i - 1][0] + prices[i]);
}
return dp[prices.size() - 1][1];
}
};
贪心算法
要得到最大利润,就不断地在价格最低时买入,价格最高时卖出
假如第0天买入,第3天卖出,那么利润为:prices[3] - prices[0]。
相当于(prices[3] - prices[2]) + (prices[2] - prices[1]) + (prices[1] - prices[0])。
此时就是把利润分解为每天为单位的维度,而不是从0天到第3天整体去考虑!
局部最优:收集每天的正利润,全局最优:求得最大利润。
全局最大利润=局部最优之和
class Solution {
public:
int maxProfit(vector<int>& prices) {
if(prices.size()<=1) return 0;
vector<int> profit(prices.size(), 0);
profit[0] = 0;
int result = 0;
for (int i = 1; i < prices.size(); i++) {
profit[i] = prices[i] - prices[i - 1];
if (profit[i] > 0)result += profit[i];
}
return result;
}
};
5.跳跃游戏
给定一个非负整数数组 nums ,你最初位于数组的 第一个下标 。
数组中的每个元素代表你在该位置可以跳跃的最大长度。
判断你是否能够到达最后一个下标。
示例 1:
输入:nums = [2,3,1,1,4]
输出:true
解释:可以先跳 1 步,从下标 0 到达下标 1, 然后再从下标 1 跳 3 步到达最后一个下标。
示例 2:
输入:nums = [3,2,1,0,4]
输出:false
解释:无论怎样,总会到达下标为 3 的位置。但该下标的最大跳跃长度是 0 , 所以永远不可能到达最后一个下标。
分析
刚开始还是想到的动态规划
设dp(i)表示能否从0到达第i个元素,初始化为0
如果存在j能够到达,且使得,j+nums[j]>=i (题目是可以跳跃nums[i]的距离,因此是>=,如果是只能跳nums[i],则是==)
则dp[i]=true
class Solution {
public:
bool canJump(vector<int>& nums) {
vector<int> dp(nums.size(), false);
dp[0] = true;
for (int i = 1; i < nums.size(); i++) {//求每个dp[i]
for (int j = 0; j < i; j++) {
if (dp[j] == true && (j + nums[j] >= i)) {
dp[i] = true;
break;
}
}
}
return dp[nums.size() - 1];
}
};
超时了
贪心
每次都跳最远,在覆盖范围内不断更新最大覆盖范围,看能否覆盖终点
class Solution {
public:
bool canJump(vector<int>& nums) {
int distance = 0;//记录最大覆盖范围
for (int i = 0; i < nums.size(); i++) {
if (i <= distance) {//判断能否到达i
distance = max(distance, i + nums[i]);//更新覆盖范围
}
else return false;//i都到不了,后面更到不了
}
return true;
}
};
6.跳跃游戏II
给定一个长度为 n 的 0 索引整数数组 nums。初始位置为 nums[0]。
每个元素 nums[i] 表示从索引 i 向前跳转的最大长度。换句话说,如果你在 nums[i] 处,你可以跳转到任意 nums[i + j] 处:
0 <= j <= nums[i]
i + j < n
返回到达 nums[n - 1] 的最小跳跃次数。生成的测试用例可以到达 nums[n - 1]。
示例 1:
输入: nums = [2,3,1,1,4]
输出: 2
解释: 跳到最后一个位置的最小跳跃数是 2。
从下标为 0 跳到下标为 1 的位置,跳 1 步,然后跳 3 步到达数组的最后一个位置。
示例 2:
输入: nums = [2,3,0,1,4]
输出: 2
分析:上一题是问能否到达终点,这题是问跳到终点最少跳多少步
方法一,动态规划
dp[i]表示从0跳到i所需的最小步数
if(dp[j] != INT_MAX && (j + nums[j] >= i)) dp[i] = min(dp[i], dp[j] + 1); j从0到i-1
class Solution {
public:
int jump(vector<int>& nums) {
if (nums.size() == 0)return 0;
vector<int> dp(nums.size(), INT_MAX);
dp[0] = 0;
for (int i = 1; i < nums.size(); i++) {//求dp[i]
for (int j = 0; j < i; j++) {
if (dp[j] != INT_MAX && (j + nums[j] >= i)) {
dp[i] = min(dp[i], dp[j] + 1);
}
}
}
return dp[nums.size() - 1];
}
};
方法二,贪心
思路一:
我们的目标是到达数组的最后一个位置,因此我们可以考虑最后一步跳跃前所在的位置,该位置通过跳跃能够到达最后一个位置。
如果有多个位置通过跳跃都能够到达最后一个位置,那么我们应该如何进行选择呢?直观上来看,我们可以「贪心」地选择距离最后一个位置最远的那个位置,也就是对应下标最小的那个位置。因此,我们可以从左到右遍历数组,选择第一个满足要求的位置。
class Solution {
public:
int jump(vector<int>& nums) {
if (nums.size() == 0)return 0;
int position = nums.size() - 1;
int step = 0;
while (position > 0) {
for (int i = 0; i < position; i++) {
if (i + nums[i] >= position) {
position = i;
step++;
break;
}
}
}
return step;
}
};
思路二:
每次在上次能跳到的范围(end)内选择一个能跳的最远的位置(也就是能跳到max_far位置的点)作为下次的起跳点
class Solution {
public:
int jump(vector<int>& nums)
{
int max_far = 0;// 目前能跳到的最远位置
int step = 0; // 跳跃次数
int end = 0; // 上次跳跃可达范围右边界(下次的最右起跳点)
for (int i = 0; i < nums.size() - 1; i++)
{
max_far = max(max_far, i + nums[i]);
// 到达上次跳跃能到达的右边界了
if (i == end)
{
end = max_far; // 目前能跳到的最远位置变成了下次起跳位置的有边界
step++; // 进入下一次跳跃
}
}
return step;
}
};