数据结构与算法(六) 贪心算法

简介: 数据结构与算法(六) 贪心算法

前言


这篇文章来讲贪心算法(Greedy Algorithm),这是一种特殊的动态规划算法


正文


1、本质


我们在之前的文章中讲过,动态规划可以解决一类具有最优子结构和重叠子问题特征的问题


贪心算法本质上是一种特殊的动态规划算法,因此在看这篇文章前,建议先看之前动态规划的文章



回到正题,先来简单回顾下动态规划,动态规划的核心就是状态转移方程


无论是自上而下利用递归的解题思路,还是自下而上利用递推的解题思路,都可以用状态转移方程串联起来


状态转移方程的关键就是怎么根据较小规模的问题推导出较大规模的问题


在每一步推导的过程中,其实就是在做一件事情,即在当前状态下,尝试所有选择,到达新的状态


然后不断重复这个过程,从已知状态开始直至待求状态结束



跳跃游戏 II | leetcode45

给定一个非负整数数组,初始位于数组的第一个下标,数组中的元素代表所在位置可以跳跃的最大长度

假设一定可以到达数组最后一个位置,问最少需要跳多少次才能到达


为了更好地理解这一过程,这里以跳跃游戏为例,假设给定数组 [2, 3, 1, 1, 4],画出递归树如下:

vv.png

注意到这个问题是满足最优子结构和重叠子问题的,因此必定可以用动态规划来解

这里以自上而下的解题思路为例给出代码

class Solution {
public:
    vector<int> memo;
    // 动态规划
    int dp(vector<int>& nums, int pos) {
        int n = nums.size();
        // 边界条件
        if (pos >= n - 1) {
            return 0;
        }
        // 查备忘录
        if (memo[pos] != n) {
            return memo[pos];
        }
        // 尝试所有选择,优化点
        // 写备忘录
        for (int i = 1; i <= nums[pos]; i++) {
            memo[pos] = min( memo[pos], dp(nums, pos + i) + 1 );
        }
        // 返回
        return memo[pos];
    }
    int jump(vector<int>& nums) {
        int n = nums.size();
        memo = vector<int>(n, n); // 初始化备忘录
        return dp(nums, 0);
    }
};


动态规划在每一步的推导过程中,都要尝试所有选择,这是因为在某些问题中无法判断哪个选择是最优选择


但是贪心算法无需尝试所有选择,而是可以通过推导得出当前最优解,并且局部最优解可以到达全局最优解


假设现在站在索引 0 的位置上,其可跳长度为 2,那么可能的选择有:跳到索引 1 的位置上、跳到索引 2 的位置上。动态规划的思路是,我不知道这两种做法哪种好,所以我就两种都试一下。而贪心算法建立在更强的约束上,在面临多个选择时,能判断出当前最优解是什么。回到上述例子,索引 1 位置的可跳长度是 3,如果当前选择跳到索引 1,那么下一步最远可以跳到索引 4;同理,索引 2 位置的可跳长度是 1,如果当前选择跳到索引 2,那么下一步最远可以跳到索引 3。明显当前跳到索引 1 是优于跳到索引 2 的,因为下一步可跳位置的选择更多,因此贪心算法就会直接选择跳到索引 1,而不会再去尝试跳到索引 2


class Solution {
public:
    // 无需递归
    int jump(vector<int>& nums) {
        int n = nums.size();
        int pos = 0; // 当前位置
        int ans = 0; // 当前步数
        // 进主流程
        while (pos < n - 1) {
            int nxtPos = 0; // 下一位置
            int maxPos = 0; // 最远位置
            // 直接判断最优位置
            // 无需尝试所有选择
            for (int i = 0; i <= nums[pos]; i++) {
                if (pos + i >= n - 1) {
                    nxtPos = pos + i;
                    break;
                }
                if (maxPos < pos + i + nums[pos + i]) {
                    maxPos = pos + i + nums[pos + i];
                    nxtPos = pos + i;
                }
            }
            pos = nxtPos;
            ans = ans + 1;
        }
        // 返回
        return ans;
    }
};


