【贪心算法】(一)贪心算法理论及基础习题

理论基础

什么是贪心: 贪心的本质是选择每一阶段的局部最优,从而达到全局最优。

贪心算法没有固定套路,贪心算法大致一般分为如下四步:

  • 将问题分解为若干个子问题

  • 找出适合的贪心策略

  • 求解每一个子问题的最优解

  • 将局部最优解堆叠成全局最优解

简单例题

分发饼干

力扣原题
  假设你是一位很棒的家长,想要给你的孩子们一些小饼干。但是,每个孩子最多只能给一块饼干。对每个孩子 i,都有一个胃口值 g[i],这是能让孩子们满足胃口的饼干的最小尺寸;并且每块饼干 j,都有一个尺寸 s[j] 。如果 s[j] >= g[i],我们可以将这个饼干 j 分配给孩子 i ,这个孩子会得到满足。你的目标是尽可能满足越多数量的孩子,并输出这个最大数值。

示例 1:
输入: g = [1,2,3], s = [1,1]
输出: 1
解释: 你有三个孩子和两块小饼干,3个孩子的胃口值分别是:1,2,3。虽然你有两块小饼干,由于他们的尺寸都是 1,你只能让胃口值是 1 的孩子满足。所以应该输出1。

示例 2:
输入: g = [1,2], s = [1,2,3]
输出: 2
解释: 你有两个孩子和三块小饼干,2 个孩子的胃口值分别是1,2。你拥有的饼干数量和尺寸都足以让所有孩子满足。所以你应该输出 2。

思路

  1. 先将饼干大小s[i]与孩子的胃口g[i]进行排序
  2. 每一轮保证当前胃口最小的孩子得到饼干吃
  3. 直到最大的饼干大小都无法满足当前孩子的胃口

代码实现

class Solution {
public:
	// g[i] 最小胃口   s[i] 饼干大小		s[i] >= g[i]
    int findContentChildren(vector<int>& g, vector<int>& s) {
		int res = 0;
		int startIdx = 0;				//记录发到第几块饼干了
		sort(g.begin(), g.end());
		sort(s.begin(), s.end());
		for(int i = 0; i < g.size(); i++)
		{
			for(int j = startIdx; j < s.size(); j++)
			{
				if(s[j] >= g[i])
				{
					res++;
					startIdx = j+1;		//从下一块饼干开始发
					break;	
				}
			}
		}
		return res;
    }
};

K次取反后最大化的数组和

力扣链接

给定一个整数数组 nums 和一个整数 k ,按以下方法修改该数组:

  • 选择某个下标 i 并将 nums[i] 替换为 -nums[i]
  • 重复这个过程恰好 k 次。可以多次选择同一个下标 i

以这种方式修改数组后,返回数组 可能的最大和

核心思想

  • 局部最优: 让绝对值大的负数变为正数,当前数值达到最大
  • 整体最优: 整个数组和达到最大。

  将数组按照绝对值大小从大到小排序,从前向后遍历,遇到负数将其变为正数,同时K–;结束后如果K还大于0,那么反复转变数值最小的元素,将K用完。

代码实现

class Solution {
static bool cmp(int a, int b)
{
	return abs(a) > abs(b);
}
public:
    int largestSumAfterKNegations(vector<int>& nums, int k) {
    	int sum = 0;
		sort(nums.begin(), nums.end(),cmp);			//按绝对值从大到小排序
		// 把尽可能多的负数转为正数
		for(int i = 0; i < nums.size();i++)
		{
			if(nums[i] < 0 && k > 0)
            {
				nums[i] = nums[i] * -1;
				k--;
            }
		}
		
		if(k % 2)		// 若还需要转奇数次,把最小的数转一次
			nums[nums.size()- 1] = nums[nums.size()- 1] * -1;
			
		for(int num:nums)
			sum += num;
			
		return sum;
    }
};

柠檬水找零

力扣链接
  每位顾客只买一杯柠檬水,然后向你付 5 美元、10 美元或 20 美元。你必须给每个顾客正确找零,也就是说净交易是每位顾客向你支付 5 美元,一开始手头无钱。

示例 1:
输入:bills = [5,5,5,10,20]
输出:true

示例 2:
输入:bills = [5,5,10,10,20]
输出:false

