LeetCode刷题day50

简介: LeetCode刷题day50

134. 加油站


题目描述


难度中等858收藏分享切换为英文接收动态反馈


在一条环路上有 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 升汽油。
因此,无论怎样,你都不可能绕环路行驶一周。


思路分析


💙💙💙方法一:暴力法


**思路:**遍历每一个加油站为起点的情况,模拟一圈。如果跑了一圈,中途没有断油,而且最后油量大于等于0,说明这个起点是ok的。


注意:for循环适合模拟从头到尾的遍历,而while循环适合模拟环形遍历,要善于使用while!


💚💚💚方法二:贪心法(数学+分析)


从全局进行贪心选择,情况如下:


情况一:如果一圈中gas的总和小于cost总和,那么无论从哪里出发,一定是跑不了一圈的


情况二:rest[i] += gas[i]-cost[i] 为一站剩下的油,i从0开始计算累加到最后一站,如果累加没有出现负数,说明从0出发,油就没有断过,那么0就是起点。


情况三:如果累加的最小值min是负数,汽车就要从非0节点出发,从后向前,看哪个节点能把这个负数填平,能把这个负数填平的节点就是出发节点。


前两种情况容易理解,说一下第三种情况. 为啥当min是负数,汽车需要从后往前寻找出发点呢? 为啥不是从前往后呢??? 🐶 🐶 🐶


因为从前往后rest遇到了负值.说明可能的情况是 rest>0 ==> rest=0 ==>rest<0 ==>rest>0,说明后面的gas相对于cost会大的多,这样才可能抵消前面的负值. 所以应该从后往前寻找加油站直至能把min抵消更为合理.


💜💜💜方法三:贪心法


首先如果总油量减去总消耗大于等于零那么一定可以跑完一圈,说明 各个站点的加油站 剩油量rest[i]相加一定是大于等于零的


每个加油站的剩余量rest[i]为gas[i] - cost[i]。


i从0开始累加rest[i],和记为total,一旦total小于零,说明[0, i]区间都不能作为起始位置,起始位置从i+1算起,再从0计算rest。


如图:

bfba5294122d17599eec297e6f4e8adc.png

那么为什么一旦[i,j] 区间和为负数,起始位置就可以是j+1呢,j+1后面就不会出现更大的负数?


j 一旦出现了负数,j后面就需要正数为其进行抵消,因为耗油总和是大于零的.(前提我们已经确定了一定可以跑完全程)。而为啥出发点不可能是 [i,j] 呢? 因为正是前面的正数不够,所以才导致rest < 0 了,出发点一定是一个正数较为密集的点,这样才可以之后填充其他负数.


局部最优:当前累加和rest一旦小于0,起始位置至少要是j+1,因为从j开始一定不行。全局最优:找到可以跑一圈的起始位置。


参考代码


💛💛💛暴力法


//暴力法  TCL
int canCompleteCircuit(vector<int>& gas, vector<int>& cost) {
  for(int i = 0; i < gas.size(); i++) { //从加油站中选择一个 出发点
    int rest = gas[i]-cost[i];//当前汽车的汽油量
    int index = (i+1) % gas.size();//下一个加油站的下标
    while(rest >= 0 && index!=i) { //开始绕路一周
      rest += gas[index]-cost[index];
      index =  (index+1) % gas.size();
    }
    if(rest>=0 && index==i) {//如果汽车剩余量>=0,并且回到了出发点,则可以驾驶一周!
      return i;
    }
  }
  return -1;//遍历所有的加油站,如果没有找到可满足的则返回-1
}

时间复杂度:O ( n 2 )

空间复杂度:O ( n )


💙💙💙贪心法一

