文章目录
贪心算法
- 贪心的本质是选择每一阶段的局部最优,从而达到全局最优。如何通过局部最优,推出整体最优。
- 贪心算法的套路就是常识性推导加上举反例。
- 贪心算法解题思路:想清楚局部最优是什么,如果推导出全局最优,就够了。
55. 跳跃游戏
思路:
- 每次移动一个单位,取最大跳跃步数,获得最大覆盖范围,再判断该范围是否可以覆盖到终点!
- 贪心算法局部最优解:每次取最大跳跃步数(取最大覆盖范围),整体最优解:最后得到整体最大覆盖范围,看是否能到终点。
步骤:
- 下标i每次只能在cover范围内移动
- cover每次只取max(该元素数值补充后的范围, cover本身范围)。
- 如果cover 大于等于终点下标,直接return true就可以了。
代码:
class Solution {
public:
bool canJump(vector<int>& nums) {
if(nums.size() == 1) return true;//单元素数组 肯定可以到达终点
int cover = 0;
for(int i=0; i<=cover; i++)//i每次只能在cover范围内移动
{
cover = max(i+nums[i], cover);//更新最大覆盖范围
if(cover >= nums.size()-1) return true;//可以覆盖到终点
}
return false;
}
};
45.跳跃游戏II
方法一 考虑终点
思路:
- 如果当前步的最大覆盖距离还没覆盖,以最小的步数增加覆盖范围,即再走一步增加覆盖范围,覆盖范围一旦覆盖了终点,就得到最小步数
- 贪心算法局部最优:当前可移动距离尽可能大,如果还没到终点,步数再加一。整体最优:一步尽可能多走,从而达到最小步数。
- 需要统计两个覆盖范围,当前这一步的最大覆盖和下一步最大覆盖。
注意:
- 1.更新下一步覆盖最大距离=max(该元素数值补充后的范围, 本身范围)。
- 2.还有个特殊情况,当移动下标达到了当前覆盖的最远距离下标时:
- 如果当前覆盖最远距离下标不是集合终点,步数就加一,还需要继续走,然后再判断下一步是否可以到达终点,可以就结束。
- 如果当前覆盖最远距离下标就是是集合终点,步数不用加一,因为不能再往后走了,直接结束
代码:
class Solution {
public:
int jump(vector<int>& nums) {
if(nums.size() == 1) return true;
int curdistance = 0;// 当前覆盖最远距离下标
int nextdistance = 0;// 下一步覆盖最远距离下标
int leap = 0;// 记录走的最大步数
for(int i=0; i<nums.size(); i++)
{
nextdistance = max(nums[i] + i, nextdistance);// 更新下一步覆盖最远距离下标
// 当移动下标达到了当前覆盖的最远距离下标时
if(i == curdistance)
{
// 如果当前覆盖最远距离下标不是终点 再走一步 增加覆盖范围
if(curdistance < nums.size()-1)
{
leap++;
curdistance = nextdistance;
// 下一步的覆盖范围已经可以达到终点,结束循环
if(nextdistance >= nums.size()-1) break;
}
else break;// 当前覆盖最远距到达集合终点,直接结束
}
}
return leap;
}
};
方法二 不终点
思路:
如果不考虑终点,那么移动下标只要遇到当前覆盖最远距离的下标,直接步数加一。因此只要让移动下标最远只能移动到nums.size - 2。当移动下标指向nums.size - 2时,
- 如果移动下标等于当前覆盖最大距离下标, 需要再走一步,即leap++,最后一步一定可以到终点
- 如果移动下标不等于当前覆盖最大距离下标,说明当前覆盖最远距离就可以直接达到终点了,不需要再走一步。
关键在于控制移动下标i只移动到nums.size() - 2的位置,所以移动下标只要遇到当前覆盖最远距离的下标,直接步数加一,不用考虑终点了
代码:
class Solution {
public:
//不考虑终点
int jump(vector<int>& nums)
{
int curdistance = 0;
int nextdistance = 0;
int leap = 0;
//关键 i最远指向nums.size()-1 考虑终点i最远指向nums.size()
for(int i=0; i<nums.size()-1; i++)
{
nextdistance = max(i+nums[i], nextdistance);
if(i==curdistance)//移动下标等于当前覆盖最大距离下标 需要再走一步
{
leap++;
curdistance = nextdistance;//更新当前覆盖的最远距离下标
}
}
return leap;
}
};
134. 加油站
暴力解法 双层循环 for+while
思路: for循环适合模拟从头到尾的遍历,而while循环适合模拟环形遍历,要善于使用while
代码: 力扣可以过用例,但是会超时
class Solution {
public:
//暴力解法
int canCompleteCircuit(vector<int>& gas, vector<int>& cost) {
for(int i=0; i<cost.size(); i++)
{
int res = gas[i] - cost[i];//剩余油量
int index = (i+1) % cost.size();//行驶起点
// 模拟以i为起点行驶一圈
while(res > 0 && index != i)
{
res += gas[index] - cost[index];
index = (index + 1) % cost.size();
}
// 如果以i为起点跑一圈,剩余油量>=0,返回该起始位置
if(res >= 0 && index == i) return i;
}
return -1;
}
};
贪心算法 全局最优
思路: 直接从全局进行贪心选择,情况如下
- 情况一:如果gas的总和小于cost总和,无论从哪出发,一定跑不了一圈
- 情况二:rest[i] = gas[i]-cost[i]为一天剩下的油,i从0开始累加到最后一站,如果累加没有出现负数,说明从0出发,油就没有断过,那么0就是起点。
- 情况三:如果累加的最小值是负数,汽车就要从非0节点出发,从后向前,看哪个节点能把这个负数填平,能把这个负数填平的节点就是出发节点。
代码:
class Solution {
public:
//贪心算法 方法1
int canCompleteCircuit(vector<int>& gas, vector<int>& cost)
{
int gassum = 0;
int gasmin = INT_MAX;//从起点出发,记录油箱里的油量最小值
int res = 0;//油箱剩余油量
//1.从0开始出发 累加油量 记录油箱剩余油量最小值
for(int i=0; i<gas.size(); i++)
{
res = gas[i] - cost[i];
gassum += res;
if(gassum < gasmin) gasmin = gassum;
}
//2.gas的总和小于cost总和
if(gassum < 0) return -1;
//3.油箱剩余油量最小值≥0 从0出发最后回到0
if(gasmin >= 0) return 0;
//4.油箱剩余油量最小值<0 从非0节点出发 寻找该节点
for(int i=gas.size()-1; i>=0; i--)
{
res = gas[i] - cost[i];
gasmin += res;//负数填平
if(gasmin >= 0) return i;
}
return -1;
}
};
贪心算法 局部最优→全局最优
思路
- 如果总油量减去总消耗油量≥零,那么一定可以跑完一圈,说明 各个加油站的**剩油量rest[i]**的和一定大于等于零。
- i从0开始,累加rest[i],rest[i]=gas[i] - cost[i],剩油量和gassum小于0,说明[0, i]区间都不能作为起始位置,选择该区间任一个位置作为起点,没无法到达 i位置,只能从i+1开始,重新累加油量和
- 局部最优:当前累加rest[i]和gassum一旦小于0,起始位置至少是i+1,才可以跑完一圈。全局最优:找到可以跑一圈的起始位置。
i+1后面就不会出现更大的负数? 如果出现更大的负数,就更新i,起始位置变成新的i+1。
有没有可能 [0,i] 区间 选某一个作为起点,累加到i,gassum不小于零?
gassum<0,区间和1+区间和2<0,又区间和2>0,所以区间和1<0,那么就会重新选择起始位置直到gassum不小于0,也就是图中假设位置。
代码:
class Solution {
public:
//贪心算法 方法2
int canCompleteCircuit(vector<int>& gas, vector<int>& cost)
{
int start = 0;//起始位置 跑完一圈的位置
int gassum = 0;//当前起始位置的剩余油量 累加rest[i]和
int totalsum = 0;//跑一圈总耗油量
for(int i=0; i<gas.size(); i++)
{
gassum += gas[i] - cost[i];
totalsum += gas[i] - cost[i];
//当前累加rest[i]和 gassum<0
if(gassum < 0)
{
start = i+1;//更新起始位置 也就是可以跑完一圈的位置
gassum = 0;//剩余油量 累加rest[i]和清空 要重新计算
}
}
if(totalsum < 0) return -1;//总耗油量<0 不可能跑一圈
return start;
}
};
135. 分发糖果
思路,采用了两次贪心的策略:
一次是从左到右遍历,只比较右边孩子评分比左边大的情况。
一次是从右到左遍历,只比较左边孩子评分比右边大的情况。
1.先确定右边孩子评分高于左边孩子评分情况,从前向后遍历
如果ratings[i] > ratings[i - 1],那么[i]的糖 一定要比[i - 1]的糖多一个,第i个小孩的糖果数量为candyVec[i] = candyVec[i - 1] + 1
局部最优:只要右孩子评分比左孩子大,右孩子就多一个糖果;全局最优:相邻的孩子中,评分高的右孩子获得比左边孩子更多的糖果。
2.再确定左边孩子评分高于右边孩子情况,从后向前遍历
3.为什么不能从前向后遍历呢?
- rating[3]应该是获得糖果数最多的,依次是 rating[4], rating[5], rating[6]与 rating[2]、 rating[1]并列, rating[0]。如果从前向后遍历,得到的结果是[1, 1, 1, 2, 2, 2, 1],并不符合题目要求。
- 要让rating[3]获得最多糖果,需要与rating[4]、rating[5]、rating[6]都比较,rating[3]与rating[4]的比较要利用rating[5]与rating[4]的比较结果,从前往后遍历是无法得知rating[3]的评分最高。
- 因此,要从后向前遍历,rating[3]与rating[4]的比较才能利用rating[5]与rating[4]的比较结果。
4.candyVec[i]选择
- 如果ratings[i] > ratings[i + 1],此时candyvec[i]有两个选择,一个是从前向后遍历的candyVec[i + 1] + 1,一个是从后向前遍历的candyVec[i]。
- 贪心局部最优:取candyVec[i + 1] + 1 和 candyVec[i] 最大的糖果数量,保证第i个小孩的糖果数量既保持对左边candyVec[i - 1]的糖果多,也比右边candyVec[i + 1]的糖果多。全局最优:相邻的孩子中,评分高的孩子获得更多的糖果。
代码:
class Solution {
public:
int candy(vector<int>& ratings) {
vector<int> candyVec(ratings.size(), 1);//每个孩子都有一个糖果
//从前往后
for(int i=1; i<ratings.size(); i++)//两个孩子比较
{
if(ratings[i] > ratings[i-1]) candyVec[i] = candyVec[i-1] + 1;
}
//从后往前
for(int i=ratings.size()-2; i>=0; i--)//两个孩子比较
{
if(ratings[i] > ratings[i+1]) candyVec[i] = max(candyVec[i], candyVec[i+1]+1);
}
int result = 0;
for(int c : candyVec) result += c;
return result;
}
};
860.柠檬水找零
-
有三种情况:
情况一:账单是5,直接收下
情况二:账单是10,消耗一个5,增加一个10
情况三:账单是20,优先消耗一个10和一个5,如果不够,再消耗三个5 -
局部最优:遇到账单20,优先消耗美元10,完成本次找零。全局最优:完成全部账单的找零。
代码:
class Solution {
public:
bool lemonadeChange(vector<int>& bills) {
int five = 0, ten = 0;
for(int bill : bills)
{
//情况一:账单是5,直接收下
if(bill == 5) five++;
//情况二:账单是10,消耗一个5,增加一个10
if(bill == 10)
{
if(five <= 0) return false;//没有零钱找了
five--;
ten++;
}
//情况三:账单是20,优先消耗一个10和一个5,如果不够,再消耗三个5
if(bill == 20)
{
if(five > 0 && ten > 0)//有5和10的零钱
{
ten--;
five--;
}
//没有10 但有3个5
else if(five >= 3) five -= 3;
else return false;
}
}
return true;
}
};
406.根据身高重建队列
思路:
- 按照身高从大到小排序,身高相同的话则k小的站前面,让高个子在前面。然后,优先按身高高的people的k来插入,后序插入节点也不会影响前面已经插入的节点,最终按照k的规则完成了队列。
- 在按照身高从大到小排序后,贪心算法的局部最优:优先按身高高的people的k来插入,插入操作过后的people满足队列属性;全局最优:最后都做完插入操作,整个队列满足题目队列属性
过程:
- 排序完的people: [[7,0], [7,1], [6,1], [5,0], [5,2],[4,4]]
- 插入的过程:
- 插入[7,0]:[[7,0]]
- 插入[7,1]:[[7,0],[7,1]]
- 插入[6,1]:[[7,0],[6,1],[7,1]]
- 插入[5,0]:[[5,0],[7,0],[6,1],[7,1]]
- 插入[5,2]:[[5,0],[7,0],[5,2],[6,1],[7,1]]
- 插入[4,4]:[[5,0],[7,0],[5,2],[6,1],[4,4],[7,1]]
代码:
- 数组实现
class Solution {
public:
static bool cmp(const vector<int>& a, const vector<int>& b)
{
if(a[0]==b[0]) return a[1] < b[1];//身高相同 k小的在前面
return a[0] > b[0];//身高高的在前面
}
vector<vector<int>> reconstructQueue(vector<vector<int>>& people) {
//1.排序
sort(people.begin(), people.end(), cmp);
//2.数组
vector<vector<int>> que;
for(int i=0; i<people.size(); i++)
{
int position = people[i][1];//队列插入位置
que.insert(que.begin()+position, people[i]);
}
return que;
}
};
- 链表实现
class Solution {
public:
static bool cmp(const vector<int>& a, const vector<int>& b)
{
if(a[0]==b[0]) return a[1] < b[1];//身高相同 k小的在前面
return a[0] > b[0];//身高高的在前面
}
vector<vector<int>> reconstructQueue(vector<vector<int>>& people) {
//1.排序
sort(people.begin(), people.end(), cmp);
//3.链表实现
list<vector<int>> que;
for(int i=0; i<people.size(); i++)
{
int position = people[i][1];
std::list<vector<int>>::iterator it = que.begin();//起始迭代器
//找到插入位置 链表只能依次访问
while(position--)
{
it++;
}
que.insert(it, people[i]);
}
return vector<vector<int>>(que.begin(), que.end());//转换类型
}
};
使用动态数组vector来insert很费时,如果插入元素大于预先普通数组大小,vector底部先扩容,即申请两倍于原先普通数组的大小,然后把数据拷贝到另一个更大的数组上。链表虽然没有数组访问便捷,但是插入时快很多。