2、核心


贪心算法是一种特殊的动态规划算法,它的约束更强,适用场景更少,但一般来说具有更高的效率


这是因为动态规划需要尝试所有选择,而贪心算法则可以通过推导直接选择当前最优解



贪心算法所解决的问题除了要满足最优子结构外,更重要的是要满足贪心选择性质

所谓的贪心选择性质就是说每次选择仅依赖于之前的选择,而不依赖于后续的选择


理解这个性质很关键,这是贪心算法的核心


在每一步推导过程中,贪心算法在当前状态下做局部最优解以到达下一状态,直至到达待求的问题


对于一个具体的问题,使用贪心算法前必须要证明每一步推导所做的局部最优解能到达全局最优解



凑零钱问题 | leetcode322

给定一个整数数组 coins 表示不同面额的硬币,以及一个整数 amount 表示总金额

假设每种硬币的数量是无限的,问凑成总金额所需的最少的硬币个数,若无法凑成,则返回 -1


注意,如果盲目使用贪心算法,可能取不到全局最优解,这里以凑零钱问题为例说明这种情况


对于上述问题,使用贪心算法的一个直观思路是:每次选择面额最大的硬币,直至凑满总金额


这对于普通的面额分布确实是可行的,例如 coins = [1, 5, 10], amount = 13


但对于特殊的面额分布就会出现问题,例如 coins = [1, 4, 5 ], amount = 13


如果使用贪心算法,那么选择的硬币依次是 5、5、1、1、1,但最优选择方式却是 5、4、4


3、框架


贪心算法的框架很简单,总结起来就是一句话:在推导过程中,每次都选择当前最优解

只不过在使用贪心算法时需要特别注意,局部最优解必须能到达全局最优解


4、例题


(1)无重叠区间 | leetcode435


给定一个区间集合,返回移除区间的最小数量,使得剩余的区间互不重叠


基本思路

题目要求移除最少数量的区间使得剩余的区间互不重叠

可转换为选择最多数量的区间使得选择的区间互不重叠

根据贪心算法的思路,每次选择右端点最小且不与先前区间重叠的区间即可

这是因为同样是增加一个区间,如果选择右端点较小的区间,那么以后选择的可能性更多

class Solution {
public:
    int eraseOverlapIntervals(vector<vector<int>>& intervals) {
        // 特判
        int n = intervals.size();
        if (n <= 0) {
            return 0;
        }
        // 预处理,为了提高效率
        // 按右端点从小到大排序
        sort(intervals.begin(), intervals.end(), [](const auto& u, const auto& v) {
            return u[1] < v[1];
        });
        // 每次选择右端点最小且不与先前区间重叠的区间
        int ans = 1;
        int end = intervals[0][1];
        for (int i = 1; i < n; i++) {
            // 因为数组已提前排序,所以按顺序遍历就能选择右端点最小的区间
            // 然后只需保证当前区间与先前区间无重叠,也即当前区间的左端点大于等于先前区间的右端点
            if (intervals[i][0] >= end) {
                ans = ans + 1;
                end = intervals[i][1];
            }
        }
        // 返回结果
        return n - ans;
    }
};


(2)单调递增的数字 | leetcode738


给定一个整数,返回小于等于这个数字的最大数字,使得返回的数字是单调递增的


基本思路


从折线图的角度来看,从左往后找第一个谷顶,将这个数字减去一,将后面数字改为九

如:13542 -> 13499


若第一个谷顶是平的,则找谷顶最左边的位置,将这个数字减去一,将后面数字改为九

如:15542 -> 14999