//贪心一:
int canCompleteCircuit(vector<int>& gas, vector<int>& cost) {
  int min = INT_MAX;//绕圈过程中汽车的最小汽油量
  int rest = 0;//当前汽车的汽油量
  //从0开始走一圈,看看是否可能跑 一圈
  for(int i = 0; i < gas.size(); i++) {
    rest  += gas[i] - cost[i]  ;
    if(rest < min) {
      min = rest;
    }
  }
  if(rest <0) { //如果gas的综合小于cost总和,则无论从哪里出发,都无法跑一圈
    return -1;
  }
  if(min >= 0) { //如果res最小值 >=0,则0汽油站便是出发点
    return 0;
  }
  //从后往前,看看何时能使min>=0
  for(int i = gas.size()-1; i >=0; i--) {//?为啥要从后往前呢? 因为从前往后res遇到了负值.说明可能的情况是 
  //res先>0 ==> res=0 ==>res <0 ==>res>0,说明后面的gas相对于cost会大的多,这样才可能抵消前面的负值. 所以应该从后往前寻找加油站直至能把min抵消. 
    int curSum = gas[i]-cost[i];//curSum当前加油和小号的差值.
    min += curSum;
    if(min >= 0) { //什么时候抵消了,就从哪里开始.
      return i;
    }
  }
  return -1;//如果最后还没有找到,则返回-1
}

时间复杂度:O ( n 2 )

空间复杂度:O ( n )


💜💜💜贪心法二


//贪心二
int canCompleteCircuit(vector<int>& gas, vector<int>& cost) {
  int rest =  0;//到当前加油站为止,汽油的剩余量
  int total = 0;//记录总的gas和cost的差值
  int startIndex = 0;//起始下标
  for(int i = 0;i < gas.size(); i++){
    rest += gas[i]-cost[i];
        total += gas[i]-cost[i];
    if(rest < 0){//如果rest<0,则起始下标改成i+1,rest重新开始计数
      startIndex = i + 1 ;
      rest = 0;
    }
  }
  if(total < 0){//如果gas的和值  < cost 和值,则无法行驶一周 
    return -1;
  }
  return startIndex;//从startIndex开始并没有出现负值,则返回加油站位置 
}


时间复杂度:O ( n )

空间复杂度:O ( 1 )


135. 分发糖果


题目描述

n 个孩子站成一排。给你一个整数数组 ratings 表示每个孩子的评分。

你需要按照以下要求,给这些孩子分发糖果:


  • 每个孩子至少分配到 1 个糖果。
  • 相邻两个孩子评分更高的孩子会获得更多的糖果。


请你给每个孩子分发糖果,计算并返回需要准备的 最少糖果数目


示例 1:

输入:ratings = [1,0,2]
输出:5
解释:你可以分别给第一个、第二个、第三个孩子分发 2、1、2 颗糖果。

示例 2:


输入:ratings = [1,2,2]
输出:4
解释:你可以分别给第一个、第二个、第三个孩子分发 1、2、1 颗糖果。
     第三个孩子只得到 1 颗糖果,这满足题面中的两个条件。


思路分析


这道题目一定是要确定一边之后,再确定另一边,例如比较每一个孩子的左边,然后再比较右边,如果两边一起考虑一定会顾此失彼


先确定右边评分大于左边的情况(也就是从前向后遍历)


此时局部最优:只要右边评分比左边大,右边的孩子就多一个糖果,全局最优:相邻的孩子中,评分高的右孩子获得比左边孩子更多的糖果


局部最优可以推出全局最优。


如果ratings[i] > ratings[i - 1] 那么[i]的糖 一定要比[i - 1]的糖多一个,所以贪心:candy[i] = candy[i - 1] + 1


代码如下:


//从前往后 右边孩子大于左边孩子 ,右边孩子糖果数增加
for(int i = 1;i < ratings.size();i++){
    if(ratings[i] > ratings[i-1]){
        candy[i] = candy[i-1]+1; //右边糖果+1 
    }
} 


从前往后遍历后的结果,如图所示

7e4389ddcfd93cac47e4c65c45dfe3c5.png

再确定左孩子大于右孩子的情况(从后向前遍历)


遍历顺序这里有同学可能会有疑问,为什么不能从前向后遍历呢?


如果从前向后遍历,根据左孩子(当前孩子)对比右孩子,当前孩子的糖果做出变化.但是等到下一个孩子和下下个孩子对比,如果依旧是 > ,则下个孩子的糖果数量将发生变化.而当前的左孩子和右孩子的糖果数量关系未必又满足评分数组了. 一句话: 当前孩子的糖果数不能受下一个孩子糖果数的影响,就无法满足评分数组. 而反之,从后往前,左孩子(当前孩子)的糖果一旦确定,后序左孩子(当前孩子)以及右孩子的糖果便不再发生变化.