核心思想
贪心:
局部最优: 遇到账单20,优先消耗美元10,完成本次找零。
全局最优: 完成全部账单的找零。
因为美元10只能给账单20找零,而美元5可以给账单10和账单20找零,美元5更万能!

代码实现

class Solution {
public:
    bool lemonadeChange(vector<int>& bills) {
	    int money5 = 0;
		int money10 = 0;
		for(int i = 0; i < bills.size();i++)
		{
			if(bills[i] == 5)
				money5++;
			else if(bills[i] == 10)
			{
				money10++;
				if(money5 > 0)	
					money5--;
				else
					return false;
			}
            //收到20 有10块的先找10块的 
			else if(bills[i] == 20)
			{
				if(money5 > 0 && money10 > 0)
				{
					money5--;
					money10--;
				}
				else if(money5 >= 3)
					money5 -= 3;
				else
					return false;
			}
		}
		return true;
    }
};

买卖股票的最佳时机 Ⅱ

力扣原题

  给你一个整数数组 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 。

贪心思想
这道题目可能我们只会想,选一个低的买入,再选个高的卖,再选一个低的买入…循环反复,其实最终利润是可以分解的。

假如第 0 天买入,第 3 天卖出,那么利润为:prices[3] - prices[0]。

相当于(prices[3] - prices[2]) + (prices[2] - prices[1]) + (prices[1] - prices[0])。

此时就是把利润分解为每天为单位的维度,而不是从 0 天到第 3 天整体去考虑!
在这里插入图片描述
局部最优:收集每天的正利润,全局最优:求得最大利润。

代码实现

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

单调递增的数字

力扣链接

给定一个非负整数 N,找出小于或等于 N 的最大的整数,同时这个整数需要满足其各个位数上的数字是单调递增。

暴力求解
  暴力的思想即是通过逆序遍历N,判断每一个数字是否符合单调递增的规律,若符合,则直接返回。

bool judge(int num)	
{
	int max = 10;
       while (num) 
	{
           int t = num % 10;	// 依次取每一位
           if (max >= t) 		// 单调递增
			max = t;
           else 
			return false;
           num = num / 10;
       }
       return true;
}

贪心求解

  • 拿一个两位的数字举例,例如98,一旦出现strNum[i - 1] > strNum[i]的情况(非单调递增),首先想让strNum[i - 1]--,然后strNum[i]给为9,即得到小于98的最大的单调递增整数。
  • 从后向前遍历,可以重复利用上次比较得出的结果,从后向前遍历332的数值变化为:332 -> 329 -> 299
  • 最后代码实现的时候,也需要一些技巧,例如用一个flag来标记从哪里起(从第一次出现 strNum[i - 1] > strNum[i] )后面开始赋值9。

代码实现

int monotoneIncreasingDigits(int n) 
{
	string strNum = to_string(n);
	int flag = strNum.size();			// 标记才哪开始起全部赋值为9
	for(int i = strNum.size()-1; i > 0; i--)
	{
		if(strNum[i-1] > strNum[i])		// 第一次出现非递增情况
		{
			strNum[i-1]--;
			flag = i;					// 记录下标,后面全部赋值为9
		}	
	}
	for(int i = flag; i < strNum.size(); i++)
		strNum[i] = '9';
		
	return stoi(strNum);
}

摆动序列

力扣原题

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

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

子序列 可以通过从原始序列中删除一些(也可以不删除)元素来获得,剩下的元素保持其原始顺序。给你一个整数数组 nums ,返回 nums 中作为 摆动序列最长子序列的长度

思路

  • 本题是典型的查找变号 / 波峰波谷问题,但存在诸多特殊情况的细节问题

  • 局部最优:删除单调坡度上的节点(不包括单调坡度两端的节点),那么这个坡度就可以有两个局部峰值。

  • 整体最优:整个序列有最多的局部峰值,从而达到最长摆动序列。

  • 但本题要考虑三种特殊平坡情况:
    (1)情况一:上下坡中有平坡
    (2)情况二:数组首尾两端
    (3)情况三:单调坡中有平坡

