贪心策略
目录
概念
贪心策略的百度解释是:
在对问题求解时,总是做出在当前看来是最好的选择。也就是说,不从整体最优上加以考虑,算法得到的是在某种意义上的局部最优解 。
贪心算法不是对所有问题都能得到整体最优解,关键是贪心策略的选择 。
基本上所有的贪心算法都可以用回溯来解决(全排列),就是说如果真的想不到好的贪心策略去实现,全排列也可以解决这个算法问题,只是时间复杂度会非常高(>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; } };
类似题目
零钱问题
题目
小明是一个销售员,客人在他的地方买了东西,付给了小明一定面值的钱之后,小明需要把多余的钱退给客人。客人付给了小明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; } };
类似题目
股票问题(最多持有一支,可以买卖无限次)
题目
给你一个整数数组 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; } };
小船过河
题目
给定数组 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; } };
任务调度器
题目
给你一个用字符数组 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(); } };
摆动序列
题目
如果连续数字之间的差严格地在正数和负数之间交替,则数字序列称为 摆动序列 。第一个差(如果存在的话)可能是正数或负数。仅有一个元素或者含两个不等元素的序列也视作摆动序列。
- 例如, [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); } }