个人主页:@Sherry的成长之路
学习社区:Sherry的成长之路(个人社区)
专栏链接:练题
长路漫漫浩浩,万事皆有期待
1005. K 次取反后最大化的数组和 - 力扣(LeetCode)
先看第一种思路,由于数组中可能存在正数也可能存在负数,且数据大小不一,我们先使用排序函数将数组排一个序,然后用循环遍历,判断范围是用完全部的k次,我们排完序后,那么最小的数一定是在最前面的,无论它是正数最小还是说有负数的情况负数最小,反正一定是最小的数,那么我们直接让排完序后的最小数变为它本身的负数,然后我们紧接着再排一次序,这次的排序还是为了使本次最小的数排到前面,反复多次,直到用完k次机会。
class Solution {
public:
int largestSumAfterKNegations(vector<int>& nums, int k) {
sort(nums.begin(),nums.end());int sum=0;
if(nums.size()==0)return 0;
for(int i=1;i<=k;i++){
nums[0]=-nums[0];
sort(nums.begin(),nums.end());
}
for(int i=0;i<nums.size();i++)
sum+=nums[i];
return sum;
}
};
第二种方法要省效率的多,思路是将数组按照数据的绝对值排序,这样只需要排一次序,按照绝对值从大到小排序,我们在循环里就可以判断如果这个数是负数直接反转为正数,这样在k次数小的时候,我们可以优先翻转绝对值大的负数,如果k次数没有用完一直判断该数组,直到数组内没有负数为止,目的就是将负数全变为正数,这样肯定和就大了,然后如果k还有次数,那么判断是否k是奇数,如果是奇数那么就将数组中最小的那个也就是该数组最后一个数据,因为是按照绝对值大小排的序,将它变为负数,k如果是偶数直接不用管了,因为偶数k随便翻转一个数k次得到的还是其本身,对结果毫无影响。
class Solution {
public:
static bool cmp(int a,int b){
return abs(a)>abs(b);//从大到小按绝对值排序,方便k先把绝对值大的负数变成正数
}
int largestSumAfterKNegations(vector<int>& nums, int k) {
sort(nums.begin(),nums.end(),cmp);int sum=0;
for(int i=0;i<nums.size();i++){
if(k&&nums[i]<0){
nums[i]=-nums[i];
k--;
}
}
if(k%2==1)nums[nums.size()-1]*=-1;
for(int a:nums) sum+=a;
return sum;
}
};
这种方法优点在于数组只需要排序一次,避免了每次都需要排序,而且在数据中没有负数时候,k为奇数时候仅仅翻转一次最小的数,省去了反复翻转但是却不影响结果的无用消耗,但是两者思路都是局部推整体,从而实现了整体的最大和。
加油站是一个有点难度的题,没做过很难。重要的我认为不要过分纠结它那奇怪的规则,耗油数组给的是到下一个油站耗油数,而并不是到本站的耗油数,这一点我觉得很奇怪。但是实际上我们并不需要对此做调整,因为我们是要走一圈的,根据题目的示例也可以看出来,即使我们是从3号油箱走的,那耗油的下标为3的耗油量也需要减去,虽然说它代表的是到第四号油箱的耗油量,说到这里不难想清楚,到最后一个油箱就是也就是减去当前的耗油量,代表的就是重新回到下标为3的时候。
解题思路是,因为要走一周,所以我们每一个加油站是一定会挨个走一次的,那么如果总用油量大于总存油量的话,就说明了从哪一个加油站走,都不可能走完一周了,所以返回-1。那么是如何体现局部最优推出全局最优的呢?我们设置两个变量一个存储总存油减总耗油,一个存储从起始位置走到现在的存油-耗油。如果加到当前的给油量减去耗油量加到的总和里,总和小于0了,说明当前选择的有错误,不应该从那个起始位置出发,直接将位置跳到i+1,也就是当前位置+1处开始重新计算,并且使当前记录历史剩油量的和变成0。这种思路是什么道理呢?是因为刚好走到此处,我们的油发现不够了,那么应该从新的位置重新走了,因为刚才最后走的位置耗油太多了,应该避免过早接触油耗过大的节点,应该是有足够的油了我们再考虑走到那一步,所以我们直接从刚才那步的下一步走,当前和赋值给0.
那如果是前面的数组里前部分耗油大,但是后面的部分耗油很少啊,为什么不从初始位置的下一个走,而是结束位置后面走呢?如果是这种情况不就错过了?实际这也间接的可以说明,我们之前[0,i]的那一段后面的加油量也不够,如果足够的话并不会出现这种情况,所以我们在起始位置后面接着遍历也是差不多的情况,而且并不会错过,因为我们无论如何是要走完一周的,题目已经说了如果有答案,一定是唯一的,所以我们肯定要先从存油大消耗少的位置出发,这样也会增加一些找到答案的可能性,当然具体是否能够实现,肯定是要看总加油数量减去总耗油数量。
class Solution {
public:
int canCompleteCircuit(vector<int>& gas, vector<int>& cost) {
int start=0;
int zongsum=0;int sum=0;
for(int i=0;i<gas.size();i++){
zongsum+=gas[i]-cost[i];
sum+=gas[i]-cost[i];
if(sum<0){
start=i+1;sum=0;//正好走到当前的i了油不够了。所以应该避免过早走到此处应从i+1开始走,把油积累起来后再走该处。
}
}
if(zongsum<0)return -1;
return start;
}
};
首次遇到的贪心困难题,题目要求是要统计每个小孩能拿多少糖,起初每个小孩只能拿一颗,但是如果这个孩子比他相邻的孩子评分高那么他就可以拿到比相邻孩子多一颗的糖果,这里要注意是比他相邻两个孩子,拿到最多糖果那个还要多一颗,这点要注意。不过要生两个孩子分数相等的话,他只拿一颗糖果。
这道题的解题思路实际上是先判断从左往右或者从右往左一趟顺序下的糖果数目,然后再从另一相反的方向再判断一次,不要两边一起判断,这样代码很难写出,这里是什么意思呢?就比如说{2,5,3}5比2大比3大,我们不要一次性就判断出来中间孩子应该得到几颗糖,而是要判断一个方向,再判断一个方向,最后再得出结果。
这里我们先来判断后面的评分是否大于当前评分的情况,这里的判断方向是从前向后遍历,遍历方向不是随机的,是按照如何判断来写的,这一点是经过合理的推导的。因为我们要比较的是右边数据是否大于左边,我们知道右边数据如果比当前数据大,那么他获得当前这个小孩子的糖果数+1,那么当前这个孩子的糖果数受什么影响呢?受前面的那个孩子糖果数量影响,这一点很重要,所以我们用从前向后遍历的顺序,因为这样会保证我们想要的数据,都是已经确定下来的。从前向后遍历,前面的糖果数肯定是确定下来的。
但是如果是判断左孩子是否大于当前孩子呢?为啥这还要重复判断一次呢?{2,5,3}不就是判断一次就好了嘛?那如果是{2,5,3,2}呢?只判断一个方向的话,明明3那个位置孩子得到的糖果是大于1的,但是根据上面的判断右边孩子没有大于当前孩子的分数,那么3这个分数就只拿到一颗糖果,但是实际上它的分数比相邻的2要大,他拿到2颗糖果是没有问题的,这也是为什么我们要对相反方向再做一次判断。说完了这个原因,下一个值的注意的点是在第二次的遍历中,如果发现左孩子大于了右孩子,那么要比较右孩子的糖果数+1得到的多还是左孩子本身拥有的糖果数量多,取最大的那个,因为如果数据序列像{2,5,6,1,1}这样的,在第一次遍历比较时候6分的孩子就拿到了三块糖果,但是从后向前遍历时,他只能拿到两颗,那么肯定是不行的,他拿到三颗是正确的,为了避免中间数比左右两边都大的时候,出现一些意想不到的错误,所以我们第二次遍历要取两者最大数,作为获得糖果数量,更新数组。
class Solution {
public:
int candy(vector<int>& ratings) {
vector<int>candys(ratings.size(),1);int count=0;
for(int i=1;i<ratings.size();i++){
if(ratings[i]>ratings[i-1])
candys[i]=candys[i-1]+1;
}
for(int i=ratings.size()-1;i>0;i--){
if(ratings[i-1]>ratings[i])
candys[i-1]=max(candys[i-1],candys[i]+1);
}
for(int i=0;i<candys.size();i++)count+=candys[i];
return count;
}
};
今天我们完成了K 次取反后最大化的数组和、加油站、分发糖果三道题,相关的思想需要多复习回顾。接下来,我们继续进行算法练习。希望我的文章和讲解能对大家的学习提供一些帮助。
当然,本文仍有许多不足之处,欢迎各位小伙伴们随时私信交流、批评指正!我们下期见~