(1)情况一:上下坡中有平坡
在这里插入图片描述
如图,可以统一规则,保留平坡最右边的数字,即删除左边的三个 2:
在这里插入图片描述

  • 在图中,当 i 指向第一个 2 的时候,prediff > 0 && curdiff = 0 ,当 i 指向最后一个 2 的时候 prediff = 0 && curdiff < 0

  • 如果我们采用,删左面三个 2 的规则,那么 当 prediff = 0 && curdiff < 0 也要记录一个峰值,因为他是把之前相同的元素都删掉留下的峰值。

  • 所以我们记录峰值的条件应该是: (preDiff <= 0 && curDiff > 0) || (preDiff >= 0 && curDiff < 0),为什么这里允许 prediff == 0 ,就是为了 上面我说的这种情况。

(2)情况二:数组首尾两端

  • 可以假设,数组最前面还有一个数字,那这个数字和第一个数字相同,即使得prediff = 0,curdiff < 0 或者 >0 也记为波谷。
  • 那么为了规则统一,针对序列[2,5],可以假设为[2,2,5],这样它就有坡度了即 preDiff = 0,如图:
    在这里插入图片描述
    针对以上情形,result 初始为 1(默认最右面有一个峰值),此时 curDiff > 0 && preDiff <= 0,那么 result++(计算了左面的峰值),最后得到的 result 就是 2(峰值个数为 2 即摆动序列长度为 2)

代码初步实现:

// 版本一
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 ((preDiff <= 0 && curDiff > 0) || (preDiff >= 0 && curDiff < 0)) {
                result++;
            }
            preDiff = curDiff;
        }
        return result;
    }
};

(3)情况三:单调坡中有平坡
在上述代码中,忽略了一种情况,即 如果在一个单调坡度上有平坡,例如[1,2,2,2,3,4],如图:
在这里插入图片描述

  • 图中,我们可以看出,上述代码在三个地方记录峰值,但其实结果因为是 2,因为 单调中的平坡 不能算峰值(即摆动),之所以版本一会出问题,是因为我们实时更新了 prediff。
  • 我们只需要在 这个坡度 摆动变化的时候,更新 prediff 就行,这样 prediff 在 单调区间有平坡的时候 就不会发生变化,造成我们的误判。

代码最终实现

class Solution {
public:
    int wiggleMaxLength(vector<int>& nums) {
    	int res = 1;			// 最长子序列 默认最右边有坡度
    	int prediff = 0;		// 保存上一轮结果正负
    	int curdiff = 0;		// 暂存当前结果正负
    	bool flag = false;	
    	// 1个
		if(nums.size() == 1)
			return 1;
		for(int i = 0; i < nums.size() - 1; i++)	//去掉最后一个 默认有波动
		{
			curdiff = nums[i+1] - nums[i];
			//波动
			if(prediff >= 0 && curdiff < 0 || prediff <= 0 && curdiff > 0)
			{
				res++;
				prediff = curdiff;
			}
		}
		return res;
	}
};

两个纬度权衡问题

  这种双边题目一定是要确定一边之后,再确定另一边,如果两边一起考虑一定会顾此失彼。

分发糖果

力扣链接

n 个孩子站成一排。给你一个整数数组 ratings 表示每个孩子的评分,你需要按照以下要求,给这些孩子分发糖果:

  • 每个孩子至少分配到 1 个糖果。
  • 相邻两个孩子评分更高的孩子会获得更多的糖果。

请你给每个孩子分发糖果,计算并返回需要准备的 最少糖果数目 。

示例 1:
输入:ratings = [1,0,2]
输出:5
解释:你可以分别给第一个、第二个、第三个孩子分发 2、1、2 颗糖果。

示例 2:
输入:ratings = [1,2,2]
输出:4
解释:你可以分别给第一个、第二个、第三个孩子分发 1、2、1 颗糖果。第三个孩子只得到 1 颗糖果,这满足题面中的两个条件。