class Solution {
public:
    int monotoneIncreasingDigits(int n) {
        if (n < 10) {
            return n;
        }
        string s = std::to_string(n);
        int m = s.size();
        int i = 0;
        while (i < m - 1 && s[i] <= s[i + 1]) {
            i++;
        }
        if (i == m - 1) {
            return n;
        }
        while (i > 0 && s[i] == s[i - 1]) {
            i--;
        }
        s[i] = s[i] - 1;
        i++;
        while (i < m) {
            s[i] = '9';
            i++;
        }
        return stoi(s);
    }
};

目录
相关文章
|
1月前
|
存储 人工智能 算法
数据结构与算法细节篇之最短路径问题:Dijkstra和Floyd算法详细描述,java语言实现。
这篇文章详细介绍了Dijkstra和Floyd算法,这两种算法分别用于解决单源和多源最短路径问题,并且提供了Java语言的实现代码。
70 3
数据结构与算法细节篇之最短路径问题:Dijkstra和Floyd算法详细描述,java语言实现。
|
13天前
|
算法 Python
在Python编程中,分治法、贪心算法和动态规划是三种重要的算法。分治法通过将大问题分解为小问题,递归解决后合并结果
在Python编程中,分治法、贪心算法和动态规划是三种重要的算法。分治法通过将大问题分解为小问题,递归解决后合并结果;贪心算法在每一步选择局部最优解,追求全局最优;动态规划通过保存子问题的解,避免重复计算,确保全局最优。这三种算法各具特色,适用于不同类型的问题,合理选择能显著提升编程效率。
30 2
|
1月前
|
机器学习/深度学习 存储 缓存
数据结构与算法学习十:排序算法介绍、时间频度、时间复杂度、常用时间复杂度介绍
文章主要介绍了排序算法的分类、时间复杂度的概念和计算方法,以及常见的时间复杂度级别,并简单提及了空间复杂度。
26 1
数据结构与算法学习十:排序算法介绍、时间频度、时间复杂度、常用时间复杂度介绍
|
1月前
|
存储 算法 Java
Set接口及其主要实现类(如HashSet、TreeSet)如何通过特定数据结构和算法确保元素唯一性
Java Set因其“无重复”特性在集合框架中独树一帜。本文解析了Set接口及其主要实现类(如HashSet、TreeSet)如何通过特定数据结构和算法确保元素唯一性,并提供了最佳实践建议,包括选择合适的Set实现类和正确实现自定义对象的hashCode()与equals()方法。
33 4
|
1月前
|
搜索推荐 算法
数据结构与算法学习十四:常用排序算法总结和对比
关于常用排序算法的总结和对比,包括稳定性、内排序、外排序、时间复杂度和空间复杂度等术语的解释。
20 0
数据结构与算法学习十四:常用排序算法总结和对比
|
1月前
|
存储 缓存 分布式计算
数据结构与算法学习一:学习前的准备,数据结构的分类,数据结构与算法的关系,实际编程中遇到的问题,几个经典算法问题
这篇文章是关于数据结构与算法的学习指南,涵盖了数据结构的分类、数据结构与算法的关系、实际编程中遇到的问题以及几个经典的算法面试题。
31 0
数据结构与算法学习一:学习前的准备,数据结构的分类,数据结构与算法的关系,实际编程中遇到的问题,几个经典算法问题
|
1月前
|
机器学习/深度学习 存储 算法
【数据结构与算法基础】——算法复杂度
【数据结构与算法基础】——算法复杂度
|
1月前
|
算法 Java C++
【贪心算法】算法训练 ALGO-1003 礼物(C/C++)
【贪心算法】算法训练 ALGO-1003 礼物(C/C++)
【贪心算法】算法训练 ALGO-1003 礼物(C/C++)
|
1月前
|
机器学习/深度学习 存储 算法
【初阶数据结构】算法效率大揭秘 | 时间与空间复杂度的深度剖析
【初阶数据结构】算法效率大揭秘 | 时间与空间复杂度的深度剖析
|
1月前
|
机器学习/深度学习 搜索推荐 算法
探索数据结构:初入算法之经典排序算法
探索数据结构:初入算法之经典排序算法

热门文章

最新文章

下一篇
无影云桌面