当前位置:   article > 正文

算法基础:贪心策略

贪心策略

贪心策略

目录

贪心策略

概念

思路

算法考题


概念

贪心策略的百度解释是:

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

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

基本上所有的贪心算法都可以用回溯来解决(全排列),就是说如果真的想不到好的贪心策略去实现,全排列也可以解决这个算法问题,只是时间复杂度会非常高(>O(n!) && <O(n∗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<std::string> 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<int> 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<int>& 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<int>& 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<char> &tasks, int n){
        if(!n){
            return tasks.size();
        }
        std::vector<int> 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<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) || (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);
    }
}

 

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/小桥流水78/article/detail/830154
推荐阅读
相关标签
  

闽ICP备14008679号