1.跳跃游戏(55题)
题目描述:
给定一个非负整数数组,你最初位于数组的第一个位置。
数组中的每个元素代表你在该位置可以跳跃的最大长度。
判断你是否能够到达最后一个位置。
示例 1:
- 输入: [2,3,1,1,4]
- 输出: true
- 解释: 我们可以先跳 1 步,从位置 0 到达 位置 1, 然后再从位置 1 跳 3 步到达最后一个位置。
贪心算法: 跳跃覆盖范围究竟可不可以覆盖到终点,
每次移动取最大跳跃步数(得到最大的覆盖范围),每移动一个单位,就更新最大覆盖范围。
贪心算法局部最优解:每次取最大跳跃步数(取最大覆盖范围),整体最优解:最后得到整体最大覆盖范围,看是否能到终点。
i 每次移动只能在 cover 的范围内移动,每移动一个元素,cover 得到该元素数值(新的覆盖范围)的补充,让 i 继续移动下去。cover 每次只取 max(该元素数值补充后的范围, cover 本身范围)如果 cover 大于等于了终点下标,直接 return true 就可以了。
cpp
class Solution {
public:
bool canJump(vector<int>& nums) {
int cover = 0;//定义一个覆盖范围
if(nums.size() == 1)return true;//如果只有一个元素我们直接就返回正确
//这里需要注意i不能超过cover的覆盖范围,在这里遍历
for(int i = 0;i <= cover;i++){
cover = max(i + nums[i],cover);//我们更新下标范围,不断递归覆盖范围里的i和数值之和比大小
if(cover >= nums.size() - 1)return true;//如果可以覆盖到最后,返回正确
}
return false;
}
};
- 时间复杂度: O(n)
- 空间复杂度: O(1)
2.跳跃游戏 II(45题)
题目描述:
给定一个非负整数数组,你最初位于数组的第一个位置。
数组中的每个元素代表你在该位置可以跳跃的最大长度。
你的目标是使用最少的跳跃次数到达数组的最后一个位置。
示例:
- 输入: [2,3,1,1,4]
- 输出: 2
- 解释: 跳到最后一个位置的最小跳跃数是 2。从下标为 0 跳到下标为 1 的位置,跳 1 步,然后跳 3 步到达数组的最后一个位置。
说明: 假设你总是可以到达数组的最后一个位置。
贪心算法:
要从覆盖范围出发,不管怎么跳,覆盖范围内一定是可以跳到的,以最小的步数增加覆盖范围,覆盖范围一旦覆盖了终点,得到的就是最少步数!需要统计两个覆盖范围,当前这一步的最大覆盖和下一步最大覆盖
cpp
class Solution {
public:
int jump(vector<int>& nums) {
if(nums.size() == 1)return 0;
int cur = 0;//当前最大覆盖范围
int next = 0;//下一个最大最大覆盖范围
int result = 0;
//这里注意i的范围
for(int i = 0;i < nums.size();i++){
next = max(i + nums[i],next);//覆盖范围的更新
//如果遍历到最大当前范围
if(i == cur){
result++;//我们要记录结果
cur = next;//把最大范围更新成下一个最大范围
if(next >= nums.size()-1)break;//如果满足条件跳出
}
}
return result;
}
};
- 时间复杂度: O(n)
- 空间复杂度: O(1)
3.K次取反后最大化的数组和(1005题)
题目描述:
给定一个整数数组 A,我们只能用以下方法修改该数组:我们选择某个索引 i 并将 A[i] 替换为 -A[i],然后总共重复这个过程 K 次。(我们可以多次选择同一个索引 i。)
以这种方式修改数组后,返回数组可能的最大和。
示例 1:
- 输入:A = [4,2,3], K = 1
- 输出:5
- 解释:选择索引 (1,) ,然后 A 变为 [4,-2,3]。
贪心算法:
那么本题的解题步骤为:
- 第一步:将数组按照绝对值大小从大到小排序,注意要按照绝对值的大小
- 第二步:从前向后遍历,遇到负数将其变为正数,同时K--
- 第三步:如果K还大于0,那么反复转变数值最小的元素,将K用完
- 第四步:求和
cpp
class Solution {
//自定义一个函数实现绝对值大小的排序且是倒序
static bool cmp(int a,int b){
return abs(a) > abs(b);
}
public:
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 && k > 0){
nums[i] *= -1;//把数字反转
k--;//反转次数
}
}
//当全部负数转换为正数,正数情况下对最后一个数进行多次的反转
if(k % 2 == 1)nums[nums.size()-1] *= -1;
int result = 0;
for(int a : nums){
result += a;//加和操作
}
return result;
}
};
- 时间复杂度: O(nlogn)
- 空间复杂度: O(1)
4.加油站(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 可为起始索引。
暴力写法:for循环适合模拟从头到尾的遍历,而while循环适合模拟环形遍历,要善于使用while,注意下标就可以实现
cpp
class Solution {
public:
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+1是下一个坐标开始记录
//这里的循环条件汽有剩余并且下标不为i
while(rest > 0 && index != i){
rest += gas[index] - cost[index];//下标的剩余汽加入剩余汽
index = (index+1)%cost.size();//更新下标
}
if(rest >= 0 && index == i)return i;//最后判断如果满足条件我们返回下标位置
}
return -1;
}
};
- 时间复杂度:O(n^2)
- 空间复杂度:O(1)
回溯算法:
cpp
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;
if(min >= 0)return 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;
}
};
- 时间复杂度:O(n)
- 空间复杂度:O(1)
贪心方法:
局部最优:当前累加rest[i]的和curSum一旦小于0,起始位置至少要是i+1,因为从i之前开始一定不行。全局最优:找到可以跑一圈的起始位置。
cpp
class Solution {
public:
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,返回-1
if(totalsum < 0)return -1;
return start;
}
};
- 时间复杂度:O(n)
- 空间复杂度:O(1)
5.分发糖果(135题)
题目描述:
老师想给孩子们分发糖果,有 N 个孩子站成了一条直线,老师会根据每个孩子的表现,预先给他们评分。
你需要按照以下要求,帮助老师给这些孩子分发糖果:
- 每个孩子至少分配到 1 个糖果。
- 相邻的孩子中,评分高的孩子必须获得更多的糖果。
那么这样下来,老师至少需要准备多少颗糖果呢?
示例 1:
- 输入: [1,0,2]
- 输出: 5
- 解释: 你可以分别给这三个孩子分发 2、1、2 颗糖果。
确定右边评分大于左边的情况(也就是从前向后遍历),
此时局部最优:只要右边评分比左边大,右边的孩子就多一个糖果,全局最优:相邻的孩子中,评分高的右孩子获得比左边孩子更多的糖果
局部最优可以推出全局最优。
如果ratings[i] > ratings[i - 1] 那么[i]的糖 一定要比[i - 1]的糖多一个,所以贪心:candyVec[i] = candyVec[i - 1] + 1
确定左孩子大于右孩子的情况一定要从后向前遍历
如果 ratings[i] > ratings[i + 1],此时candyVec[i](第i个小孩的糖果数量)就有两个选择了,一个是candyVec[i + 1] + 1(从右边这个加1得到的糖果数量),一个是candyVec[i](之前比较右孩子大于左孩子得到的糖果数量)。
那么又要贪心了,局部最优:取candyVec[i + 1] + 1 和 candyVec[i] 最大的糖果数量,保证第i个小孩的糖果数量既大于左边的也大于右边的。全局最优:相邻的孩子中,评分高的孩子获得更多的糖果。
局部最优可以推出全局最优。
所以就取candyVec[i + 1] + 1 和 candyVec[i] 最大的糖果数量,candyVec[i]只有取最大的才能既保持对左边candyVec[i - 1]的糖果多,也比右边candyVec[i + 1]的糖果多。
cpp
class Solution {
public:
int candy(vector<int>& ratings) {
vector<int>candyvec(ratings.size(),1);//这里我们定义一个数组来接收糖果
//从前向后遍历,注意i的遍历位置
for(int i = 1;i < ratings.size();i++){
if(ratings[i] > ratings[i - 1])candyvec[i] = candyvec[i-1]+1;//右边孩子大于左边孩子,糖果+1
}
//从后向前遍历,注意i的位置
for(int i = ratings.size() - 2;i >= 0;i--){
if(ratings[i] > ratings[i+1])candyvec[i] = max(candyvec[i+1]+1,candyvec[i]);//左边孩子大于右边孩子,且做一个操作取两次遍历的最大值
}
int result = 0;//结果
for(int i = 0;i < candyvec.size();i++){
result += candyvec[i];//我们结算整个糖果的和
}
return result;
}
};
- 时间复杂度: O(n)
- 空间复杂度: O(n)
6.柠檬水找零( 860题)
题目描述:
在柠檬水摊上,每一杯柠檬水的售价为 5 美元。
顾客排队购买你的产品,(按账单 bills 支付的顺序)一次购买一杯。
每位顾客只买一杯柠檬水,然后向你付 5 美元、10 美元或 20 美元。你必须给每个顾客正确找零,也就是说净交易是每位顾客向你支付 5 美元。
注意,一开始你手头没有任何零钱。
如果你能给每位顾客正确找零,返回 true ,否则返回 false 。
示例 1:
- 输入:[5,5,5,10,20]
- 输出:true
- 解释:
- 前 3 位顾客那里,我们按顺序收取 3 张 5 美元的钞票。
- 第 4 位顾客那里,我们收取一张 10 美元的钞票,并返还 5 美元。
- 第 5 位顾客那里,我们找还一张 10 美元的钞票和一张 5 美元的钞票。
- 由于所有客户都得到了正确的找零,所以我们输出 true。
贪心算法:
只需要维护三种金额的数量,5,10和20。
有如下三种情况:
- 情况一:账单是5,直接收下。
- 情况二:账单是10,消耗一个5,增加一个10
- 情况三:账单是20,优先消耗一个10和一个5,如果不够,再消耗三个5
cpp
class Solution {
public:
bool lemonadeChange(vector<int>& bills) {
int five = 0,ten = 0,twenty = 0;
for(int bill : bills){
if(bill == 5)five++;
if(bill == 10){
if(five <= 0)return false;
ten++,five--;
}
if(bill == 20){
//优先消耗10和5组合
if(ten > 0 && five > 0){
ten--;
five--;
twenty++;
}else if(five >=3){
five -=3;//消耗三张5
twenty++;
}else{
return false;
}
}
}
return true;
}
};
- 时间复杂度: O(n)
- 空间复杂度: O(1)
总结:
跳跃游戏:此题就是看跳跃范围是否能覆盖终点,每次移动一个单位就更新一个最大覆盖范围即可,i 每次移动只能在 cover 的范围内移动,每移动一个元素,cover 得到该元素数值(新的覆盖范围)的补充,让 i 继续移动下去。cover 每次只取 max(该元素数值补充后的范围, cover 本身范围)如果 cover 大于等于了终点下标,直接 return true 就可以了
跳跃游戏II:要从覆盖范围出发,不管怎么跳,覆盖范围内一定是可以跳到的,以最小的步数增加覆盖范围,覆盖范围一旦覆盖了终点,得到的就是最少步数!需要统计两个覆盖范围,当前这一步的最大覆盖和下一步最大覆盖 ,需要定义两个覆盖范围,当前覆盖范围和下一个覆盖范围,我们首先需要更新下一个最大覆盖范围,然后按照当前覆盖范围来确定后续,更新当前覆盖范围和结果,满足条件就跳出。
K次取反后最大化的数组和:首先按照绝对值大小来把数组排序,按照从大到小排序,因为要最大和,所以k首先需要反转绝对值大的负数,我们也方便从前向后遍历,我们首先消耗负数,剩余的K去消耗,这个可以多次对一个数进行操作,对k剩下数值判断如果奇数,反转最后一个数,然后得到结果
加油站:暴力的写法就是需要从两个循环来实现,一层循环来遍历整个数组,定义一个初始位置不变,另一个循环去遍历元素,看是否可以回到之前位置,贪心定义两个变量,一个是当前的总和,另一个是总和,如果当前总和小于0,我们从该下标+1处开始下一次遍历,记录下标,如果总和>=0则可以走一圈,返回开始下标就可以
分发糖果:这题我们需要先根据从从左向右遍历,其实需要考虑两个方向的大小都需要满足,所以需要从左向右遍历根据题目条件来规定糖果数量,再根据从右向左遍历根据条件来计算糖果,选择更多的糖果。要注意遍历的开始范围,
柠檬水找零:单纯模拟题,定义三个变量5,10,20,如果只有一张5,直接记录变量,如果是10,我们需要对5--,10++操作,如果20块,则首先考虑一张10和一张5组合,第二种情况是三张5,如果前两种情况都不满足则返回错误,简单题秒了。