LeetCode 1005.K次取反后最大化的数组和
思路:
取反后让和最大,很容易想到的策略就是如果有负数,先给负数取反,如果没有负数,则取反最小的那个数,这种策略其实就是贪心法,局部的最优可以导致整体最优,所以这题的难点就是如何维持这种策略。
首先考虑给负数取反,最先取反的负数肯定是最小的那个数,这样取反后才是最大的,所以首先要给数组排序,为了找到绝对值最大的数。排序后遍历数组,每遇到一个负数就给它取反然后把k减一,k为0后直接中断循环,因为已经不需要再取反了,随后返回数组的和即可。
如果遍历完数组后k还是不为0,则表示所有的负数已经被取反了,这个时候需要反转绝对值最小的那个数,既然我们已经按绝对值大从大到小排序了,那么只要把数组最末尾的数反转剩余次数即可。
代码:
class Solution {
public:
static bool cmp(int a, int b)
{
return abs(a)>abs(b)?true:false;
}
int largestSumAfterKNegations(vector<int>& nums, int k) {
// 按绝对值从大到小排序
sort(nums.begin(), nums.end(), cmp);
for (int i = 0; i < nums.size(); i++)
{
// 遇到负数则反转同时k--
if (nums[i] < 0)
{
nums[i] = -nums[i];
k--;
}
// 反转次数用光了中断循环
if (k == 0)
break;
}
// 反复反转最小的数消耗剩余的k
while (k > 0)
{
k--;
nums[nums.size() - 1] = - nums[nums.size() - 1];
}
int sum = 0;
for (int i = 0; i < nums.size(); i++)
{
sum += nums[i];
cout << nums[i] << " ";
}
return sum;
}
};
LeetCode 134. 加油站
思路:
这道题目还是十分有难度的,即使是暴力法从每个加油站模拟跑一次的算法也不是很好写,需要处理很多边界条件,所以用贪心法最合适。贪心法最难的地方不是代码而是思路,如何制定贪心策略才是最关键的地方。
仔细观察题目我们注意到,想要完整的跑完一圈,gas的和是一定大于等于cost的和的,如果小于,则无论如何也不可能跑完一圈。我们可以用gas的每个下标的元素减去cost对应下标的元素,得到的为每站能获取的净汽油量net数组,该数组的每个元素之和一定是大于等于0的。同时,我们也不可能在net为负的时候出发。根据以上信息制定贪心策略:遍历net数组,用一个变量start表示当前出发站的位置,同时用另一个变量curSum来记录从start出发时的净汽油量,遍历的过程中累计net数组的元素到curSum,如果在某一个位置curSum为负了,表示从start开始无法跑完一整圈,所以更新start为当前位置的下一个位置并重置curSum。因为curSum会被重置,为了计算所有加油站的净汽油量,我们还需要一个netSum变量来保存net数组的和。最后,如果netSum为非负数,则保证了从某个位置开始一定可以跑一个循环,curSum非负则保证了一定可以跑一个循环的开始位置start,即为最终答案。
代码:
class Solution {
public:
int canCompleteCircuit(vector<int>& gas, vector<int>& cost) {
vector<int> net(gas.size());
for (int i = 0; i < gas.size(); i++)
{
net[i] = gas[i] - cost[i];
}
int netSum = 0;
int curSum = 0;
int start = 0;
for (int i = 0; i < net.size(); i++)
{
// 累计net
netSum += net[i];
curSum += net[i];
// 起始点在curSum小于0的后一位
if (curSum < 0)
{
start = i + 1;
curSum = 0;
}
}
// netSum小于0则无论从哪里开始都不可能跑完一圈
if (netSum < 0)
return -1;
return start;
}
};
LeetCode 135. 分发糖果
思路:
这道题目理解起来也比较困难,因为很难同时处理好左边和右边,所以不妨简化思路,把左边和右边分开处理。我们需要两个数组left和right,分别记录左边大于右边和右边大于左边的情况。两个数组都初始化为1,因为我们最少要给1颗糖给每人。right数组从左往右遍历ratings,只记录右边大于左边的情况,当右边大于左边时,右边获得比左边多一个的糖果,也就是right[i]=right[i-1]+1。右边小于左边的情况则不需要处理。left数组从右往左遍历ratings,只记录左边大于右边的情况。为什么要从右往左遍历呢?因为左边的结果依赖于右边的结果,也就是left[i-1]=left[i]+1。如果和right数组一样从左往右遍历,我们就不知道left[i]的情况了。
当我们获得了left和right两个数组后,就可以结合两者计算出实际上每个人应该获得的最少糖果数。我们需要在两个数组中选取较大的那个值表示每个人最终获得的糖果数,因为这样才能保证既满足left数组左大于右的情况又满足right数组右大于左的情况。然后再把每个人获得的糖果数相加得到总和。
代码:
class Solution {
public:
int candy(vector<int>& ratings) {
// 记录右边比左边大的情况
vector<int> right(ratings.size(),1);
for (int i = 1; i < ratings.size(); i++)
{
if (ratings[i] > ratings[i-1])
right[i] = right[i-1] + 1;
}
// 记录左边比右边大的情况
// 因为是基于右边的数量所以要从后往前
vector<int> left(ratings.size(),1);
for (int i = ratings.size() - 1; i > 0; i--)
{
if (ratings[i] < ratings[i-1])
left[i-1] = left[i] + 1;
}
int sum = 0;
for (int i = 0; i < left.size(); i++)
sum += left[i]>right[i]?left[i]:right[i];
return sum;
}
};