如果 ratings[i] > ratings[i + 1] ,此时candy[i](第i个小孩的糖果数量)就有两个选择了,一个是candy[i + 1] + 1,一个是candy[i]。取其中最大值


**局部最优:**取candy[i + 1] + 1 和 candy[i] 最大的糖果数量,保证第i个小孩的糖果数量即大于左边的也大于右边的。**全局最优:**相邻的孩子中,评分高的孩子获得更多的糖果。


局部最优可以推出全局最优。


从后往前遍历过程,如图所示:



c01ecbaa273d84ab65e21662cf0c6e41.png

参考代码

#include<bits/stdc++.h>
using namespace std;
int candy(vector<int>& ratings) {
  int res = 0;
  vector<int> candy(ratings.size(),1);//初始化糖果数组 每个孩子至少为1个 
  //从前往后 右边孩子大于左边孩子 ,右边孩子糖果数增加
  for(int i = 1;i < ratings.size();i++){
    if(ratings[i] > ratings[i-1]){
      candy[i] = candy[i-1]+1; //右边糖果+1 
    }
  } 
  //从后往前 左边孩子大于右边孩子,左边孩子糖果增加 
  for(int i = ratings.size()- 2; i >=0;i--)//从倒数第二个开始 
  {
    if(ratings[i]>ratings[i+1]){
      candy[i] = max(candy[i],candy[i+1]+1); // 选candy[i] candy[i+1]+1  ,如果candy[i]<=candy[i+1]则是candy[i+1]+1. 如果candy[i] > candy[i+1],则为candy[i] 
    }
  }
  for(int i = 0;i < candy.size();i++){
    res += candy[i];
  } 
  return res;
}


860. 柠檬水找零


题目描述


在柠檬水摊上,每一杯柠檬水的售价为 5 美元。顾客排队购买你的产品,(按账单 bills 支付的顺序)一次购买一杯。


每位顾客只买一杯柠檬水,然后向你付 5 美元、10 美元或 20 美元。你必须给每个顾客正确找零,也就是说净交易是每位顾客向你支付 5 美元。


注意,一开始你手头没有任何零钱。


给你一个整数数组 bills ,其中 bills[i] 是第 i 位顾客付的账。如果你能给每位顾客正确找零,返回 true ,否则返回 false 。


示例 1:

输入:bills = [5,5,5,10,20]
输出:true
解释:
前 3 位顾客那里,我们按顺序收取 3 张 5 美元的钞票。
第 4 位顾客那里,我们收取一张 10 美元的钞票,并返还 5 美元。
第 5 位顾客那里,我们找还一张 10 美元的钞票和一张 5 美元的钞票。
由于所有客户都得到了正确的找零,所以我们输出 true。

示例 2:

输入:bills = [5,5,10,10,20]
输出:false
解释:
前 2 位顾客那里,我们按顺序收取 2 张 5 美元的钞票。
对于接下来的 2 位顾客,我们收取一张 10 美元的钞票,然后返还 5 美元。
对于最后一位顾客,我们无法退回 15 美元,因为我们现在只有两张 10 美元的钞票。
由于不是每位顾客都得到了正确的找零,所以答案是 false。

思路分析


这道题目刚一看,可能会有点懵,这要怎么找零才能保证完整全部账单的找零呢?


但仔细一琢磨就会发现,可供我们做判断的空间非常少!


只需要维护三种金额的数量,5,10和20


有如下三种情况:


  • 情况一:账单是5,直接收下。
  • 情况二:账单是10,消耗一个5,增加一个10
  • 情况三:账单是20,优先消耗一个10和一个5,如果不够,再消耗三个5


情况一和情况二很容易理解, 情况三逻辑也不复杂甚至感觉纯模拟就可以了,其实情况三这里是有贪心的


账单是20的情况,为什么要优先消耗一个10和一个5呢?


因为美元10只能给账单20找零,而美元5可以给账单10和账单20找零,美元5更万能!


所以局部最优:遇到账单20,优先消耗美元10,完成本次找零。全局最优:完成全部账单的找零。


局部最优可以推出全局最优,并找不出反例,那么就试试贪心算法!


