学会贪心1

1.贪心算法理论简介

贪心算法,又名贪婪法,是寻找最优解问题的常用方法,这种方法模式一般将求解过

程分成若干个步骤,但每个步骤都应用贪心原则,选取当前状态下最好/最优的选择

(局部最有利的选择),并以此希望最后堆叠出的结果也是最好/最优的解。

贪婪法的基本步骤:

步骤1:从某个初始解出发;

步骤2:采用迭代的过程,当可以向目标前进一步时,就根据局部最优策略,得到一

部分解,缩小问题规模;

步骤3:将所有解综合起来。

举一个例子:

例如,有一堆钞票,你可以拿走十张,如果想达到最大的金额,你要怎么拿?

指定每次拿最大的,最终结果就是拿走最大数额的钱。

每次拿最大的就是局部最优,最后拿走最大数额的钱就是推出全局最优。

2.题目

1.分发饼干

假设你是一位很棒的家长,想要给你的孩子们一些小饼干。但是,每个孩子最多只能给一块饼干。

对每个孩子 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.

分析

贪心:让每块饼干刚好满足一个孩子的胃口,能够最大可能的物尽其用

class Solution {
public:
	int findContentChildren(vector<int>& g, vector<int>& s) {
		vector<int> g_ = g;
		int result = 0;
		for (int i = 0; i < s.size(); i ++ ) {//给没块饼干尽量找一个刚好能满足的小孩,即浪费最小的优先满足
			int pos = -1;
			int tmp = INT_MAX;
			for (int j = 0; j < g_.size(); j++) {
				int a = s[i] - g_[j];
				if (a >= 0 && a < tmp) {
					tmp = a;
					pos = j;
				}
			}
			if (tmp != INT_MAX) {
				result++;
				g_.erase(g_.begin() + pos, g_.begin() + pos + 1);
			}
		}
		return result;
	}
};

2.摆动序列

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

  • 例如, [1, 7, 4, 9, 2, 5] 是一个 摆动序列 ,因为差值 (6, -3, 5, -7, 3) 是正负交替出现的。

  • 相反,[1, 4, 7, 2, 5] 和 [1, 7, 4, 5, 5] 不是摆动序列,第一个序列是因为它的前两个差值都是正数,第二个序列是因为它的最后一个差值为零。

子序列 可以通过从原始序列中删除一些(也可以不删除)元素来获得,剩下的元素保持其原始顺序。

给你一个整数数组 nums ,返回 nums 中作为 摆动序列最长子序列的长度

示例 1:

输入:nums = [1,7,4,9,2,5]
输出:6
解释:整个序列均为摆动序列,各元素之间的差值为 (6, -3, 5, -7, 3) 。

示例 2:

输入:nums = [1,17,5,10,13,15,10,5,16,8]
输出:7
解释:这个序列包含几个长度为 7 摆动序列。
其中一个是 [1, 17, 10, 13, 10, 16, 8] ,各元素之间的差值为 (16, -7, 3, -3, 6, -8) 。

示例 3:

输入:nums = [1,2,3,4,5,6,7,8,9]
输出:2

分析

最先想到的是动态规划

设dp(i)表示以第i个元素为结尾的摆动序列的最长子序列长度

flag[i]表示以第i个元素为结尾的摆动序列的最长子序列的最后一个差值,

则有:

对j:1到i-1:

如果flag(j)!=1&&flag(j)*(nums[i]-nums[j])<0) : dp[i]=max{dp[i], dp[j]+1},flag[i]=nums[i]-nums[j]

如果dp(j)==1代表以j结尾的摆动序列最大长度为1,此时flag(j)无意义,如果dp(i)<dp(j)+1,则dp[i]=dp[j], flag[i]=nums[i]-nums[j]

class Solution {
public:
	int wiggleMaxLength(vector<int>& nums) {
		if (nums.size()==1||(nums.size() == 2 && nums[0] == nums[1])) return 1;
		if (nums.size() == 2 && nums[0] != nums[1])return 2;
		vector<int> dp(nums.size(), 1);
		vector<int> flag(nums.size());//从第二个元素开始
		if(nums[1]!=nums[0])dp[1] = 2;
		flag[1] = nums[1] - nums[0];
		for (int i = 2; i < nums.size(); i++) {
			for (int j = 1; j < i; j++) {
				if (dp[j] == 1) {
					if (nums[i] != nums[j] && dp[i] < 2) {
						dp[i] = 2;
						flag[i] = nums[i] - nums[j];
					}
				}
                if(dp[j]!=1&&flag[j] * (nums[i] - nums[j]) < 0){
						if (dp[j] + 1 > dp[i]) {
							dp[i] = dp[j] + 1;
							flag[i] = nums[i] - nums[j];
					}
				}
			}
		}
		int result = 0;
		for (int i = 0; i < nums.size(); i++) {
			result = max(result, dp[i]);
		}
		return result;
	}
};

