贪心法:遵循某种规律,不断贪心的选取当前最优策略的算法设计方法。
例1:钞票支付问题
有1元、2元、5元、10元、20元、50元、100元的钞票无穷多张。现使用这些钞票支付x元,最少需要多少张?
例如,X=628最佳支付方法:
6张100块的,1张20块的,1张5块的,1张2块的,1张1块的;
共需要6+1+1+1+1=10张。
直觉告诉我们:尽可能多的使用面值较大的钞票!
为何这么做一定是对的?
面额为1元、2元、5元、10元、20元、50元、100元,每个面额是比自己小的面额的2倍或以上。
所以当使用一张较大面额钞票时,若用较小面额钞票替换,需要两张或更多的钞票!
例如:
2=1+1
5=2+2+1
10=5+5
20=10+10
50=20+20+10
100=50+50
故,当前最优解即为全局最优解,贪心成立!
思考:如果增加7元面额,贪心还成立吗?条件就不成立了,因为一味的找当前最优解可能导致问题无解或产生错误解
示例代码:
void pay() {
const int RMB[] = { 100,50,20,10,5,2,1 };
const int NUM = 7;
int x = 628;
int count = 0;
for (int i = 0; i < NUM; ++i) {
int use = x / RMB[i];
count +=use;
x = x - RMB[i] * use;
cout << "需要面额为" << RMB[i] << "的" << use << "张" << endl;
cout << "剩余需要支付RMB" << x << endl;
}
cout << "总共需要" << count << "张RMB" << endl;
}
例2:分糖果
已知一些孩子和一些糖果,每个孩子有需求因子g,每个糖果有大小s,当某个糖果的大小s>=某个孩子的需求因子g时,代表该糖果可以满足该孩子;求使用这些糖果,最多能满足多少孩子?(注意,某个孩子最多只能用1个糖果满足)
例如,需求因子数组g=【5,10,2,9,15,9】;糖果大小数组s=【6,1,20,3,8】;最多可以满足3个孩子。(注意:某个孩子最多只能用1个糖果进行满足)
分析:
需求因子数组g=【2,5,9,9,10,15】;糖果大小数组s=【1,3,6,8,20】。
核心目标:让更多孩子得到满足,有如下规律:
1.某个糖果如果不能满足某个孩子,则该糖果也一定不能满足需求因子更大的孩子。
如,糖果1(s=1)不能满足孩子1(g=2),则不能满足孩子2、孩子3、…、孩子7;
糖果2(s=3)不能满足孩子2(g=5),则不能满足孩子3、孩子4、.、孩子7;
2.某个孩子可以用更小的糖果满足,则没必要用更大糖果满足,因为可以保留更大的糖果满足需求因子更大的孩子。(贪心!)如,孩子1(g=2),可以被糖果2(s=3)满足,则没必要用糖果3、糖果4、糖果5满足;
孩子2(g=5),可以被糖果3(s=6)满足,则没必要用糖果4、糖果5满足;
3.孩子的需求因子更小则其更容易被满足,故优先从需求因子小的孩子尝试,用某个糖果满足一个较大需求因子的孩子或满足一个较小需求因子的孩子效果是一样的(最终满足的总量不变)。(贪心!)
最终:
使用糖果2(s=3)满足孩子1(g=2);
使用糖果3(s=6)满足孩子2(g=5);
使用糖果5(s=20)满足孩子3(g=3);
child变量即为最后结果!糖果1与糖果4没有被使用到。
示例代码:
class Solution {
public:
int FindContentChildren(vector<int>&g, vector<int>&s) {
sort(g.begin(), g.end());
sort(s.begin(), s.end());//对两组数组排序;
int child = 0;//child代表已经满足的几个孩子
int cookie = 0;//cookie代表尝试了几个糖果
while ((child<g.size())&&(cookie<s.size()))//当糖果盒孩子同时均未尝试完时
{
if (g[child]<=s[cookie])//当孩子的满足因子小于或等于糖果的时候
{
child++;//代表该糖果满足了孩子,孩子数组指针向后移动
}
cookie++;//无论失败还是成功,每个糖果只尝试一次,糖果数组的指针向后移动
}
return child;//最终child就是得到满足的孩子的数目
}
};
int main() {
Solution solve;
vector<int >g;
vector<int>s;
g = { 5,10,2,9,15,9 };//孩子的满足因子数组
s = { 6,1,20,3,8 };//糖果的大小数组
printf("%d\n", solve.FindContentChildren(g, s));//输出为3
return 0;
}
例3:摇摆序列
一个整数序列,如果两个相邻元素的差恰好正负(负正) 交替出现 ,则该序列被称为摇摆序列 。一个小于2个元素的序列 直接 为摇摆序列。
例如:
序列 [1, 7, 4, 9, 2, 5],相邻元素的差 (6, -3, 5, -7, 3),该序列为摇摆序列。
序列 [1,4,7,2,5] (3, 3, -5, 3)、 [1,7,4,5,5] (6, -3, 1, 0)不是摇摆序列。
给一个随机序列,求这个序列 满足摇摆序列 定义的 最长子序列 的长度。(可以不连续)
例如:
输入[1,7,4,9,2,5],结果为6;输入[1,17,5,10,13,15,10,5,16,8],结果为7([1,17,10,13,10,16,8]
);输入[1,2,3,4,5,6,7,8,9],结果为2。
分析:
[1, 17, 5, 10, 13, 15, 10, 5, 16, 8], 整体 不是摇摆序列:
观察该序列 前 前6位 位 :[1, 17, 5, 10, 13, 15 …] ; 橙色 部分为上升段:
其中它有3个子序列是摇摆序列:
[1, 17, 5, 10, …]
[1, 17, 5, 13, …]
[1, 17, 5, 15, …]
在不清楚原始序列的7位是什么的情况下, 只看前6位 位 ,摇摆子序列的 第四位 从
10, 13, 15 中 选择1个 个 数。
我们应该选择 哪个最好 呢?
摇摆子序列:[1,17,5,10,…]、[1,17,5,13,…]、[1,17,5,15,…]
那么我们有一个目标即是:成为摇摆子序列的下一个元素的概率跟大,就更加有可能使摇摆子序列长度++
根据这一目标,选择最大的15(贪心)更加有可能使原始数组的第七位成为摇摆序列的下一个元素。
可以的出一下规律:当序列有一段 连续的递增( 或递减) 时,为形成 摇摆子序列 ,我们只需要保留 这段连续的递增(或递减)的 首尾元素 ,这样 更可能 使得尾部的后一个元素成为摇摆子序列的下一个元素。
示例代码:
class Solution {
public:
int wiggleMaxLnegth(vector<int>&nums) {
if (nums.size() < 2) {//根据定义,任何序列个数小于2的数组都是摇摆序列
return nums.size();
}
const int BEGIN = 0;
const int UP = 1;//扫描序列时的三种状态
const int DOWN = 2;
int STATE = BEGIN;//初始化为begin
int max_Lneght = 1;//子序列的最小长度为1
for (int i=1; i < nums.size(); ++i)//遍历原始序列
{
switch (STATE)
{
case BEGIN://刚开始都是Bgein,根据下一个数来判断接下来的状态
if (nums[i-1] < nums[i]) {
STATE = UP;
max_Lneght++;
}
else if(nums[i-1]>nums[i])
{
STATE = DOWN;
max_Lneght++;
}
break;
/*对于up与down状态,如果为其中某一种,在接下来的序列中任然为当前状态就直接break
因为要找上升或下降的最大或最小值,以保证接下来的序列可能成为摇摆序列*/
case UP:
if (nums[i - 1] > nums[i]) {
STATE = DOWN;
max_Lneght++;
}
break;
case DOWN:
if (nums[i - 1] < nums[i]) {
STATE = UP;
max_Lneght++;
}
break;
default:
break;
}
}
return max_Lneght;
}
};
int main() {
vector<int>nums;
nums = { 1,17,5,10,13,15,10,5,16,8 };
Solution solve;
printf("%d\n", solve.wiggleMaxLnegth(nums));
return 0;
}
例4:移除K个数字
已知一个使用 字符串 表示的 非负整数num ,将num中的 k 个数字 移除,求移除k个数字后,可以获得的 最小的 可能的新数字。
例如:
输入 : num = “1432219” , k = 3
在 去掉3 个数字 后得到的很多很多可能里,如1432、4322、2219、 1219
、1229…;去掉数字4、3、2得到的1219最小!
输出:字符串
分析
若去掉某一位数字,为了使得到的新数字最小,需要 尽可能 让得到的新数字 优先最高位 最小, 其次次高位 最小,再其次第3位 位 最小…
例如:
一个4位数如 “ 1***” ,一定比任何 “9***”、 “8***”、…、“ 2***” 小 !
一个4位数若最高位确定 , 如“ 51**”一定比任何 “ 59**”、 “58**”、… 、“ 52**”小 !
一个4位数若最高、次高位确定 ,如“ 531*”一定比任何 “ 539*”、 “538*”、…、“ 532*” 小 !
举例说明:
对于数字1432219来说
我们可以得到以下规律:从 高位向低位 遍历,如果对应的数字 大于 下一位数字,则把该位数字去掉,得到的数字 最小 !
使用暴力法:去掉K个数字,就从最高位遍历k次。
考虑使用栈来进行优化
最后输出栈中的元素组成字符串
异常处理:
1.当所有数字都扫描完成后, k 仍然>0 ,应该做怎样的处理?
例如: num = 12345, k = 3 时。
2.当数字中 有 有0 出现 时,应该有怎样的特殊处理?
例如: num = 100200, k = 1 时。
3.如何将最后结果存储为 字符串 并返回?
示例代码:
class Solution {
public:
string removeKdigits(string num, int k) {
deque<int>S;
string result = "";
for (int i = 0; i < num.length(); i++)//遍历给定字符串
{
int number = num[i] - '0';//将字符转换为int
while (S.size()!=0&&S.back()>number&&k>0)//当队列不为空,且新来的数字小于队列尾部(栈顶)元素,且k大于0,就出元素出栈,--k
{
S.pop_back();
--k;
}
if (number!=0||S.size()!=0)//这里在防止字符中存在0的情况,当存在0的情况,且S为空的时候,不插入队列,目的就是不让0作为字符串的首个字符。
{
S.push_back(number);
}
}
while (S.size()!=0&&k>0)//当k不等于0了,但是原始字符串已经遍历完了,将队列尾部元素(栈顶)出队。可以保证得到的数是最小的。
{
S.pop_back();
--k;
}
for (int i = 0; i < S.size(); i++)//将队列元素组成string
{
result.append(1,'0'+S[i]);
}
if (result=="")//如果result为空,说明全0,就减result置0
{
result = '0';
}
return result;
}
};
int main() {
Solution solve;
string result = solve.removeKdigits("1432219", 3);
printf("%s\n", result);//输出1219
result = solve.removeKdigits("100200", 1);
printf("%s\n", result);//输出200
return 0;
}
例5:跳跃游戏1
一个数组存储了 非负整型数据 ,数组中的第i个元素a[i],代表了可以从数组第i个位置 最多 向前跳跃a[i]步;已知数组 各元素 的情况下,求是否可以从数组的第0个 位置 跳跃 到数组的 最后一个 元素的位置?
例如:
nums = [2, 3, 1, 1, 4] ,可以从nums[0] = 2 跳跃至 nums[4] = 4;
nums = [3, 2, 1, 0, 4] ,不可以从nums[0] = 3 跳跃至 nums[4] = 4。
分析
本题最难的点在于:每到一步的时候,根据当前位置的nums[i]确定的最大值到底应该选择跳到哪一个位置。
从第i个位置, 最远 可跳nums[i]步:
nums = [2, 3, 1, 1, 4, …];
从第i个位置, 最远可跳至 第index[i]个位置:
index[i] = i + nums[i];
index = [2, 4, 3, 4, 8, …]
若从第0位置 最远 可以跳至第i个位置;
则从第0位置 也一定 可以跳至:
第1个位置、第2个位置、…、第i-1个位置。
从第0个位置,应该跳至第1、第2、…、第i-1、第i个位置中的 哪个 ?
应该跳至第1、2、…、i-1、i位置中, 又可向前跳至 更最远位置
(即index[1]、index[2]、…、index[i-1]、…、index[i]最大的那个) 的位置! ( 贪心)
总结下来:若此时处在第i位置,该位置 最远 可以跳至第j位置(index[i]),故第i位置 还可跳至 :第i+1、i+2、…、j-1、j位置;
从第i位应 跳至 第i+1、i+2、…、j-1、j位中可以跳的 更远位置的位置 ,即index[i+1]、index[i+2]、…、index[j-1]、index[j] 最大的 那个!
原因:
假设接下来要跳的位置为x,index[x]最大,故从位置x出发,可以跳至i+1、i+2、…、j-1、j 所有
位置可以达到的位置 ,甚至还可以超过j;所以跳至位置x 最理想 。
算法思路
1.求从第i位置 最远可跳至 第index[i]位置:
根据从第i位置最远可跳nums[i]步: index[i] = nums[i] + i;
2.初始化:
1)设置变量jump代表 当前所处 的位置,初始化为0;
2)设置变量max_index代表从第0位置至第jump位置这个 过程中 , 最远可到达的位置 ,初始化为 index[0]。
3.利用jump扫描index数组, 直到 jump达到index数组尾部或jump超过max_index,扫描过程中,
更新max_index 。
4.若最终jump 为 数组长度 ,则返回true,否则返回false。
示例代码:
class Solution {
public:
bool canJump(vector<int >&nums) {
vector<int>index;//最远能跳到的位置
for (int i = 0; i < nums.size(); i++)
{
nums.push_back(i + nums[i]);//构造index数组,index数组表示的是当前i位置,最远能跳的位置
}
int jump = 0;
int max_index = index[0];//max_index就是当前位置能跳到的范围内,再下一次能跳到的最大位置(下标)。
while (jump < index.size()&&jump<=max_index)//如果jump到index的尾端,或者jump小于max_index了,结束循环
{
if (index[jump]>max_index)
{
max_index = index[jump];
}
++jump;
}
if (jump==index.size())//只有当jump跳到了index的尾端,才返回true
{
return true;
}
return false;
}
};
int main() {
vector<int > nums;
nums = { 2,3,1,1,4 };
Solution solve;
printf("%d\n", solve.canJump(nums));//输出1
return 0;
}