算法学习Day34加油站、发糖果
Day34加油站、发糖果
By HQWQF 2024/01/18
笔记
1005.K次取反后最大化的数组和
给定一个整数数组 A,我们只能用以下方法修改该数组:我们选择某个索引 i 并将 A[i] 替换为 -A[i],然后总共重复这个过程 K 次。(我们可以多次选择同一个索引 i。)
以这种方式修改数组后,返回数组可能的最大和。
示例 1:
- 输入:A = [4,2,3], K = 1
- 输出:5
- 解释:选择索引 (1,) ,然后 A 变为 [4,-2,3]。
示例 2:
- 输入:A = [3,-1,0,2], K = 3
- 输出:6
- 解释:选择索引 (1, 2, 2) ,然后 A 变为 [3,1,0,2]。
贪心算法
为了让数组最大和尽可能大,我们应该选择让绝对值大的负数优先翻转,如果把所有负数都翻转了还有翻转次数k没有用完,则看剩下的k是否为双数,为双数相当于不用翻转,为单数就翻转最小的正数以最小化损失。
这里找到绝对值大的负数和找到最小的正数两个操作可以使用将数组按照绝对值从大到小排列来实现。
class Solution {
static bool cmp(int a, int b) {
return abs(a) > abs(b);
}
public:
int largestSumAfterKNegations(vector<int>& A, int K) {
sort(A.begin(), A.end(), cmp); //按照绝对值从大到小
for (int i = 0; i < A.size(); i++) { // 绝对值大的负数优先翻转
if (A[i] < 0 && K > 0) {
A[i] *= -1;
K--;
}
}
if (K % 2 == 1) A[A.size() - 1] *= -1; // 剩下单数就翻转最小的正数以最小化损失。
int result = 0;
for (int a : A) result += a; // 计算总合
return result;
}
};
134. 加油站
在一条环路上有 N 个加油站,其中第 i 个加油站有汽油 gas[i] 升。
你有一辆油箱容量无限的的汽车,从第 i 个加油站开往第 i+1 个加油站需要消耗汽油 cost[i] 升。你从其中的一个加油站出发,开始时油箱为空。
如果你可以绕环路行驶一周,则返回出发时加油站的编号,否则返回 -1。
说明:
- 如果题目有解,该答案即为唯一答案。
- 输入数组均为非空数组,且长度相同。
- 输入数组中的元素均为非负数。
示例 1: 输入:
- gas = [1,2,3,4,5]
- cost = [3,4,5,1,2]
输出: 3 解释:
- 从 3 号加油站(索引为 3 处)出发,可获得 4 升汽油。此时油箱有 = 0 + 4 = 4 升汽油
- 开往 4 号加油站,此时油箱有 4 - 1 + 5 = 8 升汽油
- 开往 0 号加油站,此时油箱有 8 - 2 + 1 = 7 升汽油
- 开往 1 号加油站,此时油箱有 7 - 3 + 2 = 6 升汽油
- 开往 2 号加油站,此时油箱有 6 - 4 + 3 = 5 升汽油
- 开往 3 号加油站,你需要消耗 5 升汽油,正好足够你返回到 3 号加油站。
- 因此,3 可为起始索引。
全局法
如果gas的总和小于cost总和,那么无论从哪里出发,一定是跑不了一圈的。
rest[i] = gas[i]-cost[i]为一天剩下的油,0节点开始计算当前汽油累加值到最后一站,如果累加值没有出现负数,说明从0出发,油就没有断过,那么0就是起点。(从节点出发是否能走完取决于路程中汽油最少的时候有多少。)
如果累加值的最小值是负数,汽车就要从非0节点出发,说明要走完一圈,汽车在到达0节点前必须有足够大于累加值最小值绝对值的汽油,所以我们从0节点之前往前计算从该点出发到0节点的累加汽油,有足够的那个节点就是目标节点。
class Solution {
public:
int canCompleteCircuit(vector<int>& gas, vector<int>& cost) {
int curSum = 0;
int min = INT_MAX; // 从起点出发,油箱里的油量最小值
for (int i = 0; i < gas.size(); i++) {
int rest = gas[i] - cost[i];
curSum += rest;
if (curSum < min) {
min = curSum;
}
}
if (curSum < 0) return -1; // gas的总和小于cost总和
if (min >= 0) return 0; // 从0出发就行
// 从0出发不行,往0前找
for (int i = gas.size() - 1; i >= 0; i--) {
int rest = gas[i] - cost[i];
min += rest;
if (min >= 0) {
return i;
}
}
return -1;
}
};
贪心法
一个事实是,首先如果每个站剩下的汽油加起来小于0那一定跑不了一圈,我们可以利用这个确保我们要判断的一定是能跑完的。
然后从0开始跑,过程中记录当前汽油,在n节点当前汽油小于0了,不但说明0出发无法跑完。而且从0到n的每个节点都无法跑完。
因为0能到n,说明0到0~n之间的所有节点时都是有剩余汽油的,而在0~n中其他节点出发是没有剩余汽油,更加不可能跑完。
此时我们可以从下一个节点开始考虑了,但是我们没必要利用这个事实去减少暴力解法的工作量,而是假定当前循环的下一个节点i+1就是目标节点,本质是去排除无法跑完的节点,因为我们可以利用上面的事实去确保一定能跑完,如果一定能跑完,排除过一遍之后最后的假定目标节点就是目标节点。
class Solution {
public:
int canCompleteCircuit(vector<int>& gas, vector<int>& cost) {
int curSum = 0;
int totalSum = 0;
int tmpStart = 0;
for(int i = 0 ;i < gas.size();i++)
{
curSum += gas[i] - cost[i];
totalSum += gas[i] - cost[i];
if(curSum < 0){curSum = 0;tmpStart = i + 1;}
}
if(totalSum < 0){return -1;}
return tmpStart ;
}
};
135. 分发糖果
老师想给孩子们分发糖果,有 N 个孩子站成了一条直线,老师会根据每个孩子的表现,预先给他们评分。
你需要按照以下要求,帮助老师给这些孩子分发糖果:
- 每个孩子至少分配到 1 个糖果。
- 相邻的孩子中,评分高的孩子必须获得更多的糖果。
那么这样下来,老师至少需要准备多少颗糖果呢?
示例 1:
- 输入: [1,0,2]
- 输出: 5
- 解释: 你可以分别给这三个孩子分发 2、1、2 颗糖果。
普通法
首先我们初始化一个candy数组用以了解每个孩子的糖果情况,由于大家必须至少有一个所以初始值是1,同时定义result变量记录糖果数。
然后我们遍历孩子去检测当前分配是否合理,不合理就给孩子加糖果,并且增加result变量。
但是这样一遍后其实不能保证分配合理,因为前面分配合理后,后面由于分配不合理某个孩子增加糖果后,会破坏前面的分配合理性。
如:[1,2,87,87,87,2,1]在倒数第2、3个孩子时发生的情况。
所以我们需要从尾开始反向遍历一遍检测一遍,就可以了。
class Solution {
public:
int candy(vector<int>& ratings) {
int result = ratings.size();
vector<int> candy(ratings.size(),1);
for(int i = 0;i < ratings.size()-1;i++)
{
if(ratings[i] > ratings[i+1] && candy[i] <= candy[i+1])
{
int tmp = candy[i];
candy[i] = candy[i+1] + 1;
result += candy[i] - tmp;
}
else if(ratings[i] < ratings[i+1] && candy[i] >= candy[i+1])
{
int tmp = candy[i+1];
candy[i+1] = candy[i] + 1;
result += candy[i+1] - tmp;
}
}
reverse(ratings.begin(),ratings.end());
reverse(candy.begin(),candy.end());
for(int i = 0;i < ratings.size()-1;i++)
{
if(ratings[i] > ratings[i+1] && candy[i] <= candy[i+1])
{
int tmp = candy[i];
candy[i] = candy[i+1] + 1;
result += candy[i] - tmp;
}
else if(ratings[i] < ratings[i+1] && candy[i] >= candy[i+1])
{
int tmp = candy[i+1];
candy[i+1] = candy[i] + 1;
result += candy[i+1] - tmp;
}
}
return result;
}
};
贪心法
局部最优:只要右边评分比左边大,右边的孩子就多一个糖果,全局最优:相邻的孩子中,评分高的右孩子获得比左边孩子更多的糖果。局部最优可以推出全局最优。右边评分比左边小也同理。
我们可以一次从前向后检测右边评分比左边大则增加右边,一次从后向前检测左边评分比右边大则增加左边。
本质在于,这样增加的糖果不会影响之前的检测结果。
class Solution {
public:
int candy(vector<int>& ratings) {
vector<int> candyVec(ratings.size(), 1);
// 从前向后
for (int i = 1; i < ratings.size(); i++) {
if (ratings[i] > ratings[i - 1]) candyVec[i] = candyVec[i - 1] + 1;
}
// 从后向前
for (int i = ratings.size() - 2; i >= 0; i--) {
if (ratings[i] > ratings[i + 1] ) {
candyVec[i] = max(candyVec[i], candyVec[i + 1] + 1);
}
}
// 统计结果
int result = 0;
for (int i = 0; i < candyVec.size(); i++) result += candyVec[i];
return result;
}
};