贪心算法

删除单调坡中间的节点

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

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

局部最优推出全局最优

class Solution {
public:
	int wiggleMaxLength(vector<int>& nums) {
		//思路,找出连续递增和连续递减区间,然后删除中间元素
		//即统计极值点的个数,极值点的特点:极值点分别减前/后两个值的差值异号
		if (nums.size() == 1|| (nums.size() == 2 && nums[0] == nums[1])) return 1;
		if (nums.size() == 2 && nums[0] != nums[1])return 2;
		int result = 1;//最后一个元素
		int pre_d=0, after_d;
		for (int i = 0; i < nums.size()-1; i++) {
			after_d = nums[i + 1] - nums[i];
			if ((pre_d <= 0 && after_d > 0) || (pre_d >= 0 && after_d < 0)) {
				result++;
				pre_d=after_d;
			}
		}
		return result;
	}
};

3.最大子数组和

给你一个整数数组 nums ,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。

子数组 是数组中的一个连续部分。

示例 1:

输入:nums = [-2,1,-3,4,-1,2,1,-5,4]
输出:6
解释:连续子数组 [4,-1,2,1] 的和最大,为 6 。

示例 2:

输入:nums = [1]
输出:1

示例 3:

输入:nums = [5,4,-1,7,8]
输出:23

分析

第一想到的动态规划

class Solution {
public:
	int maxSubArray(vector<int>& nums) {
		if (nums.size() == 1)return nums[0];
		vector<int> dp(nums.size() + 1, 0);//dp[i]表示以第i个元素结尾的连续子数组的最大和
		dp[1] = nums[0];
		int result = dp[1];
		for (int i = 2; i <= nums.size(); i++)
		{
			dp[i] = max(dp[i - 1] + nums[i - 1], nums[i - 1]);
			result = max(result, dp[i]);
		}
		return result;
	}
};

贪心算法

贪心贪的是哪里呢?

如果 -2 1 在一起,计算起点的时候,一定是从1开始计算,因为负数只会拉低总和,这就是贪心贪的地方!

局部最优:当前“连续和”为负数的时候立刻放弃,从下一个元素重新计算“连续和”,因为负数加上下一个元素 “连续和”只会越来越小。

全局最优:选取最大“连续和”

局部最优的情况下,并记录最大的“连续和”,可以推出全局最优

从代码角度上来讲:遍历nums,从头开始用count累积,如果count一旦加上nums[i]变为负数,那么就应该从nums[i+1]开始从0累积count了,因为已经变为负数的count,只会拖累总和。

class Solution {
public:
	int maxSubArray(vector<int>& nums) {
		if (nums.size() == 1)return nums[0];
		int count = nums[0];
		int result = nums[0];
		for (int i = 1; i < nums.size(); i++) {
			if (count > 0) {
				count += nums[i];
				result = max(result, count);
			}
			else {
				count = nums[i];
				result = max(result, count);
			}
			cout << count << endl;
		}
		return result;
	}
};

4.买卖股票的最佳时机 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 。

分析

先想到的还是动态规划

dp(i,0)表示第i天持有股票的最大收益,dp(i,1)表示第i天不持有股票的最大收益

则有:

dp(i,0)=max{dp(i-1,0), dp(i-1,1)-price[i]}

dp(i,1)=max{dp(i-1,1), dp(i-1,0)+price[i]}

class Solution {
public:
	int maxProfit(vector<int>& prices) {
		if (prices.size() == 0)return 0;
		vector<vector<int>> dp(prices.size(), vector<int>(2));
		dp[0][0] = -prices[0];//0表示持有,1表示不持有
		dp[0][1] = 0;
		for (int i = 1; i < prices.size(); i++) {
			dp[i][0] = max(dp[i - 1][0], dp[i - 1][1] - prices[i]);
			dp[i][1] = max(dp[i - 1][1], dp[i - 1][0] + prices[i]);
		}
		return dp[prices.size() - 1][1];
	}
};

贪心算法

要得到最大利润,就不断地在价格最低时买入,价格最高时卖出

假如第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) {
        if(prices.size()<=1) return 0;
		vector<int> profit(prices.size(), 0);
		profit[0] = 0;
		int result = 0;
		for (int i = 1; i < prices.size(); i++) {
			profit[i] = prices[i] - prices[i - 1];
			if (profit[i] > 0)result += profit[i];
		}
		return result;
	}
};

5.跳跃游戏

给定一个非负整数数组 nums ,你最初位于数组的 第一个下标

数组中的每个元素代表你在该位置可以跳跃的最大长度。

判断你是否能够到达最后一个下标。

示例 1:

输入:nums = [2,3,1,1,4]
输出:true
解释:可以先跳 1 步,从下标 0 到达下标 1, 然后再从下标 1 跳 3 步到达最后一个下标。

