专题九 贪心算法
一、K次取反后最大化的数组和
1.题目
Leetcode:第 1005 题
给你一个整数数组 nums
和一个整数 k
,按以下方法修改该数组:
- 选择某个下标
i
并将nums[i]
替换为-nums[i]
。
重复这个过程恰好 k
次。可以多次选择同一个下标 i
。
以这种方式修改数组后,返回数组 可能的最大和 。
示例 1:
输入:nums = [4,2,3], k = 1 输出:5 解释:选择下标 1 ,nums 变为 [4,-2,3] 。
示例 2:
输入:nums = [3,-1,0,2], k = 3 输出:6 解释:选择下标 (1, 2, 2) ,nums 变为 [3,1,0,2] 。
示例 3:
输入:nums = [2,-3,-1,5,-4], k = 2 输出:13 解释:选择下标 (1, 4) ,nums 变为 [2,3,-1,5,4] 。
2.解题思路
使用贪心算法解决K次取反后最大化的数组和问题。
在 largestSumAfterKNegations
函数中,我们首先使用 sort
函数和一个自定义的比较函数 cmp
对数组 nums
进行排序。这个比较函数 cmp
使用 abs
函数比较两个数的绝对值,确保绝对值较大的负数排在前面。接着,我们遍历数组,将前 k
个负数取反。如果 k
是奇数,且遍历结束后仍有剩余的 k
,则将最后一个元素取反。最后,我们初始化一个变量 result
来存储最终的和,并遍历排序并取反后的数组 nums
,将所有元素相加得到最终结果。这个方法利用了贪心算法的思想,即在每一步选择中都尝试达到最优化的结果,从而希望导致结果是全局最优的。
3.实现代码
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
class Solution {
// 定义一个静态比较函数,用于自定义排序规则
static bool cmp(int a, int b) {
return abs(a) > abs(b); // 返回绝对值比较的结果,确保绝对值大的负数排在前面
}
public:
// largestSumAfterKNegations 函数用于计算最大约和
int largestSumAfterKNegations(vector<int>& nums, int k) {
sort(nums.begin(), nums.end(), cmp); // 使用自定义的比较函数对数组进行排序
// 遍历数组
for (int i = 0; i < nums.size(); i++) {
if (nums[i] < 0 && k > 0) {// 如果当前元素是负数且 k 大于 0
nums[i] = nums[i] * (-1);// 将当前元素取反,并减少 k 的值
k--;
}
}
// 如果 k 是奇数,最后一个元素取反
if (k % 2 == 1) {
nums[nums.size() - 1] *= -1;
}
int result = 0;// 初始化结果变量
for (int a : nums) {
result += a;// 遍历排序后的数组,计算所有元素的和
}
return result; // 返回计算得到的结果
}
};
//测试
int main()
{
Solution p;
vector<int> nums = { 4,2,3 };
int k = 1;
int result = p.largestSumAfterKNegations(nums,k);
cout << "nums数组可能的最大和:" << result << endl;
cout << endl;
return 0;
}
二、加油站
1.题目
Leetcode:第 134 题
在一条环路上有 n
个加油站,其中第 i
个加油站有汽油 gas[i]
升。
你有一辆油箱容量无限的的汽车,从第 i
个加油站开往第 i+1
个加油站需要消耗汽油 cost[i]
升。你从其中的一个加油站出发,开始时油箱为空。
给定两个整数数组 gas
和 cost
,如果你可以按顺序绕环路行驶一周,则返回出发时加油站的编号,否则返回 -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 可为起始索引。
示例 2:
输入: gas = [2,3,4], cost = [3,4,3] 输出: -1 解释: 你不能从 0 号或 1 号加油站出发,因为没有足够的汽油可以让你行驶到下一个加油站。 我们从 2 号加油站出发,可以获得 4 升汽油。 此时油箱有 = 0 + 4 = 4 升汽油 开往 0 号加油站,此时油箱有 4 - 3 + 2 = 3 升汽油 开往 1 号加油站,此时油箱有 3 - 3 + 3 = 3 升汽油 你无法返回 2 号加油站,因为返程需要消耗 4 升汽油,但是你的油箱只有 3 升汽油。 因此,无论怎样,你都不可能绕环路行驶一周。
2.解题思路
(1)一般解法
(2)使用贪心算法解决加油站问题。
在 canCompleteCircuit
函数中,我们使用两个变量 curSum
和 totalSum
分别来记录当前窗口的剩余油量和整个数组的总剩余油量。同时,我们使用变量 start
来记录可能的环绕起始位置。循环遍历每个油量和消耗的位置,我们将当前位置的油量减去消耗,并累加到 curSum
和 totalSum
中。如果 curSum
小于 0,这意味着从当前起始位置开始的窗口内无法环绕一圈,因此我们将 start
移动到下一个位置,并将 curSum
重置为 0,以便开始计算新的窗口。在循环结束后,我们检查 totalSum
,如果它小于 0,则表示在整个数组范围内无法环绕一圈,因此返回 -1
。否则,我们返回 start
作为可能的环绕起始位置。这种方法利用了贪心算法的思想,通过维护一个滑动窗口的剩余油量来找到可能的环绕起始位置。
3.实现代码
#include <iostream>
#include <vector>
using namespace std;
// 一、一般解法
class Solution1 {
public:
// canCompleteCircuit 函数用于判断是否能够环绕一圈
int canCompleteCircuit(vector<int>& gas, vector<int>& cost) {
// 遍历油量和消耗的对应位置
for (int i = 0; i < cost.size(); i++) {
int rest = gas[i] - cost[i];// 计算当前位置的剩余油量
int index = (i + 1) % cost.size();// 初始化下一个检查的位置为当前位置后的第一个位置
// 模拟从当前位置 i 开始环绕行驶
while (rest > 0 && index != i) {
// 更新剩余油量,如果下一个位置的油量加上剩余油量大于等于消耗,则继续前进
rest += gas[index] - cost[index];
// 移动到下一个位置
index = (index + 1) % cost.size();
}
// 如果从位置 i 开始能够环绕一圈回到起始位置,并且剩余油量非负,则返回起始位置
if (rest >= 0 && index == i) return i;
}
return -1;// 如果没有找到可以环绕一圈的起始位置,返回 -1
}
};
// 二、贪心算法
class Solution2 {
public:
// canCompleteCircuit 函数用于判断是否能够环绕一圈
int canCompleteCircuit(vector<int>& gas, vector<int>& cost) {
int curSum = 0; // 初始化当前窗口的剩余油量
int totalSum = 0; // 初始化整个数组的总剩余油量
int start = 0; // 初始化可能的起始位置
// 遍历油量和消耗的对应位置
for (int i = 0; i < gas.size(); i++) {
curSum += gas[i] - cost[i];// 将当前位置的油量和消耗相减,并加到当前窗口的剩余油量上
totalSum += gas[i] - cost[i];// 将当前位置的油量和消耗相减,并加到总剩余油量上
// 如果当前窗口的剩余油量小于 0,说明当前窗口内无法环绕一圈
if (curSum < 0) {
start = i + 1;// 重置起始位置为当前位置之后,即窗口滑动到下一个位置
curSum = 0;// 重置当前窗口的剩余油量为 0
}
}
// 如果整个数组的总剩余油量小于 0,说明整个数组内无法环绕一圈
if (totalSum < 0) return -1;
return start;// 返回可能的环绕起始位置
}
};
//测试
int main()
{
Solution2 p;
vector<int> gas = { 1,2,3,4,5 };
vector<int> cost = { 3,4,5,1,2 };
int result = p.canCompleteCircuit(gas, cost);
cout << "可能的环绕起始位置:" << result << endl;
cout << endl;
return 0;
}
三、分发糖果
1.题目
Leetcode:第 135 题
n
个孩子站成一排。给你一个整数数组 ratings
表示每个孩子的评分。
你需要按照以下要求,给这些孩子分发糖果:
- 每个孩子至少分配到
1
个糖果。 - 相邻两个孩子评分更高的孩子会获得更多的糖果。
请你给每个孩子分发糖果,计算并返回需要准备的 最少糖果数目 。
示例 1:
输入:ratings = [1,0,2] 输出:5 解释:你可以分别给第一个、第二个、第三个孩子分发 2、1、2 颗糖果。
示例 2:
输入:ratings = [1,2,2] 输出:4 解释:你可以分别给第一个、第二个、第三个孩子分发 1、2、1 颗糖果。 第三个孩子只得到 1 颗糖果,这满足题面中的两个条件。
2.解题思路
使用贪心算法解决分发糖果问题。
在 candy
函数中,我们首先创建了一个与 ratings
数组等长的 candyVec
数组,所有元素初始为 1,表示每个孩子至少得到 1 个糖果。
然后,我们进行两次遍历:
-
第一次从前向后遍历
ratings
数组,如果一个孩子的评分高于他左边的孩子,那么他的糖果数量应该比左边的孩子多一个。 -
第二次从后向前遍历
ratings
数组,如果一个孩子的评分高于他右边的孩子,那么他的糖果数量应该取右边孩子糖果数量加一和当前数量中的较大值。最后,我们遍历candyVec
数组,将所有孩子的糖果数量累加起来,得到总共需要的糖果数量,并返回这个结果。这种方法利用了贪心算法的思想,通过维护一个糖果数组来动态地计算每个孩子应该得到的糖果数量。
3.实现代码
#include <iostream>
#include <vector>
using namespace std;
class Solution {
public:
// candy 函数用于计算分配糖果的最少数量
int candy(vector<int>& ratings) {
// 初始化一个与 ratings 大小相同的数组 candyVec,所有元素初始为 1
// 这个数组将用于存储每个孩子应该得到的糖果数量
vector<int> candyVec(ratings.size(), 1);
// 从 ratings 数组的第二个元素开始向前遍历
for (int i = 1; i < ratings.size(); i++) {
if (ratings[i] > ratings[i - 1]) { // 如果当前孩子的评分高于他左边的孩子
candyVec[i] = candyVec[i - 1] + 1;// 那么当前孩子的糖果数量应该比左边的孩子多一个
}
}
// 从 ratings 数组的倒数第二个元素开始向前遍历
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;// 初始化结果变量 result,用于计算总共需要的糖果数量
// 遍历 candyVec 数组,将所有孩子的糖果数量累加到 result 中
for (int i = 0; i < candyVec.size(); i++) {
result += candyVec[i];
}
return result;// 返回总共需要的糖果数量
}
};
//测试
int main()
{
Solution p;
vector<int> ratings = { 1, 0, 2 };
int result = p.candy(ratings);
cout << "总共需要的糖果数量:" << result << endl;
cout << endl;
return 0;
}
ps:以上皆是本人在探索算法旅途中的浅薄见解,诚挚地希望得到各位的宝贵意见与悉心指导,若有不足或谬误之处,还请多多指教。