基于局部最优的选择逐渐推导出全局最优解
将问题分解为若干个子问题;
找出合适的贪心策略;
求解每一个子问题的最优解;
将局部最优解合成为全局最优解;
题目
如果连续数字之间的差严格地在正数和负数之间交替,则数字序列称为 摆动序列 。第一个差(如果存在的话)可能是正数或负数。仅有一个元素或者含两个不等元素的序列也视作摆动序列。
例如, [1, 7, 4, 9, 2, 5] 是一个 摆动序列 ,因为差值 (6, -3, 5, -7, 3) 是正负交替出现的。
相反,[1, 4, 7, 2, 5] 和 [1, 7, 4, 5, 5] 不是摆动序列,第一个序列是因为它的前两个差值都是正数,第二个序列是因为它的最后一个差值为零。 子序列 可以通过从原始序列中删除一些(也可以不删除)元素来获得,剩下的元素保持其原始顺序。
给你一个整数数组 nums ,返回 nums 中作为 摆动序列 的 最长子序列的长度 。
来源:力扣(LeetCode) 链接:力扣 著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
思路
求取摆动子序列元素个数,则与原数组峰值个数有关;
遍历原数组,记录相邻两个数的差值,若后面相邻两数差值与之前异号,则摆动子序列元素个数加1;
代码
class Solution { public: int wiggleMaxLength(vector& nums) { if(nums.size() == 1)return 1; int pre_off = 0, cur_off;//pre_off用于记录最后录入两个元素的差值的正负,cur_off用于记录原数组相邻两个数的差值 int result = 1; for(int i=1; i 0) || (pre_off >=0 && cur_off <0)){//pre_off需要设置=0是由于初始化时pre_off设置为0,后面由于pre_off = cur_off则不可能再出现=0可能 result++; pre_off = cur_off; } } return result; } };
题目
给定一个数组 prices ,其中 prices[i] 表示股票第 i 天的价格。
在每一天,你可能会决定购买和/或出售股票。你在任何时候 最多 只能持有 一股 股票。你也可以购买它,然后在 同一天 出售。 返回 你能获得的 最大 利润 。
来源:力扣(LeetCode) 链接:力扣 著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
思路
方法一(贪心算法)
遍历数组,若后一天价格高于前一天则累加收益;
上面操作能获得最大利润的原因是,遍历到某一天时并不确定当天是否进行买入,直到第二天看到存在利润才决定前一天的是否进行买入,而看到利润的同时并没有决定是否卖出,直至后面出现新利润才决定是否卖出;
方法二(动态规划)
p[i] [0]示第i+1天手中无股票,可能昨天没有或者昨天有今天卖出;
p[i] [1]表示第i+1天手中有股票,可能昨天有股票或者昨天没有今天买入;
由前一天两种可能的状态推断今天两种可能的状态(获取较高的利润);
代码
方法一(贪心算法)
class Solution { public: int maxProfit(vector& prices) { int result = 0; for(int i=1; i prices[i-1]){ result+=prices[i]-prices[i-1]; } } return result; } };
方法二(动态规划)
class Solution { public: int maxProfit(vector& prices) { int len = prices.size(); vector > p(len, vector (2, 0)); p[0][0] = 0; p[0][1] = -prices[0]; for(int i=1; i 3. LeetCode 45. 跳跃游戏
题目
给你一个非负整数数组 nums ,你最初位于数组的第一个位置。
数组中的每个元素代表你在该位置可以跳跃的最大长度。
你的目标是使用最少的跳跃次数到达数组的最后一个位置。
假设你总是可以到达数组的最后一个位置。
来源:力扣(LeetCode) 链接:力扣 著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
思路
每次跳跃时,计算每个可跳跃节点中最远跳跃距离下标,当可跳跃节点遍历后,即进行下一次跳跃,跳跃次数增加1;
用temp记录尝试过可跳跃节点最远跳跃下标,在每次可跳跃节点遍历后更新reach;
当reach>=nums.size()-1时代表本次跳跃可以跳到最后,增加跳跃次数后,退出循环;
代码
class Solution { public: int jump(vector& nums) { int count = 0;//记录跳跃次数 int reach = nums[0];//记录一次跳跃中可跳跃节点最远跳跃距离下标 int temp = nums[0];//持续更新跳跃最远距离下标 for(int i=1; i =nums.size()-1){ count++; break; } if(i == reach){ reach = temp; count++; } } return count; } }; 4. LeetCode 1005. K次取反后最大化的数组和
题目
给你一个整数数组 nums 和一个整数 k ,按以下方法修改该数组:
选择某个下标 i 并将 nums[i] 替换为 -nums[i] 。 重复这个过程恰好 k 次。可以多次选择同一个下标 i 。
以这种方式修改数组后,返回数组 可能的最大和 。
来源:力扣(LeetCode) 链接:力扣 著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
思路
将数组按绝对值从大到小进行排序;
从头遍历数组,将负数变为正数,同时k减1,当k为0或者数组遍历结束后退出循环;
退出循环后,若k仍大于0,且为奇数时,则将数组最后一个元素取反;
思想就是将最小的负数变为正数,若所有元素均为正数时,且变化次数剩余为奇数,则将最小正数直接取反;(若变化剩余次数为偶数,则可认为将任意一个数做偶次数取反变化,最终值不变化)
代码
class Solution { private: static bool mycompare(const int &a, const int &b){ return abs(a)>abs(b); } public: int largestSumAfterKNegations(vector& nums, int k) { sort(nums.begin(), nums.end(), mycompare); for(int i=0; i 0; i++){ if(nums[i] < 0){ nums[i] = -nums[i]; k--; } } if(k%2==1){ nums[nums.size()-1] *= -1; } int result = 0; for(int i=0; i 5. LeetCode 134. 加油站
题目
在一条环路上有 n 个加油站,其中第 i 个加油站有汽油 gas[i] 升。
你有一辆油箱容量无限的的汽车,从第 i 个加油站开往第 i+1 个加油站需要消耗汽油 cost[i] 升。你从其中的一个加油站出发,开始时油箱为空。
给定两个整数数组 gas 和 cost ,如果你可以绕环路行驶一周,则返回出发时加油站的编号,否则返回 -1 。如果存在解,则 保证 它是 唯一 的。
来源:力扣(LeetCode) 链接:力扣 著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
思路
遍历一遍,用run记录总油量是否可以走完全程(即能否完成一圈)
oil用于记录从某一位置开始油箱中的当前油量,当油量为负数时,说明前一部分路程不能到达,开始位置应该设置为下一地点
代码
class Solution { public: int canCompleteCircuit(vector& gas, vector & cost) { int oil = 0, run = 0, start = 0; for(int i=0; i =0?start:-1; } }; 6. LeetCode 135. 分发糖果
题目
n 个孩子站成一排。给你一个整数数组 ratings 表示每个孩子的评分。
你需要按照以下要求,给这些孩子分发糖果:
每个孩子至少分配到 1 个糖果。 相邻两个孩子评分更高的孩子会获得更多的糖果。 请你给每个孩子分发糖果,计算并返回需要准备的 最少糖果数目 。
来源:力扣(LeetCode) 链接:力扣 著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
思路
由于每个孩子所能获得的糖果同时与两边孩子评分有关,则单通过一个方向的遍历不能得到答案;
从两个方向遍历,即可同时兼顾两边情况,当从左向右遍历时,若左边评分大于自身评分,则设置糖果数比左边大1,否则设置为1;
当从右向左遍历时,若右边评分小于自身评分,则比较上次遍历后该位置糖果数与右边糖果数+1,取较大者,从而使该位置同时满足两个方向的要求;
最终得到的数组即为所需糖果数最少的分配方案;
代码
class Solution { public: int candy(vector& ratings) { vector result(ratings.size(), 1); for(int i=1; i ratings[i-1]){ result[i] = result[i-1]+1; } } for(int i=ratings.size()-2; i>=0; i--){ if(ratings[i] > ratings[i+1]){ result[i] = max(result[i], result[i+1]+1); } } int count = 0; for(int i=0; i 7. LeetCode 406. 根据身高重建队列
题目
思路
方法一(vector插入元素)
由于身高较小的人对身高较高的人参数无影响,则将数组按照身高从高到低进行排序,若身高相等则参数二较小的在前面;
按照数组从左向右遍历,设置vector结果数组,依次将每个人按照第二参数添加到vector数组;
方法二(list插入元素)(省时)
思路与方法一相同,唯一不同在于设置list结果链表;
由于使用vector是非常费时的,C++中vector(可以理解是一个动态数组,底层是普通数组实现的)如果插入元素大于预先普通数组大小,vector底部会有一个扩容的操作,即申请两倍于原先普通数组的大小,然后把数据拷贝到另一个更大的数组上。
所以使用vector(动态数组)来insert,是费时的,因此使用链表速度更快;
代码
方法一(vector插入元素)
class Solution { private: static bool mycompare(vectora, vector b){ if(a[0] == b[0]) return a[1]b[0]; } public: vector > reconstructQueue(vector >& people) { sort(people.begin(), people.end(), mycompare); vector >queue; for(int i=0; i 方法二(list插入元素)
class Solution { private: static bool mycompare(vectora, vector b){ if(a[0] == b[0]) return a[1]b[0]; } public: vector > reconstructQueue(vector >& people) { sort(people.begin(), people.end(), mycompare); list >queue; for(int i=0; i >::iterator it = queue.begin(); for(int j=0; j >result(queue.begin(), queue.end()); return result; } }; 8. LeetCode 452. 用最少数量的箭引爆气球
题目
在二维空间中有许多球形的气球。对于每个气球,提供的输入是水平方向上,气球直径的开始和结束坐标。由于它是水平的,所以纵坐标并不重要,因此只要知道开始和结束的横坐标就足够了。开始坐标总是小于结束坐标。
一支弓箭可以沿着 x 轴从不同点完全垂直地射出。在坐标 x 处射出一支箭,若有一个气球的直径的开始和结束坐标为 xstart,xend, 且满足 xstart ≤ x ≤ xend,则该气球会被引爆。可以射出的弓箭的数量没有限制。 弓箭一旦被射出之后,可以无限地前进。我们想找到使得所有气球全部被引爆,所需的弓箭的最小数量。
给你一个数组 points ,其中 points [i] = [xstart,xend] ,返回引爆所有气球所必须射出的最小弓箭数。
来源:力扣(LeetCode) 链接:力扣 著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
思路
将所有气球按照左边界从小到大进行排序,从左向右遍历数组,记录(保证本次射箭开始后至今所有气球均被引爆情况)射箭最大右边界;
若遍历至气球左边界大于射箭右边界,则需要增加射箭次数,并更新射箭最大右边界;
若遍历至气球右边界小于射箭右边界,为保证该气球被引爆,则需更改射箭右边界为该气球右边界;
代码
class Solution { private: static bool mycompare(const vector&a, const vector &b){ return a[0]>& points) { if(points.size() ==1 ) return 1; sort(points.begin(), points.end(), mycompare); int result = 1; for(int i=1; i points[i-1][1]){ result++; } else{ points[i][1] = min(points[i-1][1], points[i][1]); } } return result; } }; 9. LeetCode 435. 无重叠区间
题目
给定一个区间的集合 intervals ,其中 intervals[i] = [starti, endi] 。返回 需要移除区间的最小数量,使剩余区间互不重叠 。
来源:力扣(LeetCode) 链接:力扣 著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
思路
方法一(记录不交叉区间个数)
由于最右边界越小,为其它区间留下的空间越大,同时存在的区间数越多;
对区间集合按照右边界从小到大进行排序;
利用当前不交叉最右边界判断后面区间是否与前面区间重叠,并记录不交叉区间个数,同时更新最右边界;
用区间总数减去不交叉区间个数即为所求;
方法二(记录交叉区间个数)
将区间按照左边界从小到大进行排序,若左边界相等,则右边界较小的在前面;
记录目前不交叉区间最右边界,若遍历当前区间右边界小于不交叉最右边界,则交叉区间个数加1,并且更新不交叉区间最右边界;
若遍历当前区间左边界小于不交叉最右边界,则交叉区间个数加1;
否则其他情况就为遍历当前区间左边界大于等于不交叉最右边界,则更新不交叉最右边界;
代码
方法一
class Solution { private: static bool mycompare(const vector&a, const vector &b){ return a[1]>& intervals) { if(intervals.size() == 1) return 0; sort(intervals.begin(), intervals.end(), mycompare); int count = 1;// 用于记录不交叉集合个数 int pre_end = intervals[0][1]; for(int i=0; i 方法二
class Solution { private: static bool mycompare(const vector&a, const vector &b){ if(a[0] == b[0]){ return a[1]>& intervals) { if(intervals.size() == 1) return 0; int result = 0, pre_end; sort(intervals.begin(), intervals.end(), mycompare); pre_end = intervals[0][1]; for(int i=1; i 10. LeetCode 763. 划分字母区间
题目
字符串
S
由小写字母组成。我们要把这个字符串划分为尽可能多的片段,同一字母最多出现在一个片段中。返回一个表示每个字符串片段的长度的列表。思路
遍历字符串,记录每个字母最后出现位置;
设置begin用于记录每个段的起始下标,end用于记录每个段中各字母最后出现的最右下标;
重新遍历字符串,不断更新end使其为该段中字母最后出现下标,直至遍历下标到达end则完成一段的分割,记录段中字符个数,继续循环;
代码
class Solution { public: vectorpartitionLabels(string s) { int begin = 0, end; vector result; vector array(26, 0); for(int i=0; i 11. LeetCode 738. 单调递增的数字
题目
当且仅当每个相邻位数上的数字 x 和 y 满足 x <= y 时,我们称这个整数是单调递增的。
给定一个整数 n ,返回 小于或等于 n 的最大数字,且数字呈 单调递增 。
来源:力扣(LeetCode) 链接:力扣 著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
思路
设置count用于记录循环层数,flag用于记录最左面不满足相邻位数字时循环层数,temp_n用于记录最左面不满足相邻位数字时左边剩余数字,low用于记录相邻位较低位,high用于记录相邻位较高位;
整体相当于对n从右向左按位比较,当不满足时记录循环层数flag及左边剩余数字temp_n,至循环结束就可以找到最左面不满足条件的位置;
利用temp_n与flag将temp_n重新扩充至原来位数,减一后即为不满足单调递增的结果;
若flag为初始值0,则题目所给数字n满足单调递增,同时temp_n仍为初始值n则可以直接返回temp_n;
代码
class Solution { public: int monotoneIncreasingDigits(int n) { int count = 0, flag=0, temp_n = n, low, high; while(n){ low = n%10; n = n/10; high = n%10; count++; if(low < high){ flag = count; temp_n = n; n--; } } for(int i=0; i12. LeetCode 714. 买卖股票的最佳时机含手续费
题目
给定一个整数数组 prices,其中 prices[i]表示第 i 天的股票价格 ;整数 fee 代表了交易股票的手续费用。
你可以无限次地完成交易,但是你每笔交易都需要付手续费。如果你已经购买了一个股票,在卖出它之前你就不能再继续购买股票了。
返回获得利润的最大值。
注意:这里的一笔交易指买入持有并卖出股票的整个过程,每笔交易你只需要为支付一次手续费。
来源:力扣(LeetCode) 链接:力扣 著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
思路
设置一个买入的最低票价min,遍历股票价格数组,若股票价格小于min则更新min;
若股票价格大于等于min,但小于等于min+fee,则此时卖出不获利,继续循环;
若股票价格大于min+fee,此时可以获利,增加获利结果,但并不确定在此时卖出,更改min为该天股票价-fee,若后面出现比该min小,则确定在此次卖出,若后面出现比min+fee大的股票价格,则此次不卖出;
代码
class Solution { public: int maxProfit(vector& prices, int fee) { int min = prices[0]; int result = 0; for(int i=1; i =min && prices[i]<=min+fee){ //抛弃无法获利点 continue; } else{ //尝试在该点卖出股票,若后面出现新的股票买入点时则确定在该点卖出股票,否则若仍然该分支则为不在该点卖出股票; result += prices[i] - min -fee; min = prices[i] - fee; //用于后面卖出股票时,抵消多用的手续费 //此外,min用于后面寻找买入点,只有比该值小的股票价格才有可能获利 } } return result; } }; 13. LeetCode 968. 监控二叉树
题目
给定一个二叉树,我们在树的节点上安装摄像头。
节点上的每个摄影头都可以监视其父对象、自身及其直接子对象。
计算监控树的所有节点所需的最小摄像头数量。
思路
若使监控树所需最小摄像头数,则将每个叶子节点的父节点设置为摄像头;
构造travel函数用于返回遍历节点的状态,返回0表示未覆盖,返回1表示覆盖,返回2表示摄像头;
当遍历一个节点的左右节点都覆盖时,则返回0表示该节点没有必要安装摄像头,可由父节点设置摄像头将该节点覆盖;
当遍历一个节点的左右节点存在未覆盖时,则返回2表示设置摄像头,只有这样才能将子节点覆盖;
当遍历一个节点的左右节点存在摄像头时,则返回1表示覆盖,子节点的摄像头将本节点覆盖;
代码
/** * Definition for a binary tree node. * struct TreeNode { * int val; * TreeNode *left; * TreeNode *right; * TreeNode() : val(0), left(nullptr), right(nullptr) {} * TreeNode(int x) : val(x), left(nullptr), right(nullptr) {} * TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {} * }; */ class Solution { private: int result; //travel返回值为0表示未覆盖 //返回1表示覆盖 //返回2表示摄像头 int travel(TreeNode* node){ if(!node)return 1; int left = travel(node->left); int right = travel(node->right); if(left==1 && right==1){ return 0; } else if(left==0 || right==0){ result++; return 2; } else return 1; // else if(left==2 || right==2){ // return 1; // } } public: int minCameraCover(TreeNode* root) { result = 0; if(!root){ return result; } if(travel(root) == 0){ result++; } return result; } };