Leetcode Hot 100 ——贪心算法

前置知识

一、贪心的本质是选择每一阶段的局部最优,从而达到全局最优。

这么说有点抽象,来举一个例子:

例如,有一堆钞票,你可以拿走十张,如果想达到最大的金额,你要怎么拿?

指定每次拿最大的,最终结果就是拿走最大数额的钱。

每次拿最大的就是局部最优,最后拿走最大数额的钱就是推出全局最优。

再举一个例子如果是有一堆盒子,你有一个背包体积为n,如何把背包尽可能装满,如果还每次选最大的盒子,就不行了。这时候就需要动态规划。动态规划的问题在下一个系列会详细讲解。

二、贪心算法并没有固定的套路。 所以唯一的难点就是如何通过局部最优,推出整体最优。

那么如何能看出局部最优是否能推出整体最优呢? 有没有什么固定策略或者套路呢?

也没有! 靠自己手动模拟,如果模拟可行,就可以试一试贪心策略,如果不可行,可能需要动态规划。

如何验证可不可以用贪心算法呢?

最好用的策略就是举反例,如果想不到反例,那么就试一试贪心吧。

可有有同学认为手动模拟,举例子得出的结论不靠谱,想要严格的数学证明。

一般数学证明有如下两种方法:数学归纳法与反证法。

三、贪心算法一般分为如下四步:

① 将问题分解为若干个子问题

② 找出适合的贪心策略

③ 求解每一个子问题的最优解

④ 将局部最优解堆叠成全局最优解

这个四步其实过于理论化了,我们平时在做贪心类的题目时,如果按照这四步去思考,真是有点"鸡肋"。做题的时候,只要想清楚 局部最优 是什么,如果推导出全局最优,其实就够了。

121. 买卖股票的最佳时机

思路与解法

思路就是:如果第i天卖出股票,则最大利润为(该天的股价 - 前面天数中最小的股价),然后与已知的最大利润比较,如果大于则更新当前最大利润的值。

注意只有一次遍历!

cpp 复制代码
class Solution {
public:
    int maxProfit(vector<int>& prices) {
        int minPrice = INT_MAX,maxProfit=0;
        for(int i=0;i<prices.size();i++){
            //更新到目前为止的最低价格
            minPrice = min(minPrice,prices[i]); 
            //更新最大利润
            maxProfit = max(maxProfit,prices[i]-minPrice);
        }
        return maxProfit;
    }
};

【注】

1、minPrice 包含了当天的价格是合理的,因为不可能出现当天价格最低,然后当天利润最高的情况(因为此时利润是0)

ACM:

cpp 复制代码
#include <bits/stdc++.h>
#include <vector>
#include <algorithm>
using namespace std;

class Solution {
public:
    int maxProfit(vector<int>& prices) {
        int minPrice = INT_MAX,maxProfit=0;
        for(int i=0;i<prices.size();i++){
            //更新到目前为止的最低价格
            minPrice = min(minPrice,prices[i]); 
            //更新最大利润
            maxProfit = max(maxProfit,prices[i]-minPrice);
        }
        return maxProfit;
    }
};

int main(){
    vector<int> prices;
    int price;
    while(cin>>price){
        prices.push_back(price);
    }
    Solution sol;

    cout<<sol.maxProfit(prices)<<endl;

    return 0;
}

跳跃游戏

思路与解法

刚看到本题一开始可能想:当前位置元素如果是 3,我究竟是跳一步呢,还是两步呢,还是三步呢,究竟跳几步才是最优呢?
其实跳几步无所谓,关键在于可跳的覆盖范围!

不一定非要明确一次究竟跳几步,每次取最大的跳跃步数,这个就是可以跳跃的覆盖范围。

这个范围内,别管是怎么跳的,反正一定可以跳过来。

那么这个问题就转化为跳跃覆盖范围究竟可不可以覆盖到终点

每次移动取最大跳跃步数(得到最大的覆盖范围),每移动一个单位,就更新最大覆盖范围。
贪心算法局部最优解: 每次取最大跳跃步数(取最大覆盖范围),整体最优解: 最后得到整体最大覆盖范围,看是否能到终点。

cpp 复制代码
class Solution {
public:
    bool canJump(vector<int>& nums) {
        int cover = 0;
        if (nums.size() == 1) return true; // 只有一个元素,就是能达到
        for (int i = 0; i <= cover; i++) { // 注意这里是小于等于cover
            cover = max(i + nums[i], cover);
            if (cover >= nums.size() - 1) return true; // 说明可以覆盖到终点了
        }
        return false;
    }
};

