• 算法基础:贪心策略


    贪心策略

    目录

    贪心策略

    概念

    思路

    算法考题


    概念

    贪心策略的百度解释是:

    在对问题求解时,总是做出在当前看来是最好的选择。也就是说,不从整体最优上加以考虑,算法得到的是在某种意义上的局部最优解

    贪心算法不是对所有问题都能得到整体最优解,关键是贪心策略的选择 。

    基本上所有的贪心算法都可以用回溯来解决(全排列),就是说如果真的想不到好的贪心策略去实现,全排列也可以解决这个算法问题,只是时间复杂度会非常高(>O(n!) &&

    所以说贪心是一种优化算法,是在已经能解决问题的基础上,对题目本身的底层逻辑进行分析,之后找到优化策略去优化算法的时间复杂度。

    思路

    贪心算法其实并没有严格意义上的解题思路

    如何证明贪心策略是正确的呢?任何贪心策略的提出都是先提出再证明。先使用全排列的方法获取正确答案,再和贪心策略获得的结果进行比较,如果不等说明贪心策略出现错误

    算法考题

    字典序最小

    题目

    给定一个由字符串组成的数组strs,必须把所有的字符串拼接起来,返回所有拼接结果中,字典序最小的结果。

    示例 1:

    • 输入: [bb,aa,lintcode,c]
    • 输出: aabbclintcode
    • 说明: 在字典序中,"aa" < "bb" < "c" < "lintcode"

    思路

    贪心策略:

    • 先对字符串数组进行排序:对于任意两个字符串A和B,如果A与B的拼接结果小于B与A的拼接结果,则A排在前面,否则B排在前面
    • 再将排序后的数组按照顺序挨个拼接起来

    代码

    class Solution {
    
    public:
        std::string lowestString(std::vector strs){
            if(strs.empty()){
                return "";
            }
            std::sort(strs.begin(), strs.end(), [](std::string &l , std::string & r){
                return (l + r) < (r + l);
            });
    
            std::string ans ;
            for (auto & str : strs) {
                ans += str;
            }
            return ans;
        }
    };

    类似题目

    零钱问题

    题目

    lintcode:1546. 零钱问题

    小明是一个销售员,客人在他的地方买了东西,付给了小明一定面值的钱之后,小明需要把多余的钱退给客人。客人付给了小明n,小明的东西的售价为m,小明能退回给客人的面额只能为[100,50,20,10,5,2,1]的组合。现在小明想要使找出去纸币张数最小,请返回这个最小值。

    示例 1:

    • 输入: n=100, m=29
    • 输出: 3
    • 解释: 100-29=71 小明找回1张50,一张20,一张1。 所以答案为3。

    思路

    策略:

    • 从最大的钱开始找,能找大面额的尽量找大面额的,否则尝试小面额的,以此类推,最后剩下的用1元来补齐
    • 在贡献纸币数目的情况下,我们希望多贡献点金额,这样就可以让纸币数更少。这里用到了贪心的思想

    代码

    class Solution {
    private:
        std::vector comb  {100, 50, 20, 10, 5, 2, 1};
    public:
         int coinProblem(int n, int m) {
            int mon = n - m;  // 还需要找多少钱
            int res = 0;    // 一共找出去的钱的张数
            for (int i : comb){
                res += mon / i;  // 当前面额最大可以找多少钱
                mon = mon % i;  // 还剩下多少钱需要找
            }
            return res;
        }
    };

    类似题目

    股票问题(最多持有一支,可以买卖无限次)

    题目

    leetcode:122. 买卖股票的最佳时机 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 。

    提示:

    • 1 <= prices.length <= 3 * 10^4
    • 0 <= prices[i] <= 10^4

    解析

    分析:

    • 可以买卖任意多次,如果要得到最大利润,那么就要只要有正利润那么就一定要得到。
    • 即尽可能的多挣

    思路:

    • 如果今天的价格比昨天的高,就昨天买、今天卖

    代码

    class Solution {
    public:
        int maxProfit(vector& prices) {
            int res = 0;
            for (int i = 1; i < prices.size(); ++i) {
                if(prices[i] - prices[i - 1] > 0){
                    res += prices[i] - prices[i - 1];
                }
            }
            return res;
        }
    };

    小船过河

    题目

    leetcode:881. 救生艇

    给定数组 people 。people[i]表示第 i 个人的体重 ,船的数量不限,每艘船可以承载的最大重量为 limit。

    每艘船最多可同时载两人,但条件是这些人的重量之和最多为 limit。

    返回 承载所有人所需的最小船数 。

     示例 1:

    • 输入:people = [1,2], limit = 3
    • 输出:1
    • 解释:1 艘船载 (1, 2)

    示例 2:

    • 输入:people = [3,2,2,1], limit = 3
    • 输出:3
    • 解释:3 艘船分别载 (1, 2), (2) 和 (3)

    示例 3:

    • 输入:people = [3,5,3,4], limit = 5
    • 输出:4
    • 解释:4 艘船分别载 (3), (3), (4), (5)

    思路

    典型的 N:M类型问题,类似的问题还有,N个进程每一类型用时不同,M个相同CPU,求最少时间,本质上也是求最小资源使用。类似的可以参考后面题目<项目调度器>

    分析:

    • 一条船一次可以乘坐1个人、2个人(乘坐0个人没有意义);而且每次乘船的人的体重加起来不能超过船的载重
    • 要求求出最小船数,那我们应该更多的人组成2人组,也就是说这里用到了贪心思想:在满足相同船只的情况下,消耗了更多的人。
    • 应该让哪些人组成2人组呢?最轻的和最重的,当然,前提是不要超过承重

    解题步骤:

    • 先遍历一遍数组,如果有一个人的体重超过了limit,那么直接返回无穷大,表示多少条船都搞不定
    • 否则:
      • 先对people排序
      • 然后用双指针从两边开始向中间遍历,让重的人和轻的人组成2人组,如果当前最重的人和最轻的人的重量超过了承载,那么就让重的人先乘坐一条船(因为这个人一定要过河,但是已经没有人可以和他组成2个组了,所以他单独坐船)
      • 之后轻的人再继续找下一个重的人配对,直到最重的人和最轻的人可以满足再limit之内
      • 数组遍历结束,算法结束
    • PS:这里可以用一些优化的操作:在轻指针指向元素的值超过limit/2,那么就可以认为,两个指针之间的所有值进行pair都是会超过limit 的,因此可以直接将 指针相减得到距离,加到使用的船只上去。

    代码

    class Solution {
    public:
        int numRescueBoats(vector& people, int limit) {
            int N = people.size();
            std::sort(people.begin(), people.end());
            if(people[N - 1] > limit){
                return -1;
            }
            
            int ans = 0, l = 0, r = N - 1, sum = 0;
            while (l <= r){
                // 当 people [l] > limit/2 时,ans+= (r-l == 0 ? 1:r-l)
                if (people [l] > limit/2.) {
                    ans+= (r-l == 0 ? 1:r-l);
                    break;
                }
                // 当人数总数为奇数时处理边界条件
                sum = l == r ? people[l] : people[l] + people[r];
                if(sum > limit){
                    r--;
                }else{
                    l++;
                    r--;
                }
                ans++;
            }
            return ans;
        }
    };
    

    任务调度器

    题目

    leetcode:621. 任务调度器

    给你一个用字符数组 tasks 表示的 CPU 需要执行的任务列表。其中每个字母表示一种不同种类的任务。任务可以以任意顺序执行,并且每个任务都可以在 1 个单位时间内执行完。在任何一个单位时间,CPU 可以完成一个任务,或者处于待命状态。

    然而,两个 相同种类 的任务之间必须有长度为整数 n 的冷却时间,因此至少有连续 n 个单位时间内 CPU 在执行不同的任务,或者在待命状态。

    你需要计算完成所有任务所需要的 最短时间

    示例 1:

    输入:tasks = ["A","A","A","B","B","B"], n = 2

    输出:8

    解释:A -> B -> (待命) -> A -> B -> (待命) -> A -> B

    在本示例中,两个相同类型任务之间必须间隔长度为 n = 2 的冷却时间,而执行一个任务只需要一个单位时间,所以中间出现了(待命)状态。

    示例 2:

    • 输入:tasks = ["A","A","A","B","B","B"], n = 0
    • 输出:6
    • 解释:在这种情况下,任何大小为 6 的排列都可以满足要求,因为 n = 0

    ["A","A","A","B","B","B"]

    ["A","B","A","B","A","B"]

    ["B","B","B","A","A","A"]

    ...

    诸如此类

    示例 3:

    • 输入:tasks = ["A","A","A","A","A","A","B","C","D","E","F","G"], n = 2
    • 输出:16
    • 解释:一种可能的解决方案是:

    A -> B -> C -> A -> D -> E -> A -> F -> G -> A -> (待命) -> (待命) -> A -> (待命) -> (待命) -> A

    提示:

    • 1 <= task.length <= 104
    • tasks[i] 是大写英文字母
    • n 的取值范围为 [0, 100]

    思路

    分析:

    • 本题要求我们求执行完所有任务的最短时间。因为每种任务的执行所需时间和冷却时间都是一样的,所以最小耗时时间取决于最多的那个任务数量,
    • 本质上是用贪心思想: 尽可能排执行个数最多的任务A,然后在任务A的冷却时间内插入当前数量最多的任务

    可以分为两种情况:

    • 实例一(有冷却时间):AAABBBCC n=3,那么:
      • 第一步:A _ _ _ A _ _ _A
        • 假设A出现的次数为x,那么先不管最后的A
        • 我们把 A _ _ _ 看成一个整体,即有(x - 1)个整体
        • 每个A _ _ _的长度 = “n个_ + 1个A",即长度为n+1
        • 因此,除了最后的A,至少需要时间min_time = (x - 1) * (n + 1)
      • 第二步:插入其他任务
        • 放入B,B的出现次数和A相等,有A B _ _A B _ _A B
        • 放入C,C的出现次数少于A,有A B C _A B C _A B
        • 从上面可以看出:
          • 最后的AB只跟出现最多的次数的任务种类相关
      • 综上,假设出现次数为X的任务种类数为X,需要的时间total = (x - 1) * (n + 1) + count
    • 实例二(无冷却时间):AAABBBCCDD,n=2
      • 第一步:安排A,A _ _A _ _A
      • 第二步:
        • 放入B,A B _A B _A B
        • 放入C,A B CA B _ A B
        • 放入D,A B CA B D A B
        • 放入C,A B CA B D A B C
        • 放入D,A B CA B D A B C D
      • 综上:
        • 结果为10,刚好是数组的长度。
        • 如果按照实例一的方法来算,结果是8。
    • 问题:怎么区分有冷却时间还是没有冷却时间呢?
      • 先按照有冷却时间的公式算 :total = (x - 1) * (n + 1) + count
      • 然后比较 total 和数组长度:
        • 如果total <= 数组长度,说明所有的冷却时间都已经用完了,但是还有任务没有排好。即无冷却时间
        • 如果total > 数组长度,说明CPU出现了空闲

    步骤:

    • 计算每个任务出现的次数
    • 找出出现次数最多的任务,假设出现次数为 x
    • 计算至少需要的时间 (x - 1) * (n + 1),记为 min_time
    • 计算出现次数为 x 的任务总数 count,计算最终结果total = min_time + count
    • 返回 total和数组长度中比较大的那一个

    PS: 上述过程简化的公式可以为:

      • 若CdNum == 0 ,时间总数 = AllNum
      • 若TaskANum = =TaskBNum = =TaskXNum = = maxTaskNum
        • CdNum -= (MaxNumTaskCount-1)
          • 若CdNum <= 0 ,时间总数 = AllNum
      • 若OtherTaskNum > maxTaskNum * CdNum, 时间总数 = maxTaskNum +OtherTaskNum
      • 若OtherTaskNum < maxTaskNum * CdNum,且CdNum !=0 时间总数 = OtherTaskNum/CdNum >= maxTaskNum-1 ?AllNum: (maxTaskNum-1)*MaxNumTaskCount*CdNum + MaxNumTaskCount*maxTaskNum

    代码

    class Solution{
        static bool cmp(int &a, int &b){
            return a > b;
        }
    public:
        int leastInterval(std::vector &tasks, int n){
            if(!n){
                return tasks.size();
            }
            std::vector nums(26, 0);
            for(char tast : tasks){
                nums[tast - 'A'] += 1;
            }
            std::sort(nums.begin(), nums.end(), cmp);
            int total = (n - 1) * (nums[0] - 1) + 1;
            for (int i = 1; i < nums.max_size(); ++i) {
                if(nums[i] == nums[0]){
                    total++;
                }
            }
            return total > tasks.size() ? total : tasks.size();
        }
    };
    

    摆动序列

    题目

    leetcode:376.摆动序列

    如果连续数字之间的差严格地在正数和负数之间交替,则数字序列称为 摆动序列 。第一个差(如果存在的话)可能是正数或负数。仅有一个元素或者含两个不等元素的序列也视作摆动序列。

    • 例如, [1, 7, 4, 9, 2, 5] 是一个 摆动序列 ,因为差值 (6, -3, 5, -7, 3) 是正负交替出现的。
    • 相反,[1, 4, 7, 2, 5] 和 [1, 7, 4, 5, 5] 不是摆动序列,第一个序列是因为它的前两个差值都是正数,第二个序列是因为它的最后一个差值为零。

    子序列 可以通过从原始序列中删除一些(也可以不删除)元素来获得,剩下的元素保持其原始顺序。

    给你一个整数数组 nums ,返回 nums 中作为 摆动序列最长子序列的长度 。

    思路

    PS: 这个题目改个名字就很简单了:判断一个数组波峰波谷的和

    分析:

    • 从原始序列中删除一些元素,最终能够形成的摆动序列最长长度。那么,应该删除哪些元素呢?
    • 贪心策略:只选波峰 波谷,不在上升下降过程中选点
    • 当然,我们没有必要真的去删,数波峰波谷即可

    代码

    class Solution {
    public:
        int wiggleMaxLength(vector& 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) || (preDiff >= 0 && curDiff < 0)) {
                    result++;
                    preDiff = curDiff;
                }
            }
            return result;
        }
    };
    
    
    class Solution {
        public int wiggleMaxLength(int[] nums) {
            int n = nums.length;
            if (n < 2) {
                return n;
            }
            // 因为有边界节点(起点、终点)因此最少一个长度
            int up = 1;
            int down = 1;
            for (int i = 1; i < n; i++) {
                if (nums[i] > nums[i - 1]) {
                    up = down + 1;
                }
                if (nums[i] < nums[i - 1]) {
                    down = up + 1;
                }
            }
            return Math.max(up, down);
        }
    }

     

  • 相关阅读:
    企业浏览器安全管理
    LCOV 工具来统计 Google Test 的代码覆盖率
    Java 的反射
    SIMetrix导入MOS管参数的另一种方法
    CSS 什么是伪类?什么是伪元素?
    一位工作七年的Java工程师给毕业生的经验分享
    linux中开始mysql中binlog日志
    HC32L110(五) Ubuntu20.04 VSCode的Debug环境配置
    java基于springboot+vue的园区入驻停车管理系统
    涂鸦智能携手亚马逊云科技 共建“联合安全实验室” 为IoT发展护航
  • 原文地址:https://blog.csdn.net/qq_32378713/article/details/127462762