贪心算法理论基础

文章目录

  • 贪心算法理论基础
    • [1. 贪心算法的基本概念](#1. 贪心算法的基本概念)
      • [1.1 基本术语](#1.1 基本术语)
      • [1.2 贪心算法的特点](#1.2 贪心算法的特点)
      • [1.3 贪心算法的本质](#1.3 贪心算法的本质)
    • [2. 贪心算法的适用条件](#2. 贪心算法的适用条件)
      • [2.1 贪心选择性质](#2.1 贪心选择性质)
      • [2.2 最优子结构](#2.2 最优子结构)
      • [2.3 无后效性](#2.3 无后效性)
    • [3. 贪心算法与动态规划的区别](#3. 贪心算法与动态规划的区别)
      • [3.1 核心区别](#3.1 核心区别)
      • [3.2 如何选择](#3.2 如何选择)
    • [4. 贪心算法的解题步骤](#4. 贪心算法的解题步骤)
      • [4.1 第一步:理解问题](#4.1 第一步:理解问题)
      • [4.2 第二步:分析局部最优策略](#4.2 第二步:分析局部最优策略)
      • [4.3 第三步:验证贪心选择性质](#4.3 第三步:验证贪心选择性质)
      • [4.4 第四步:实现算法](#4.4 第四步:实现算法)
    • [5. 贪心算法常见题型](#5. 贪心算法常见题型)
      • [5.1 分配问题](#5.1 分配问题)
      • [5.2 区间问题](#5.2 区间问题)
      • [5.3 跳跃问题](#5.3 跳跃问题)
      • [5.4 股票问题](#5.4 股票问题)
      • [5.5 其他问题](#5.5 其他问题)
    • [6. 贪心算法模板](#6. 贪心算法模板)
      • [6.1 分配问题模板](#6.1 分配问题模板)
      • [6.2 区间问题模板](#6.2 区间问题模板)
      • [6.3 跳跃问题模板](#6.3 跳跃问题模板)
      • [6.4 股票问题模板](#6.4 股票问题模板)
      • [6.5 划分字母区间模板](#6.5 划分字母区间模板)
      • [6.6 其他问题模板](#6.6 其他问题模板)
    • [7. 贪心算法经典题目详解](#7. 贪心算法经典题目详解)
      • [7.1 分发饼干(455)](#7.1 分发饼干(455))
      • [7.2 用最少数量的箭引爆气球(452)](#7.2 用最少数量的箭引爆气球(452))
      • [7.3 跳跃游戏(55)](#7.3 跳跃游戏(55))
      • [7.4 跳跃游戏II(45)](#7.4 跳跃游戏II(45))
      • [7.5 买卖股票的最佳时机(121)](#7.5 买卖股票的最佳时机(121))
      • [7.6 买卖股票的最佳时机II(122)](#7.6 买卖股票的最佳时机II(122))
      • [7.7 划分字母区间(763)](#7.7 划分字母区间(763))
    • [8. 贪心算法的时间复杂度](#8. 贪心算法的时间复杂度)
      • [8.1 时间复杂度分析](#8.1 时间复杂度分析)
      • [8.2 空间复杂度分析](#8.2 空间复杂度分析)
    • [9. 何时使用贪心算法](#9. 何时使用贪心算法)
      • [9.1 使用贪心算法的场景](#9.1 使用贪心算法的场景)
      • [9.2 判断标准](#9.2 判断标准)
      • [9.3 贪心算法的优缺点](#9.3 贪心算法的优缺点)
    • [10. 贪心算法的证明方法](#10. 贪心算法的证明方法)
      • [10.1 反证法](#10.1 反证法)
      • [10.2 数学归纳法](#10.2 数学归纳法)
      • [10.3 交换论证](#10.3 交换论证)
      • [10.4 举反例](#10.4 举反例)
    • [11. 常见题型总结](#11. 常见题型总结)
      • [11.1 分配问题类](#11.1 分配问题类)
      • [11.2 区间问题类](#11.2 区间问题类)
      • [11.3 跳跃问题类](#11.3 跳跃问题类)
      • [11.4 股票问题类](#11.4 股票问题类)
      • [11.5 其他问题类](#11.5 其他问题类)
    • [12. 总结](#12. 总结)

贪心算法理论基础

1. 贪心算法的基本概念

**贪心算法(Greedy Algorithm)**是一种在每一步选择中都采取在当前状态下最好或最优(即最有利)的选择,从而希望导致结果是全局最好或最优的算法策略。

1.1 基本术语

  • 局部最优:在当前步骤中做出的最优选择
  • 全局最优:整个问题的最优解
  • 贪心选择性质:可以通过局部最优选择来构造全局最优解
  • 最优子结构:问题的最优解包含子问题的最优解
  • 无后效性:当前的选择不会影响后续选择的状态

1.2 贪心算法的特点

  • 局部最优:每一步都选择当前最优解
  • 不回溯:一旦做出选择,就不再改变
  • 高效性:时间复杂度通常较低
  • 适用性有限:不是所有问题都适合贪心算法

核心思想

复制代码
贪心算法的核心思想是:局部最优 -> 全局最优

想清楚局部最优,想清楚全局最优,感觉局部最优是可以推出全局最优,
并想不出反例,那么就试一试贪心。

示例

复制代码
贪心算法就像在迷宫中每次都选择当前看起来最好的路径:
- 每一步都选择距离目标最近的方向
- 不回溯,不改变已做的选择
- 希望最终能找到最优路径

1.3 贪心算法的本质

贪心算法的本质是局部最优策略的全局应用

  • 通过每一步的局部最优选择,期望得到全局最优解
  • 不需要考虑所有可能的解,只需要考虑当前最优
  • 通常需要证明贪心策略的正确性

关键点

  • 贪心选择性质:可以通过局部最优选择来构造全局最优解
  • 最优子结构:问题的最优解包含子问题的最优解
  • 无后效性:当前的选择不会影响后续选择的状态

2. 贪心算法的适用条件

2.1 贪心选择性质

定义:可以通过局部最优选择来构造全局最优解。

判断方法

  • 每一步都选择当前最优解
  • 不需要考虑未来的影响
  • 局部最优可以推导出全局最优

示例

  • 分发饼干:优先满足胃口大的孩子(局部最优),可以最大化满足的孩子数量(全局最优)
  • 跳跃游戏:每次跳到能跳最远的位置(局部最优),可以到达最远的位置(全局最优)

2.2 最优子结构

定义:问题的最优解包含子问题的最优解。

判断方法

  • 问题可以分解为子问题
  • 子问题的最优解可以组合成原问题的最优解
  • 子问题之间相互独立

示例

  • 合并区间:合并后的区间是最优的,包含的子区间也是最优的
  • 无重叠区间:删除最少的区间后,剩余区间是最优的

2.3 无后效性

定义:当前的选择不会影响后续选择的状态。

判断方法

  • 当前选择只影响当前状态
  • 不会因为之前的选择而改变后续选择
  • 状态转移是单向的

示例

  • 买卖股票:今天的买卖决策不影响明天的决策
  • 柠檬水找零:每次找零都是独立的,不影响后续找零

3. 贪心算法与动态规划的区别

3.1 核心区别

特性 贪心算法 动态规划
选择方式 每一步都选择当前最优 考虑所有可能的选择
状态转移 只考虑当前状态 考虑所有历史状态
回溯 不回溯,不改变选择 可能回溯,改变选择
时间复杂度 通常较低 O(n) 或 O(n log n) 通常较高 O(n²) 或更高
空间复杂度 通常较低 O(1) 或 O(n) 通常较高 O(n) 或 O(n²)
适用问题 具有贪心选择性质的问题 具有最优子结构的问题

3.2 如何选择

使用贪心算法

  • 问题具有贪心选择性质
  • 可以通过局部最优推导全局最优
  • 时间复杂度要求较低

使用动态规划

  • 问题具有最优子结构
  • 需要保存历史状态
  • 贪心策略无法得到最优解

示例对比

  • 跳跃游戏:可以用贪心(每次跳最远),也可以用DP(记录每个位置是否可达)
  • 背包问题:0-1背包用DP,分数背包用贪心

4. 贪心算法的解题步骤

4.1 第一步:理解问题

关键点

  • 明确问题的目标
  • 理解约束条件
  • 确定优化目标(最大化或最小化)

示例

  • 分发饼干:目标是最多满足几个孩子,约束是每个孩子只能分一个饼干
  • 跳跃游戏:目标是能否到达最后一个位置,约束是每次最多跳nums[i]步

4.2 第二步:分析局部最优策略

关键点

  • 思考每一步应该选择什么
  • 确定局部最优的选择标准
  • 考虑如何量化"最优"

示例

  • 分发饼干:局部最优 = 优先满足胃口大的孩子(用最大的饼干满足最大的胃口)
  • 跳跃游戏:局部最优 = 每次跳到能跳最远的位置

4.3 第三步:验证贪心选择性质

关键点

  • 证明局部最优可以推导全局最优
  • 思考反例,看是否存在反例
  • 如果找不到反例,可以尝试贪心

验证方法

  • 数学证明(如果可能)
  • 举反例验证
  • 通过测试用例验证

4.4 第四步:实现算法

关键点

  • 按照贪心策略实现
  • 注意边界条件
  • 优化时间复杂度

实现步骤

  1. 排序(如果需要)
  2. 遍历,应用贪心策略
  3. 更新结果

5. 贪心算法常见题型

5.1 分配问题

特点:需要将资源分配给多个对象,使得某种目标最优。

常见题目

  • 455.分发饼干:用饼干满足孩子
  • 135.分发糖果:给每个孩子分糖果
  • 860.柠檬水找零:找零问题

核心思路

  • 排序:通常需要先排序
  • 匹配:按照贪心策略进行匹配
  • 优化:最大化或最小化目标值

5.2 区间问题

特点:处理区间重叠、合并、选择等问题。

常见题目

  • 452.用最少数量的箭引爆气球:区间重叠问题
  • 435.无重叠区间:删除最少的重叠区间
  • 56.合并区间:合并所有重叠区间
  • 763.划分字母区间:划分不重叠的区间

核心思路

  • 排序:按起点或终点排序
  • 贪心策略:选择结束最早的区间(或开始最晚的区间)
  • 判断重叠:判断区间是否重叠

5.3 跳跃问题

特点:在数组上跳跃,求能否到达或最少步数。

常见题目

  • 55.跳跃游戏:判断能否到达最后一个位置
  • 45.跳跃游戏II:求到达最后一个位置的最少步数

核心思路

  • 贪心策略:每次跳到能跳最远的位置
  • 维护最远距离:记录当前能到达的最远位置
  • 更新步数:当需要跳跃时更新步数

5.4 股票问题

特点:买卖股票,使得利润最大。

常见题目

  • 122.买卖股票的最佳时机II:可以多次买卖

核心思路

  • 贪心策略:只要今天价格比昨天高就买卖
  • 累加利润:将所有正利润累加

5.5 其他问题

常见题目

  • 376.摆动序列:求最长摆动子序列
  • 53.最大子序和:求最大子数组和(也可以用DP)
  • 1005.K次取反后最大化的数组和:K次取反后最大和
  • 738.单调递增的数字:找到小于等于n的最大单调递增数字
  • 406.根据身高重建队列:按身高和前面人数重建队列

6. 贪心算法模板

6.1 分配问题模板

适用场景:资源分配问题,如分发饼干、分发糖果等。

核心思路

  • 排序:对资源和要求进行排序
  • 贪心匹配:按照贪心策略进行匹配
  • 更新结果:匹配成功后更新结果

模板代码

cpp 复制代码
// 分发饼干模板
class Solution {
public:
    int findContentChildren(vector<int>& g, vector<int>& s) {
        // 1. 排序
        sort(g.begin(), g.end());
        sort(s.begin(), s.end());
        
        // 2. 贪心匹配
        int index = s.size() - 1;  // 从最大的开始
        int result = 0;
        
        for (int i = g.size() - 1; i >= 0; i--) {
            if (index >= 0 && s[index] >= g[i]) {
                result++;
                index--;
            }
        }
        
        return result;
    }
};

关键点

  • 排序:通常需要先排序
  • 贪心策略:优先满足大的(或小的)
  • 匹配:双指针或单指针匹配

6.2 区间问题模板

适用场景:区间重叠、合并、选择等问题。

核心思路

  • 排序:按起点或终点排序
  • 贪心选择:选择结束最早的区间
  • 判断重叠:判断当前区间是否与已选区间重叠

模板代码

cpp 复制代码
// 用最少数量的箭引爆气球模板
class Solution {
public:
    int findMinArrowShots(vector<vector<int>>& points) {
        if (points.empty()) return 0;
        
        // 1. 按结束位置排序
        sort(points.begin(), points.end(), [](const vector<int>& a, const vector<int>& b) {
            return a[1] < b[1];
        });
        
        // 2. 贪心选择
        int arrows = 1;
        int end = points[0][1];
        
        for (int i = 1; i < points.size(); i++) {
            // 如果当前区间起点大于前一个区间的结束点,需要新箭
            if (points[i][0] > end) {
                arrows++;
                end = points[i][1];
            }
        }
        
        return arrows;
    }
};

关键点

  • 排序:按结束位置排序(或按开始位置)
  • 贪心策略:选择结束最早的区间
  • 重叠判断:判断区间是否重叠

6.3 跳跃问题模板

适用场景:在数组上跳跃,判断能否到达或求最少步数。

核心思路

  • 维护最远距离:记录当前能到达的最远位置
  • 贪心策略:每次跳到能跳最远的位置
  • 更新步数:当需要跳跃时更新步数

模板代码(判断能否到达)

cpp 复制代码
// 跳跃游戏模板
class Solution {
public:
    bool canJump(vector<int>& nums) {
        int cover = 0;  // 当前能覆盖的最远距离

        for (int i = 0; i <= cover; i++) {
            cover = max(cover, i + nums[i]);

            if (cover >= nums.size() - 1) return true;
        }

        return false;
    }
};

模板代码(最少步数 - 正向)

cpp 复制代码
// 跳跃游戏II模板(正向)
class Solution {
public:
    int jump(vector<int>& nums) {
        if (nums.size() == 1) return 0;

        int curDistance = 0;  // 当前覆盖的最远距离
        int ans = 0;          // 记录走的最大步数
        int nextDistance = 0; // 下一步覆盖的最远距离

        for (int i = 0; i < nums.size(); i++) {
            nextDistance = max(nextDistance, i + nums[i]);

            if (i == curDistance) {
                ans++;
                curDistance = nextDistance;

                if (curDistance >= nums.size() - 1) {
                    break;
                }
            }
        }

        return ans;
    }
};

模板代码(最少步数 - 反向)

cpp 复制代码
// 跳跃游戏II模板(反向)
class Solution {
public:
    int jump(vector<int>& nums) {
        int position = nums.size() - 1;
        int steps = 0;
        while (position > 0) {
            for (int i = 0; i < position; i++) {
                if (i + nums[i] >= position) {
                    position = i;
                    steps++;
                    break;
                }
            }
        }
        return steps;
    }
};

关键点

  • 维护最远距离:记录当前能到达的最远位置
  • 贪心策略:每次跳到能跳最远的位置
  • 步数更新:当需要跳跃时更新步数

6.4 股票问题模板

适用场景:买卖股票,使得利润最大。

类型一:只能买卖一次(121)

核心思路

  • 贪心策略:维护最低买入价,计算每天卖出的利润
  • 更新策略:不断更新最小买入价和最大利润

模板代码

cpp 复制代码
// 买卖股票的最佳时机模板(只能买卖一次)
class Solution {
public:
    int maxProfit(vector<int>& prices) {
        if (prices.empty()) return 0;

        int minPrice = prices[0];   // 当前最小买入价
        int maxProfit = 0;          // 最大利润初始化为0

        for (int i = 1; i < prices.size(); i++) {
            // 1. 当前卖出价格 - 最小买入价 = 当前利润
            int profit = prices[i] - minPrice;

            // 2. 更新最大利润
            if (profit > maxProfit) {
                maxProfit = profit;
            }

            // 3. 更新最小买入价
            if (prices[i] < minPrice) {
                minPrice = prices[i];
            }
        }

        return maxProfit;
    }
};

类型二:可以多次买卖(122)

核心思路

  • 贪心策略:只要今天价格比昨天高就买卖
  • 累加利润:将所有正利润累加

模板代码

cpp 复制代码
// 买卖股票的最佳时机II模板(可以多次买卖)
class Solution {
public:
    int maxProfit(vector<int>& prices) {
        int result = 0;
        
        for (int i = 1; i < prices.size(); i++) {
            // 只要今天价格比昨天高,就买卖
            if (prices[i] > prices[i - 1]) {
                result += prices[i] - prices[i - 1];
            }
        }
        
        return result;
    }
};

关键点

  • 121题:只能买卖一次,维护最低买入价
  • 122题:可以多次买卖,累加所有正利润
  • 不需要考虑具体买卖时机,只考虑利润

6.5 划分字母区间模板

适用场景:划分字符串,使得同一字母最多出现在一个片段中。

核心思路

  • 记录每个字符最后出现的位置
  • 贪心策略:维护当前段的最远右边界
  • 切分条件:当遍历到边界时,可以切分

模板代码

cpp 复制代码
// 划分字母区间模板
class Solution {
public:
    vector<int> partitionLabels(string s) {
        // 1. 记录每个字符最后出现的位置
        vector<int> last(26, 0);
        for (int i = 0; i < s.size(); i++) {
            last[s[i] - 'a'] = i;
        }

        vector<int> result;
        int start = 0; // 当前段起点
        int end = 0;   // 当前段终点

        // 2. 贪心遍历
        for (int i = 0; i < s.size(); i++) {
            // 更新当前段的最远右边界
            end = max(end, last[s[i] - 'a']);

            // 3. 到达边界,可以切分
            if (i == end) {
                result.push_back(end - start + 1);
                start = i + 1; // 开启新一段
            }
        }

        return result;
    }
};

关键点

  • 记录每个字符最后出现的位置
  • 贪心策略:维护当前段的最远右边界
  • 切分条件:当遍历到边界时,可以切分

6.6 其他问题模板

摆动序列模板

cpp 复制代码
// 摆动序列模板
class Solution {
public:
    int wiggleMaxLength(vector<int>& nums) {
        if (nums.size() <= 1) return nums.size();
        
        int curDiff = 0;  // 当前一对差值
        int preDiff = 0;  // 前一对差值
        int result = 1;   // 记录峰值个数,默认序列最右边有一个峰值
        
        for (int i = 0; i < nums.size() - 1; i++) {
            curDiff = nums[i + 1] - nums[i];
            
            // 出现峰值
            if ((curDiff > 0 && preDiff <= 0) || (curDiff < 0 && preDiff >= 0)) {
                result++;
                preDiff = curDiff;
            }
        }
        
        return result;
    }
};

最大子序和模板(贪心)

cpp 复制代码
// 最大子序和模板(贪心)
class Solution {
public:
    int maxSubArray(vector<int>& nums) {
        int result = INT_MIN;
        int count = 0;
        
        for (int i = 0; i < nums.size(); i++) {
            count += nums[i];
            
            // 取区间累计的最大值(相当于不断确定最大子序终止位置)
            if (count > result) {
                result = count;
            }
            
            // 相当于重置最大子序起始位置,因为遇到负数一定是拉低总和
            if (count <= 0) {
                count = 0;
            }
        }
        
        return result;
    }
};

7. 贪心算法经典题目详解

7.1 分发饼干(455)

题目:假设你是一位很棒的家长,想要给你的孩子们一些小饼干。但是,每个孩子最多只能给一块饼干。

贪心策略

  • 局部最优:优先满足胃口大的孩子(用最大的饼干满足最大的胃口)
  • 全局最优:最多满足的孩子数量

代码

cpp 复制代码
class Solution {
public:
    int findContentChildren(vector<int>& g, vector<int>& s) {
        sort(g.begin(), g.end());
        sort(s.begin(), s.end());
        int index = s.size() - 1; // 饼干数组的下标
        int result = 0;
        for (int i = g.size() - 1; i >= 0; i--) { // 遍历胃口
            if (index >= 0 && s[index] >= g[i]) { // 遍历饼干
                result++;
                index--;
            }
        }
        return result;
    }
};

关键点

  • 排序:对胃口和饼干都排序
  • 贪心:从大到小匹配
  • 时间复杂度:O(n log n)

7.2 用最少数量的箭引爆气球(452)

题目:有一些球形气球贴在一堵用 XY 平面表示的墙上。墙可以用一条无限长的线表示。气球用一个二维数组 points 表示,其中 points[i] = [xstart, xend] 表示水平直径在 xstart 和 xend 之间的气球。

贪心策略

  • 局部最优:每次选择结束最早的区间,用一支箭射穿
  • 全局最优:用最少的箭射穿所有气球

代码

cpp 复制代码
class Solution {
public:
    int findMinArrowShots(vector<vector<int>>& points) {
        if (points.empty()) return 0;

        // 按区间的结束位置排序
        sort(points.begin(), points.end(), [](const vector<int>& a, const vector<int>& b) {
            return a[1] < b[1];
        });

        int arrows = 1;  // 至少需要一支箭
        int end = points[0][1];  // 第一个区间的结束点

        for (int i = 1; i < points.size(); i++) {
            // 如果当前区间的起点大于前一个区间的结束点,说明需要一支新的箭
            if (points[i][0] > end) {
                arrows++;
                end = points[i][1];  // 更新箭的作用区间
            }
        }

        return arrows;
    }
};

关键点

  • 排序:按结束位置排序
  • 贪心:选择结束最早的区间
  • 重叠判断:判断区间是否重叠

7.3 跳跃游戏(55)

题目:给定一个非负整数数组 nums ,你最初位于数组的 第一个下标 。数组中的每个元素代表你在该位置可以跳跃的最大长度。判断你是否能够到达最后一个下标。

贪心策略

  • 局部最优:每次跳到能跳最远的位置
  • 全局最优:能否到达最后一个位置

代码

cpp 复制代码
class Solution {
public:
    bool canJump(vector<int>& nums) {
        int cover = 0;  // 当前能覆盖的最远距离

        for (int i = 0; i <= cover; i++) {
            cover = max(cover, i + nums[i]);

            if (cover >= nums.size() - 1) return true;
        }

        return false;
    }
};

关键点

  • 维护最远距离:记录当前能到达的最远位置
  • 贪心策略:每次跳到能跳最远的位置
  • 判断条件:如果能覆盖最后一个位置,返回true

7.4 跳跃游戏II(45)

题目:给定一个非负整数数组,你最初位于数组的第一个位置。数组中的每个元素代表你在该位置可以跳跃的最大长度。你的目标是使用最少的跳跃次数到达数组的最后一个位置。

贪心策略

  • 局部最优:每次跳到能跳最远的位置
  • 全局最优:用最少的步数到达最后一个位置

方法一:正向贪心(推荐)

代码

cpp 复制代码
class Solution {
public:
    int jump(vector<int>& nums) {
        if (nums.size() == 1) return 0;

        int curDistance = 0;  // 当前覆盖的最远距离
        int ans = 0;          // 记录走的最大步数
        int nextDistance = 0; // 下一步覆盖的最远距离

        for (int i = 0; i < nums.size(); i++) {
            nextDistance = max(nextDistance, i + nums[i]);

            if (i == curDistance) {
                ans++;
                curDistance = nextDistance;

                if (curDistance >= nums.size() - 1) {
                    break;
                }
            }
        }

        return ans;
    }
};

方法二:反向贪心

代码

cpp 复制代码
class Solution {
public:
    int jump(vector<int>& nums) {
        int position = nums.size() - 1;
        int steps = 0;
        while (position > 0) {
            for (int i = 0; i < position; i++) {
                if (i + nums[i] >= position) {
                    position = i;
                    steps++;
                    break;
                }
            }
        }
        return steps;
    }
};

关键点

  • 正向方法:维护两个距离(当前覆盖距离和下一步覆盖距离),从前往后遍历
  • 反向方法:从终点往前找,找到能到达当前位置的最远起点
  • 贪心策略:每次跳到能跳最远的位置
  • 步数更新:当需要跳跃时更新步数

7.5 买卖股票的最佳时机(121)

题目:给定一个数组 prices ,它的第 i 个元素 prices[i] 表示一支给定股票第 i 天的价格。你只能选择 某一天 买入这只股票,并选择在 未来的某一个不同的日子 卖出该股票。设计一个算法来计算你所能获取的最大利润。返回你可以从这笔交易中获取的最大利润。如果你不能获取任何利润,返回 0 。

贪心策略

  • 局部最优:在最低点买入,在最高点卖出
  • 全局最优:获取最大利润

代码

cpp 复制代码
class Solution {
public:
    int maxProfit(vector<int>& prices) {
        if (prices.empty()) return 0; // 空数组特判

        int minPrice = prices[0];   // 当前最小买入价
        int maxProfit = 0;          // 最大利润初始化为0

        for (int i = 1; i < prices.size(); i++) {
            // 1. 当前卖出价格 - 最小买入价 = 当前利润
            int profit = prices[i] - minPrice;

            // 2. 更新最大利润
            if (profit > maxProfit) {
                maxProfit = profit;
            }

            // 3. 更新最小买入价
            if (prices[i] < minPrice) {
                minPrice = prices[i];
            }
        }

        return maxProfit;
    }
};

关键点

  • 贪心策略:维护最低买入价,计算每天卖出的利润
  • 更新策略:不断更新最小买入价和最大利润
  • 时间复杂度:O(n)

7.6 买卖股票的最佳时机II(122)

题目:给你一个整数数组 prices ,其中 prices[i] 表示某支股票第 i 天的价格。在每一天,你可以决定是否购买和/或出售股票。你在任何时候 最多 只能持有 一股 股票。你也可以先购买,然后在 同一天 出售。返回 你能获得的 最大 利润 。

贪心策略

  • 局部最优:只要今天价格比昨天高就买卖
  • 全局最优:累加所有正利润

代码

cpp 复制代码
class Solution {
public:
    int maxProfit(vector<int>& prices) {
        int result = 0;
        
        for (int i = 1; i < prices.size(); i++) {
            // 只要今天价格比昨天高,就买卖
            if (prices[i] > prices[i - 1]) {
                result += prices[i] - prices[i - 1];
            }
        }
        
        return result;
    }
};

关键点

  • 贪心策略:只要今天价格比昨天高就买卖
  • 累加利润:将所有正利润累加
  • 不需要考虑具体买卖时机

7.7 划分字母区间(763)

题目:给你一个字符串 s 。我们要把这个字符串划分为尽可能多的片段,同一字母最多出现在一个片段中。注意,划分结果需要满足:将所有划分结果按顺序连接,得到的仍然是 s 。返回一个表示每个字符串片段的长度的列表。

贪心策略

  • 局部最优:尽可能划分出更多的片段
  • 全局最优:划分出最多的片段

代码

cpp 复制代码
class Solution {
public:
    vector<int> partitionLabels(string s) {
        // 1. 记录每个字符最后出现的位置
        vector<int> last(26, 0);
        for (int i = 0; i < s.size(); i++) {
            last[s[i] - 'a'] = i;
        }

        vector<int> result;
        int start = 0; // 当前段起点
        int end = 0;   // 当前段终点

        // 2. 贪心遍历
        for (int i = 0; i < s.size(); i++) {
            // 更新当前段的最远右边界
            end = max(end, last[s[i] - 'a']);

            // 3. 到达边界,可以切分
            if (i == end) {
                result.push_back(end - start + 1);
                start = i + 1; // 开启新一段
            }
        }

        return result;
    }
};

关键点

  • 记录每个字符最后出现的位置
  • 贪心策略:维护当前段的最远右边界
  • 切分条件:当遍历到边界时,可以切分
  • 时间复杂度:O(n)

8. 贪心算法的时间复杂度

8.1 时间复杂度分析

操作 时间复杂度 说明
排序 O(n log n) 大多数贪心问题需要先排序
遍历 O(n) 遍历数组或集合
总体 O(n log n) 通常是排序 + 遍历

注意

  • 大多数贪心问题需要先排序,所以时间复杂度通常是O(n log n)
  • 如果不需要排序,时间复杂度可以是O(n)
  • 空间复杂度通常是O(1)或O(n)

8.2 空间复杂度分析

数据结构 空间复杂度 说明
基本变量 O(1) 只使用几个变量
排序 O(1) 原地排序
辅助数组 O(n) 需要额外的数组空间

注意

  • 大多数贪心问题的空间复杂度是O(1)
  • 如果需要排序,通常是原地排序,空间复杂度O(1)
  • 如果需要额外的数组,空间复杂度O(n)

9. 何时使用贪心算法

9.1 使用贪心算法的场景

  1. 分配问题

    • 分发饼干
    • 分发糖果
    • 柠檬水找零
  2. 区间问题

    • 用最少数量的箭引爆气球
    • 无重叠区间
    • 合并区间
    • 划分字母区间
  3. 跳跃问题

    • 跳跃游戏
    • 跳跃游戏II
  4. 股票问题

    • 买卖股票的最佳时机(只能买卖一次)
    • 买卖股票的最佳时机II(可以多次买卖)
  5. 其他问题

    • 摆动序列
    • 最大子序和
    • K次取反后最大化的数组和
    • 单调递增的数字
    • 根据身高重建队列

9.2 判断标准

当遇到以下情况时,考虑使用贪心算法

  • 问题具有贪心选择性质
  • 可以通过局部最优推导全局最优
  • 时间复杂度要求较低
  • 不需要保存历史状态

当遇到以下情况时,考虑使用动态规划

  • 问题具有最优子结构
  • 需要保存历史状态
  • 贪心策略无法得到最优解
  • 需要回溯或改变选择

9.3 贪心算法的优缺点

优点

  • 时间复杂度低:通常是O(n log n)或O(n)
  • 空间复杂度低:通常是O(1)或O(n)
  • 实现简单:代码通常比较简洁
  • 效率高:不需要考虑所有可能的解

缺点

  • 适用性有限:不是所有问题都适合贪心算法
  • 需要证明:需要证明贪心策略的正确性
  • 可能不是最优解:如果贪心策略不正确,可能得到次优解
  • 难以设计:贪心策略的设计可能比较困难

10. 贪心算法的证明方法

10.1 反证法

方法:假设贪心策略不是最优的,然后推导出矛盾。

示例

  • 分发饼干:假设不优先满足胃口大的孩子,那么可能无法满足更多孩子,与最优解矛盾

10.2 数学归纳法

方法:证明贪心策略在每一步都是最优的。

示例

  • 跳跃游戏:证明每次跳到能跳最远的位置,可以到达最远的位置

10.3 交换论证

方法:证明任何最优解都可以通过交换操作转换为贪心解。

示例

  • 区间问题:证明任何最优解都可以通过交换操作转换为按结束位置排序的解

10.4 举反例

方法:如果找不到反例,可以尝试贪心策略。

注意

  • 举反例是验证贪心策略的重要方法
  • 如果找不到反例,可以尝试贪心策略
  • 但找到反例并不意味着贪心策略一定错误,可能需要调整策略

11. 常见题型总结

11.1 分配问题类

  1. 分发饼干

    • 455.分发饼干:用饼干满足孩子,优先满足胃口大的
  2. 分发糖果

    • 135.分发糖果:给每个孩子分糖果,满足左右规则
  3. 找零问题

    • 860.柠檬水找零:找零问题,优先使用大面额

11.2 区间问题类

  1. 区间重叠

    • 452.用最少数量的箭引爆气球:用最少的箭射穿所有气球
    • 435.无重叠区间:删除最少的重叠区间
  2. 区间合并

    • 56.合并区间:合并所有重叠区间
  3. 区间划分

    • 763.划分字母区间:划分不重叠的区间

11.3 跳跃问题类

  1. 判断能否到达

    • 55.跳跃游戏:判断能否到达最后一个位置
  2. 最少步数

    • 45.跳跃游戏II:求到达最后一个位置的最少步数

11.4 股票问题类

  1. 多次买卖
    • 122.买卖股票的最佳时机II:可以多次买卖,求最大利润

11.5 其他问题类

  1. 序列问题

    • 376.摆动序列:求最长摆动子序列
    • 53.最大子序和:求最大子数组和
  2. 数字问题

    • 1005.K次取反后最大化的数组和:K次取反后最大和
    • 738.单调递增的数字:找到小于等于n的最大单调递增数字
  3. 重建问题

    • 406.根据身高重建队列:按身高和前面人数重建队列
  4. 划分问题

    • 763.划分字母区间:划分字符串,使得同一字母最多出现在一个片段中

12. 总结

贪心算法是一种重要的算法策略,通过局部最优选择来构造全局最优解。

核心要点

  1. 贪心选择性质:可以通过局部最优选择来构造全局最优解
  2. 最优子结构:问题的最优解包含子问题的最优解
  3. 无后效性:当前的选择不会影响后续选择的状态
  4. 局部最优 -> 全局最优:想清楚局部最优,想清楚全局最优,感觉局部最优是可以推出全局最优,并想不出反例,那么就试一试贪心

使用建议

  • 根据问题特性判断是否适合贪心算法
  • 设计贪心策略时,想清楚局部最优和全局最优
  • 通过举反例验证贪心策略的正确性
  • 注意边界条件处理
  • 掌握常见题型的模板

常见题型总结

  • 分配问题:分发饼干、分发糖果、柠檬水找零
  • 区间问题:用最少数量的箭引爆气球、无重叠区间、合并区间、划分字母区间
  • 跳跃问题:跳跃游戏、跳跃游戏II(正向和反向两种方法)
  • 股票问题:买卖股票的最佳时机(只能买卖一次)、买卖股票的最佳时机II(可以多次买卖)
  • 其他问题:摆动序列、最大子序和、K次取反后最大化的数组和、单调递增的数字、根据身高重建队列

学习路径

  1. 理解贪心算法的基本思想
  2. 从简单的分配问题开始
  3. 学习区间问题(重叠、合并等)
  4. 学习跳跃问题和股票问题
  5. 注意贪心策略的证明
  6. 掌握常见题型的模板
相关推荐
爱喝热水的呀哈喽2 小时前
子模代数。
算法·编辑器
Trouvaille ~2 小时前
【C++篇】C++11新特性详解(三):高级特性与实用工具
开发语言·c++·stl·lambda·完美转发·包装器·可变参数模版
qq_430855882 小时前
线代第三章向量第三节:向量间的线性关系二
人工智能·算法·机器学习
AC赳赳老秦2 小时前
CSV大文件处理全流程:数据清洗、去重与格式标准化深度实践
大数据·开发语言·人工智能·python·算法·机器学习·deepseek
C语言小火车2 小时前
【C++】从零开始构建C++停车场管理系统:技术详解与实战指南
开发语言·c++·毕业设计·课程设计
AndrewHZ2 小时前
【图像处理基石】光线追踪(Ray Tracing)算法入门
图像处理·人工智能·算法·计算机视觉·计算机图形学·光线追踪·渲染技术
.简.简.单.单.2 小时前
Design Patterns In Modern C++ 中文版翻译 第九章 装饰器
开发语言·c++·设计模式
橘颂TA2 小时前
【剑斩OFFER】算法的暴力美学——两数相加
c++·算法·结构与算法
youngee112 小时前
hot100-54在排序数组中查找元素的第一个和最后一个位置
数据结构·算法·leetcode