核心思想

  • 这种双边题目一定是要确定一边之后,再确定另一边,例如比较每一个孩子的左边,然后再比较右边,如果两边一起考虑一定会顾此失彼。

  • 局部最优:只要右边评分比左边大,右边的孩子就多一个糖果,全局最优:相邻的孩子中,评分高的右孩子获得比左边孩子更多的糖果

  • 如果ratings[i] > ratings[i - 1] 那么[i]的糖 一定要比[i - 1]的糖多一个,所以贪心:candyVec[i] = candyVec[i - 1] + 1,代码如下:

    // 从前向后
    for (int i = 1; i < ratings.size(); i++) {
        if (ratings[i] > ratings[i - 1]) candyVec[i] = candyVec[i - 1] + 1;
    }
    
  • 再确定左孩子大于右孩子的情况,为什么需要从后向前遍历?

  • 如果 ratings[i] > ratings[i + 1],此时candyVec[i]有两个选择了,一个是candyVec[i + 1] + 1(从右边这个加1得到的糖果数量),一个是candyVec[i](之前比较右孩子大于左孩子得到的糖果数量),那么又要贪心了,局部最优:取candyVec[i + 1] + 1 和 candyVec[i] 最大的糖果数量,保证第i个小孩的糖果数量既大于左边的也大于右边的,遍历代码如下;

    // 从后向前
    for (int i = ratings.size() - 2; i >= 0; i--) {
        if (ratings[i] > ratings[i + 1] ) {
            candyVec[i] = max(candyVec[i], candyVec[i + 1] + 1);
        }
    }
    

代码实现

class Solution {
public:
	// 每个孩子至少分配到 1 个糖果。
	// 相邻两个孩子评分更高的孩子会获得更多的糖果。
	int candy(vector<int>& ratings) {
		int sum = 0;
		vector<int> candyVec(ratings.size(),1);
		// 右孩子比左孩子大
		for(int i = 0; i < ratings.size() - 1; i++)
		{
			if(ratings[i] < ratings[i+1])
				candyVec[i+1] = candyVec[i] + 1;
		}
		
		// 左孩子比右孩子大
		for(int i = ratings.size() - 1; i > 0; i--)
		{
			if(ratings[i-1] > ratings[i])
				candyVec[i-1] = max(candyVec[i-1], candyVec[i] + 1);
		}
		
		for(int num: candyVec)
			sum += num;
			
		return sum;
    }
};

根据身高重建队列

力扣链接

  假设有打乱顺序的一群人站成一个队列,数组 people 表示队列中一些人的属性(不一定按顺序)。每个 people[i] = [hi, ki] 表示第 i 个人的身高为 hi ,前面 正好 有 ki 个身高大于或等于 hi 的人。

请你重新构造并返回输入数组 people 所表示的队列。返回的队列应该格式化为数组 queue ,其中 queue[j] = [hj, kj] 是队列中第 j 个人的属性(queue[0] 是排在队列前面的人)。

示例 1:
输入:people = [[7,0],[4,4],[7,1],[5,0],[6,1],[5,2]]
输出:[[5,0],[7,0],[5,2],[6,1],[4,4],[7,1]]

示例 2:
输入:people = [[6,0],[5,0],[4,0],[3,2],[2,2],[1,4]]
输出:[[4,0],[5,0],[2,2],[3,2],[1,4],[6,0]]

核心思想

  • 本题有两个维度,h和k,遇到两个维度权衡的时候,一定要先确定一个维度,再确定另一个维度,如果两个维度一起考虑一定会顾此失彼
  • 如果按照k来从小到大排序,排完之后,k与h均不符合条件;若按照身高h来排序,身高一定是从大到小排,让高个子在前面。此时先确定身高维度,前面的节点一定都比本节点高!
  • 按照身高排序之后,优先按身高高的people的k来插入,后序插入节点也不会影响前面已经插入的节点,最终按照k的规则完成了队列。

局部最优: 优先按身高高的people的k来插入。插入操作过后的people满足队列属性
全局最优: 最后都做完插入操作,整个队列满足题目队列属性

代码实现

class Solution {
public:
	static bool cmp(vector<int> &a, vector<int> &b)
	{
		if(a[0] == b[0])
			return a[1] < b[1];
		else
			return a[0] > b[0];
	}
    vector<vector<int>> reconstructQueue(vector<vector<int>>& people) 
	{
		vector<vector<int>> queue;
		// 按身高从到到低排序
		sort(people.begin(), people.end(), cmp);
		for(int i = 0; i < people.size(); i++)
		{
			int insertPos = people[i][1];
			queue.insert(queue.begin() + insertPos, people[i]);
		}
		return queue;
    }
};
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

不会编程的小江江

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值