【注】

1、cover不断更新的是所能到达的最远元素的下标!

跳跃游戏 II

本题思路是相似的,还是要看最大覆盖范围

本题要计算最少步数,那么就要想清楚什么时候步数才一定要加一呢?

贪心的思路,局部最优:当前可移动距离尽可能多走,如果还没到终点,步数再加一。

整体最优:一步尽可能多走,从而达到最少步数。

思路虽然是这样,但在写代码的时候还不能真的能跳多远就跳多远,那样就不知道下一步最远能跳到哪里了。要从覆盖范围出发,不管怎么跳,覆盖范围内一定是可以跳到的,以最小的步数增加覆盖范围,覆盖范围一旦覆盖了终点,得到的就是最少步数!

这里需要统计两个覆盖范围,当前这一步的最大覆盖和下一步最大覆盖

如果移动下标达到了当前覆盖的最远距离下标时,还没有到终点的话,那么就必须再走一步来增加覆盖范围,直到覆盖范围覆盖了终点。

cpp 复制代码
class Solution {
public:
    int jump(vector<int>& nums) {
        int curDistance = 0;  //当前覆盖最远距离下标
        int nextDistance = 0; //下一步覆盖最远距离下标
        int res = 0;          //最大步数
        for(int i=0;i<nums.size();i++){
            //更新下一步覆盖最远距离下标
            nextDistance=max(i+nums[i],nextDistance);
            if(i==curDistance){     //遇到当前覆盖最远距离下标
                if(curDistance>=nums.size()-1) break;
                res++;
                curDistance=nextDistance;
            }
        }
        return res;
        
    }
};

【注】

1、for循环里先更新下一步覆盖最远距离下标 nextDistance,i到达当前覆盖最远距离下标且判断未到末尾,则将当前覆盖最远距离下标赋值为for循环刚开始计算的nextDistance,同时res++。

763. 划分字母区间

思路与解法

题目要求同一字母只能出现在一个片段中,那么如何把同一个字母的都圈在同一个区间里呢

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

可以分为如下两步:

1、统计每一个字符最后出现的位置(利用哈希数组)

2、从头遍历字符,并更新字符的最远出现下标,如果找到字符最远出现位置下标和当前下标相等了,则找到了分割点

如图:

cpp 复制代码
class Solution {
public:
    vector<int> partitionLabels(string s) {
        int hash[26];
        // 统计每一个字符最后出现的位置
        for(int i=0;i<s.size();i++){
            hash[s[i]-'a']=i;
        }
        int left=0;
        int right=0;
        vector<int> result;
        for(int i=0;i<s.size();i++){
            right = max(hash[s[i]-'a'],right);
            if(right==i){
                result.push_back(right-left+1);
                left = i+1; //更新左边界
            }
        }
        return result;       
    }
};

【注】

1、哈希数组使用 int hash[27]即可,不需要vector<int>

① 字符集大小固定为 26,无需动态扩展。

② 数组在栈上分配,内存开销极小,且访问速度快。

2、因为需要返回区间长度,定义左边界left和右边界right。

在划分好一段后别忘了更新左边界!!

相关推荐
qiuyunoqy2 小时前
Linux进程 --- 5(进程地址空间初识)
linux·c++·算法
AC__dream2 小时前
2024年秋招-美团-技术岗-第一批笔试
数据结构·算法
计算机安禾2 小时前
【C语言程序设计】第28篇:指针的概念与指针变量
c语言·开发语言·数据结构·c++·算法·visual studio code·visual studio
lxh01132 小时前
串联所有单词的子串
算法
像污秽一样2 小时前
算法设计与分析-习题5.4
数据结构·算法·排序算法
IronMurphy2 小时前
【算法二十四】101. 对称二叉树 543. 二叉树的直径
数据结构·算法·leetcode
qingy_20462 小时前
Java基础:数据类型
java·开发语言·算法
小璐资源网2 小时前
排序算法概览:十大排序算法一览
数据结构·算法·排序算法
Allen_LVyingbo2 小时前
PostgreSQL动态分区裁剪技术:查询性能优化解析(2026年版)
数据库·算法·观察者模式·postgresql·性能优化·架构