目录
(一)贪心
1、贪心算法的概念
- 贪心算法(Greedy Alogorithm),即逐步获得最优解,原理是通过局部最优来达到全局最优,是用来解决最优问题的,但其适用范围有限,在每一步的选择中,只考虑当前对自己最有利的选择
- 没有固定的框架,其核心是贪心策略的选择
- 通过局部最优来达到全局最优,贪心策略的选择不是一件容易的事情,要想证明局部最优为全局最优,还需要通过数学证明,证明其是否具有贪心选择性质,否则不能说明其为全局最优。
2、需要满足的条件
- 最优子结构:规模较大的问题的解由规模较小的子问题的解组成,规模较大的问题的解只由其中一个规模较小的子问题的解决定;
- 无后效性:后面阶段的求解不会修改前面阶段已经计算好的结果;
- 贪心选择性质:从局部最优解可以得到全局最优解。
3、基本的步骤
- 从问题的某个初始解出发
- 采用循环语句,当可以向求解目标前进一步时,就根据局部最优解策略,得到一个部分解,缩小问题的规模
- 将所有的部分解综合起来,得到问题的最终解
-
Greedy(C) //C是问题的输入集合即候选集合 { S={ }; //初始解集合为空集 while (not solution(S)) //集合S没有构成问题的一个解 { x=select(C); //在候选集合C中做贪心选择 if feasible(S, x) //判断集合S中加入x后的解是否可行 S=S+{x}; C=C-{x}; } return S;
4、leetcode上的题目
分饼干问题
class Solution {
public:
int findContentChildren(vector<int>& g,vector<int>& s){//g为胃口大小,s为饼干大小
sort(g.begin(),g.end());//给胃口排序
sort(s.begin(),s.end());//给饼干排序
int gm=g.size();//胃口个数
int sm=s.size();//饼干个数
int count=0;//答案计数
int i,j;
for(i=0,j=0;i<gm&&j<sm;i++,j++){//i为饥饿度的指针,j为饼干的指针
while(j<sm&&g[i]>s[j])//j一直移动直到找到满足条件的饼干,当前的饼干满足条件
j++;
if(j<sm)//避免上面的j越界
count++;//计数器加1,进入下一个for循环时会跳入下一个饼干
}
return count;//输出最大的饼干数
}
};
这里的贪心策略为:应该按照孩子的胃口从小到大的顺序依次满足每个孩子,且对于每个孩子,应该选择可以满足这个孩子的胃口且尺寸最小的饼干。
活动安排问题
这里的贪心策略为:就是找出最早完成的活动,然后按照活动相容的条件(上一个活动的结束时间早于下一个活动的开始时间)依次往后寻找。需要将所有活动按照结束时间进行非降序的排序,依次排列遍历找出最大的相容活动子集合。
摆动序列
class Solution {
public:
int wiggleMaxLength(vector<int>& nums) {
if(nums.size()<2)return nums.size();
int prevdiff=nums[1]-nums[0];//记录前两个数的升降情况,两个数相同则“平缓”,只算一个节点
int res= prevdiff != 0 ? 2 : 1;//两个数不相同则开始有两个节点
for(int i=2;i<nums.size();i++){
int diff=nums[i]-nums[i-1];//记录当前的节点的升降趋势
if((diff>0&&prevdiff<=0)||(diff<0&&prevdiff>=0)){//检查是否为峰谷
res++;
prevdiff=diff;//更新趋势
}
}
return res;
}
};
这里的贪心策略为:不断地交错选择「峰」与「谷」,可以使得该序列尽可能长。每次加入一个新元素时,用新的上升下降趋势与之前对比,如果出现了「峰」或「谷」,答案加一,并更新当前序列的上升下降趋势。
盛最多水的容器
class Solution {
public:
int maxArea(vector<int>& height) {
int i = 0, j = height.size() - 1, res = 0;//i从左边开始,j从右边开始
while(i < j) {//指针相遇时停下
res = height[i] < height[j] ? //找短板,移动长板s一定会变小,移动短板可能会变大
max(res, (j - i) * height[i++]): //看是否需要更新最大面积
max(res, (j - i) * height[j--]);
}
return res;
}
};
这里的贪心策略为:每次都去找短板,移动长板s一定会变小(目前最优解),移动短板s可能会变大。
跳跃游戏
class Solution {
public:
bool canJump(vector<int>& nums) {
int n=nums.size();//数组长度
int rightmost=0;//最远距离
for(int i=0;i<n;i++){//遍历每一个数的最远距离
if(i<=rightmost){//当前的i还可到达则执行
rightmost=max(rightmost,i+nums[i]);//更新最远距离
if(rightmost>=n-1)
return true;//可以到达最后一个数
}
}
return false;//不可以到达
}
};
这里的贪心策略为:遍历数组中的每一个元素,对于每一个元素,求出其可到达的最远距离。
跳跃游戏II
class Solution {
public:
int jump(vector<int>& nums) {
int maxPos = 0, n = nums.size(), end = 0, step = 0;//最大跳数,长度,边界,最短要跳
for (int i = 0; i < n - 1; ++i) {//遍历每一个元素
if (maxPos >= i) {
maxPos = max(maxPos, i + nums[i]);//更新能够到达的最大下标位置
if (i == end) {
end = maxPos;//区间上的能够到达的最大下标位置作为下一个边界
++step;
}
}
}
return step;
}
};
//当最后一个恰好为边界时,会增加一跳,所以不要
这里的贪心策略为:从第一个数开始遍历数组,根据跳数确定一个区间,区间的右边界,遍历区间,在区间上确定最大跳数的元素,将最跳数更新为这个跳数,遍历完区间时,将最大跳数作为下一个边界。
股票买卖的最佳时机
class Solution {
public:
int maxProfit(vector<int>& prices) {
int len=prices.size();
int max=0;//总利润
for(int i=1;i<len;i++){
int temp=prices[i]-prices[i-1];//第 i-1 日买入与第 i 日卖出赚取的利润
if(temp>0){//有钱赚
max+=temp;
}
}
return max;
}
};
这里的贪心策略为:第 i-1
日买入与第 i
日卖出赚取的利润,只看这两天有没有利润,局部最优
最长回文串
class Solution {
public:
int longestPalindrome(string s) {
unordered_map<char,int>count;//创建一个哈希表count
int ans=0;//计数器
for(char c:s){//从s的第一个字符开始,往后循环
++count[c];给ke为c的字符的value +1
}
for(auto p:count){//auto用来简化初始化p,根据后面的count可知p为哈希表类型
int v=p.second;//拿出当前的value
ans+=v/2*2;//每次都要拿最长的
if(v%2==1&&ans%2==0)//遇到第一个个数为奇数的字符串时,ans++,作为最中间的轴
ans++;
}
return ans;
}
};
这里的贪心策略为:每次都要拿最长的
移掉k位数字
class Solution {
public:
string removeKdigits(string num, int k) {
vector<int> s;
string result="";
for(int i=0;i<num.length();i++){
int number=num[i]-'0';//转化成整数
while(s.size()!=0&&k>0&&s[s.size()-1]>number){//必须用while,遇到“1230”
s.pop_back();
k--;
}
if(s.size()!=0||number!=0){
s.push_back(number);
}
}
while(s.size()!=0&&k>0){
s.pop_back();
k--;
}
for(int i=0;i<s.size();i++){
result.append(1,s[i]+'0');//在后面加“1”个字符
}
if(result=="") result="0";
return result;
}
};
这里的贪心策略为:从高位向地位遍历,如果对应的数字大于下一位数字,则把该位数字去掉,得到的数字最小,使得每去掉一位都能得到当前最小的数字。
最大数
class Solution {
public:
string largestNumber(vector<int>& nums) {
vector<string> str;
for(auto i : nums) {
str.push_back(to_string(i));//将数字常量转换成字符串后放入str
}
// 使用 lambda 比较 elements.
auto cmp = [](string left, string right) {
return left + right > right + left;
};
sort(str.begin(),str.end(), cmp);
//定义一个对象ss
stringstream ss;
for(auto c : str) {
ss << c;//把c放入ss中
}
string ans = ss.str();//ss中的全部的字符取出
if(ans[0] == '0'){
return "0";
}
return ans;
}
};
贪心策略:根据拼接的「结果」来决定 a和 b的排序关系:如果拼接结果 ab要比 ba好,那么我们会认为 a应该放在 b前面。按照这种规则把数组中的值重新排序。
加油站
class Solution {
public:
int canCompleteCircuit(vector<int>& gas, vector<int>& cost)
{
int start=0;// 从start出发
int spare=0; //从start出发的话到当前位置的油量
int sum=0; //记录总和
for(int i=0;i<gas.size();i++)
{
spare+=gas[i]-cost[i]; //
sum+=gas[i]-cost[i];
if(spare<0) //spare<0说明从start开始不满足,将start更新为当前位置的下一个位置
{
start=i+1;
spare=0;
}
}
return (sum<0)?-1:(start);
}
};
贪心策略:如果总油量减去总消耗大于等于零那么一定可以跑完一圈,因此要跑完一圈就要保证在各个站点的剩油量>=0。 局部最优:若当前累加到 j 的和 Sum<0 ,起始位置至少要是 j+1,因为从 j开始一定不行。 全局最优:找到可以跑一圈的起始位置。