参考代码

#include<bits/stdc++.h>
using namespace std;
bool lemonadeChange(vector<int>& bills) {
  int five = 0,ten = 0,twenty = 0;
  for(int bill : bills){
    //当支付5美元
    if(bill==5){
      five++;
    }
    //当支付10美元
    if(bill==10){
      ten++;
      if(five > 0){
        five--;
      }else{//如果five不够的话. 
        return false;
      }
    }
    //当支付20美元
    if(bill==20){
      twenty++;
      if(five > 0&&ten > 0){//优先选择1张five 和1张ten 
        five--;
        ten--;
      }else if(five >= 3){//次之选择3张five 
        five-=3;
      }else{//否则 无法找零,结束 
        return false;
      }
    } 
  }
  return true; 
}

406. 根据身高重建队列


题目描述


假设有打乱顺序的一群人站成一个队列,数组 people 表示队列中一些人的属性(不一定按顺序)。每个 people[i] = [hi, ki] 表示第 i 个人的身高为 hi ,前面 正好 有 ki 个身高大于或等于 hi 的人。


请你重新构造并返回输入数组 people 所表示的队列。返回的队列应该格式化为数组 queue ,其中 queue[j] = [hj, kj] 是队列中第 j 个人的属性(queue[0] 是排在队列前面的人)。


示例 1:

输入:people = [[7,0],[4,4],[7,1],[5,0],[6,1],[5,2]]
输出:[[5,0],[7,0],[5,2],[6,1],[4,4],[7,1]]
解释:
编号为 0 的人身高为 5 ,没有身高更高或者相同的人排在他前面。
编号为 1 的人身高为 7 ,没有身高更高或者相同的人排在他前面。
编号为 2 的人身高为 5 ,有 2 个身高更高或者相同的人排在他前面,即编号为 0 和 1 的人。
编号为 3 的人身高为 6 ,有 1 个身高更高或者相同的人排在他前面,即编号为 1 的人。
编号为 4 的人身高为 4 ,有 4 个身高更高或者相同的人排在他前面,即编号为 0、1、2、3 的人。
编号为 5 的人身高为 7 ,有 1 个身高更高或者相同的人排在他前面,即编号为 1 的人。
因此 [[5,0],[7,0],[5,2],[6,1],[4,4],[7,1]] 是重新构造后的队列。

示例 2:


输入:people = [[6,0],[5,0],[4,0],[3,2],[2,2],[1,4]]
输出:[[4,0],[5,0],[2,2],[3,2],[1,4],[6,0]]


思路分析


本题有两个维度,h和k,看到这种题目一定要想如何确定一个维度,然后在按照另一个维度重新排列


其实如果大家认真做了135. 分发糖果 (opens new window),就会发现和此题有点点的像。


如果两个维度一起考虑一定会顾此失彼


对于本题相信大家困惑的点是先确定k还是先确定h呢,也就是究竟先按h排序呢,还先按照k排序呢?


如果按照k来从小到大排序,排完之后,会发现k的排列并不符合条件,身高也不符合条件,两个维度哪一个都没确定下来。


那么按照身高h来排序呢,身高一定是从大到小排(身高相同的话则k小的站前面),让高个子在前面。


此时我们可以确定一个维度了,就是身高,前面的节点一定都比本节点高!


那么只需要按照k为下标重新插入队列就可以了,为什么呢?


以图中{5,2} 为例:

5194ffd7033a3478d08f37ce9939855a.png


按照身高排序之后,开始people的k来插入,后序插入节点也不会影响前面已经插入的节点,最终按照k的规则完成了队列。


所以在按照身高从大到小排序后:


局部最优: 优先按身高高的people的k来插入。插入操作过后的people满足队列属性


全局最优: 最后都做完插入操作,整个队列满足题目队列属性


局部最优可推出全局最优,找不出反例,那就试试贪心。


本题中,整个插入过程如下:


排序完的people: [[7,0], [7,1], [6,1], [5,0], [5,2],[4,4]]