示例 2:

输入:nums = [3,2,1,0,4]
输出:false
解释:无论怎样,总会到达下标为 3 的位置。但该下标的最大跳跃长度是 0 , 所以永远不可能到达最后一个下标。

分析

刚开始还是想到的动态规划

设dp(i)表示能否从0到达第i个元素,初始化为0

如果存在j能够到达,且使得,j+nums[j]>=i (题目是可以跳跃nums[i]的距离,因此是>=,如果是只能跳nums[i],则是==)

则dp[i]=true

class Solution {
public:
	bool canJump(vector<int>& nums) {
		vector<int> dp(nums.size(), false);
		dp[0] = true;
		for (int i = 1; i < nums.size(); i++) {//求每个dp[i]
			for (int j = 0; j < i; j++) {
				if (dp[j] == true && (j + nums[j] >= i)) {
					dp[i] = true;
					break;
				}
			}
		}
		return dp[nums.size() - 1];
	}
};

超时了

贪心

每次都跳最远,在覆盖范围内不断更新最大覆盖范围,看能否覆盖终点

class Solution {
public:
	bool canJump(vector<int>& nums) {
		int distance = 0;//记录最大覆盖范围
		for (int i = 0; i < nums.size(); i++) {
			if (i <= distance) {//判断能否到达i
				distance = max(distance, i + nums[i]);//更新覆盖范围
			}
			else return false;//i都到不了,后面更到不了
		}
		return true;
	}
};

6.跳跃游戏II

给定一个长度为 n 的 0 索引整数数组 nums。初始位置为 nums[0]。

每个元素 nums[i] 表示从索引 i 向前跳转的最大长度。换句话说,如果你在 nums[i] 处,你可以跳转到任意 nums[i + j] 处:

  • 0 <= j <= nums[i]

  • i + j < n

返回到达 nums[n - 1] 的最小跳跃次数。生成的测试用例可以到达 nums[n - 1]。

示例 1:

输入: nums = [2,3,1,1,4]
输出: 2
解释: 跳到最后一个位置的最小跳跃数是 2。
     从下标为 0 跳到下标为 1 的位置,跳 1 步,然后跳 3 步到达数组的最后一个位置。

示例 2:

输入: nums = [2,3,0,1,4]
输出: 2

分析:上一题是问能否到达终点,这题是问跳到终点最少跳多少步

方法一,动态规划

dp[i]表示从0跳到i所需的最小步数

if(dp[j] != INT_MAX && (j + nums[j] >= i)) dp[i] = min(dp[i], dp[j] + 1); j从0到i-1

class Solution {
public:
	int jump(vector<int>& nums) {
		if (nums.size() == 0)return 0;
		vector<int> dp(nums.size(), INT_MAX);
		dp[0] = 0;
		for (int i = 1; i < nums.size(); i++) {//求dp[i]
			for (int j = 0; j < i; j++) {
				if (dp[j] != INT_MAX && (j + nums[j] >= i)) {
					dp[i] = min(dp[i], dp[j] + 1);
				}
			}
		}
		return dp[nums.size() - 1];
	}
};

方法二,贪心

思路一:

我们的目标是到达数组的最后一个位置,因此我们可以考虑最后一步跳跃前所在的位置,该位置通过跳跃能够到达最后一个位置。

如果有多个位置通过跳跃都能够到达最后一个位置,那么我们应该如何进行选择呢?直观上来看,我们可以「贪心」地选择距离最后一个位置最远的那个位置,也就是对应下标最小的那个位置。因此,我们可以从左到右遍历数组,选择第一个满足要求的位置。

class Solution {
public:
	int jump(vector<int>& nums) {
		if (nums.size() == 0)return 0;
		int position = nums.size() - 1;
		int step = 0;
		while (position > 0) {
			for (int i = 0; i < position; i++) {
				if (i + nums[i] >= position) {
					position = i;
					step++;
					break;
				}
			}
		}
		return step;
	}
};

思路二:

每次在上次能跳到的范围(end)内选择一个能跳的最远的位置(也就是能跳到max_far位置的点)作为下次的起跳点

class Solution {
public:
    int jump(vector<int>& nums) 
    {
        int max_far = 0;// 目前能跳到的最远位置
        int step = 0;   // 跳跃次数
        int end = 0;    // 上次跳跃可达范围右边界(下次的最右起跳点)
        for (int i = 0; i < nums.size() - 1; i++)
        {
            max_far = max(max_far, i + nums[i]);
            // 到达上次跳跃能到达的右边界了
            if (i == end)
            {
                end = max_far;  // 目前能跳到的最远位置变成了下次起跳位置的有边界
                step++;         // 进入下一次跳跃
            }
        }
        return step;
    }
};

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值