插入的过程:


  • 插入[7,0]:[[7,0]]
  • 插入[7,1]:[[7,0],[7,1]]
  • 插入[6,1]:[[7,0],[6,1],[7,1]]
  • 插入[5,0]:[[5,0],[7,0],[6,1],[7,1]]
  • 插入[5,2]:[[5,0],[7,0],[5,2],[6,1],[7,1]]
  • 插入[4,4]:[[5,0],[7,0],[5,2],[6,1],[4,4],[7,1]]


此时就按照题目的要求完成了重新排列。


参考代码

#include<bits/stdc++.h>
using namespace std;
static bool cmp(vector<int>& a,vector<int>& b){//定义排序规则 
  if(a[0]==b[0]){
    return a[1]<b[1];
  }
  return a[0] > b[0];
}
vector<vector<int>> reconstructQueue(vector<vector<int>>& people) {
  sort(people.begin(),people.end(),cmp) ;//进行排序
  //按照k从前往后整理元素   由于升高是从高到底所以插入元素对前面元素的k无影响
  vector<vector<int>> res;
  //要是第一个元素的k>0呢,还有元素呢  ??? 中间的插入会报错嘛. 
  for(int i = 0;i < people.size();i++){
    int position = people[i][1];
    res.insert(res.begin()+position,people[i]);
  } 
  return res; 
}


相关文章
|
6天前
|
索引
【力扣刷题】两数求和、移动零、相交链表、反转链表
【力扣刷题】两数求和、移动零、相交链表、反转链表
14 2
【力扣刷题】两数求和、移动零、相交链表、反转链表
|
5天前
|
算法
"刷题记录:哈希表+双指针 | leetcode-2465. 不同的平均值数目 "
该文段是一篇关于编程题目的解答,主要讨论如何找到数组中所有不同平均值的个数。作者首先使用排序和哈希集来解决,将数组转为列表排序后,通过双指针计算平均值并存入哈希集以去重。然后,作者发现可以优化方案,通过双指针在排序后的数组中直接计算两数之和,用哈希集记录不重复的和,从而避免实际计算平均值,提高了算法效率。最终代码展示了这两种方法。
13 0
|
6天前
|
索引
【力扣刷题】删除链表的倒数第 N 个结点、两两交换链表中的节点、随机链表的复制
【力扣刷题】删除链表的倒数第 N 个结点、两两交换链表中的节点、随机链表的复制
12 0
|
6天前
|
存储 算法 索引
【力扣刷题】只出现一次的数字、多数元素、环形链表 II、两数相加
【力扣刷题】只出现一次的数字、多数元素、环形链表 II、两数相加
16 1
|
6天前
【力扣刷题】二叉树的中序遍历、二叉树的最大深度、翻转二叉树、对称二叉树
【力扣刷题】二叉树的中序遍历、二叉树的最大深度、翻转二叉树、对称二叉树
13 0
|
6天前
|
索引
【力扣刷题】数组实现栈、后缀表达式(逆波兰表达式)求值、中缀表达式转换为后缀表达式(无括号&&有括号)
【力扣刷题】数组实现栈、后缀表达式(逆波兰表达式)求值、中缀表达式转换为后缀表达式(无括号&&有括号)
11 0
|
6天前
|
索引
【力扣刷题】回文链表、环形链表、合并两个有序链表
【力扣刷题】回文链表、环形链表、合并两个有序链表
12 0
|
9天前
|
算法 索引
力扣刷题【第一期】
这是一个关于算法的总结,包含7个不同的问题。1)爬楼梯问题,使用动态规划,通过迭代找到到达n阶楼梯的不同方法数。2)两数之和,通过双重循环找出数组中和为目标值的两个数的索引。3)移动零,使用双指针将数组中的0移到末尾。4)合并有序链表,创建新链表按升序合并两个链表。5)删除链表重复值,遍历链表删除重复元素。6)环形链表检测,使用快慢指针判断链表是否有环。7)相交链表,计算链表长度找
14 1
|
9天前
|
存储 Java
JAVA数据结构刷题 -- 力扣二叉树
JAVA数据结构刷题 -- 力扣二叉树
15 0
|
18天前
|
算法 C++
【刷题】Leetcode 1609.奇偶树
这道题是我目前做过最难的题,虽然没有一遍做出来,但是参考大佬的代码,慢慢啃的感觉的真的很好。刷题继续!!!!!!
14 0