简介
- 跳跃游戏Ⅱ - LeetCode 45
- K 次取反后最大化的数组和 - LeetCode 1005
- 加油站 - LeetCode 136
- 分发糖果 - LeetCode 135
- 柠檬水找零 - LeetCode 860
题目 01 - 跳跃游戏Ⅱ
原题链接
题目描述
给定一个长度为 n 的 0 索引整数数组 nums。初始位置为 nums[0]。 每个元素 nums[i] 表示从索引 i 向后跳转的最大长度。换句话说,如果你在 nums[i] 处,你可以跳转到任意 nums[i + j] 处:
0 <= j <= nums[i] i + j < n
返回到达 nums[n - 1] 的最小跳跃次数。生成的测试用例可以到达 nums[n - 1]。
输入示例
思路
- 只要当前步数内可达的边界,即
数组的长度 - 1,当前步数内即可抵达 - 需要记录
currMax这一步的最大覆盖范围;和下一步可覆盖的最大范围nextMax; 下一步范围由i + nums[i]更新 - 每当抵达当前步数可达最远范围并且还未到边界,需要再跳下一步
- 终止条件,当前可覆盖范围包含边界
图示
代码实现
class Solution {
public int jump(int[] nums) {
int n = nums.length;
if(n == 1) { // 检查输入
return 0;
}
int currMax = 0; // 当前最远可达
int step = 0; // 最少所需步数
int totalMax = nums[0]; // 最远可达位置
for(int i = 0; i <= currMax; i++) {
totalMax = Math.max(totalMax, i + nums[i]); // 更新最远可达
if(currMax >= n - 1) { // 此步内可以抵达最后一位
return step;
}
if(i == currMax) { // 已经抵达此步可达的最远位置
step++; // 再跳一步
currMax = totalMax; // 更新最远可达为下一步最远可达
}
}
throw new RuntimeException();
}
}
执行结果
- 时间复杂度:
O(N) - 空间复杂度:
O(1)
题目 02 - K 次取反后的最大化数组和
原题链接
题目描述
给定一个长度为 n 的 0 索引整数数组 nums。初始位置为 nums[0]。 每个元素 nums[i] 表示从索引 i 向后跳转的最大长度。换句话说,如果你在 nums[i] 处,你可以跳转到任意 nums[i + j] 处:
0 <= j <= nums[i] i + j < n
返回到达 nums[n - 1] 的最小跳跃次数。生成的测试用例可以到达 nums[n - 1]。
输入示例
思路
- 越小的负数取反后越大,因此优先将所有负数取反;
- 但是有可能将所有负数取反之后未消耗完 k 次数;但是对于 k 如果为偶数的话则可以忽略。由于负负得正,那么操纵同一个数字偶数次保持不变。
- 而如果所有负数取反之后有剩余且 k % 2 后剩余 1; 则将此时最小值取反则能得到最大和;
- 比如此时最小为一个负数,那么取反后该最小负数会变为负数
- 反之如果最小为一个正数,由于该正数为最小,取反后使得和减少得也最少
图示
代码实现
class Solution {
public int largestSumAfterKNegations(int[] nums, int k) {
int n = nums.length;
// 检查输入
if (n == 0) {
return 0;
}
// 按照从小到大排序
Arrays.sort(nums);
int curr = 0;
// 优先将所有负数取反
while (k > 0 && curr < n && nums[curr] < 0) {
k--;
nums[curr] *= -1;
curr++;
}
// 负数取反次数未消耗完
if (k > 0 && k % 2 == 1) {
Arrays.sort(nums);
nums[0] *= -1;
}
return Arrays.stream(nums).sum();
}
}
执行结果
- 时间复杂度:
O(NlogN) - 空间复杂度:
O(1)
题目 03 - 加油站
原题链接
题目描述
在一条环路上有 n 个加油站,其中第 i 个加油站有汽油 gas[i] 升。 你有一辆油箱容量无限的的汽车,从第 i 个加油站开往第 i+1 个加油站需要消耗汽油 cost[i] 升。
你从其中的一个加油站出发,开始时油箱为空。
给定两个整数数组 gas 和 cost ,如果你可以按顺序绕环路行驶一周,则返回出发时加油站的编号,否则返回 -1 。如果存在解,则 保证 它是 唯一 的。
输入示例
思路
- 全局限制: 总油量需要大于等于总消耗量才能跑完一圈; 说明各个站点的加油站剩余油量 rest[i] 相加一定需要大于等于 0
- 计算抵达每个加油站后的剩余油量为:
gas[i] - cost[i] - i 从 0 开始累加 rest[i]得 curSum,若 curSum 小于零,[0, i]区间都不能作起始位置,因该区间任一起点到 i 都会断油,起始位置从 i + 1 算起,重新从 0 计算 curSum。
- 计算抵达每个加油站后的剩余油量为:
- 局部最优: 当前累加 rest[i] 的和
currSum一旦小于 0,起始位置至少要是i + 1。因为从i之前开始肯定不行。全局最优:找能跑一圈的起始位置。
图示
代码实现
class Solution {
public int canCompleteCircuit(int[] gas, int[] cost) {
int n = gas.length; // 加油站数目
int total = 0; // 总体消耗油量
int start = 0;
int tank = 0;
for (int i = 0; i < n; i++) {
total += gas[i] - cost[i]; // 标记总体消耗油量是否足够支撑走完全部站点
tank += gas[i] - cost[i]; // 标记当前站点起步的话油箱是否足够
if (tank < 0) { // 无法抵达下一个站点
start = i + 1; // 更新起始加油站下标
tank = 0;
}
}
return total < 0 ? -1 : start;
}
}
执行结果
- 时间复杂度:
O(N) - 空间复杂度:
O(1)
题目 04 - 分发糖果
原题链接
题目描述
n 个孩子站成一排。给你一个整数数组 ratings 表示每个孩子的评分。 你需要按照以下要求,给这些孩子分发糖果:
- 每个孩子至少分配到 1 个糖果。
- 相邻两个孩子评分更高的孩子会获得更多的糖果。
请你给每个孩子分发糖果,计算并返回需要准备的 最少糖果数目 。
输入示例
思路
- 只要右边评分比左边大,右边的孩子至少要多一个糖果
- 规则:相邻的孩子中,评分最高的有孩子获得比左孩子更多的糖果
- 从左往右遍历,如果 ratings[i] > ratings[i - 1] 为了保证局部最优,分配糖果总数目最少
那么分配的糖果数目 candyVec[i] = candyVec[i - 1] + 1 比左侧多一个 - 再确定左孩子大于右孩子的情况; 如果 ratings[i] > rating[i + 1] 那么此时 candyVec[i]
要保证第 i 个小孩的糖果数量既大于左边的也大于右边的,存在两种可能:
- candyVec[i + 1] + 1 从右边这个加 1 得到的糖果数量
- candyVec[i] 由于第一次分配已经大于左侧跟右侧
- 保证第 i 个小孩的糖果数量既大于左边的也大于右边的
当取 max(candyVec[i + 1] + 1, candyVec[i]) 中最大的糖果数量,
才能保证 candyVec[i - 1] 的糖果更多,也比右边 candyVec[i + 1] 糖果多
代码实现
class Solution {
public int candy(int[] ratings) {
int n = ratings.length;
int[] cnt = new int[n];
cnt[0] = 1; // 最少分配一个糖果
for(int i = 1; i < n; i++) {
// 从左往右,评分比其左侧高的孩子的糖果数目得到更多;
//否则保证局部糖果数目最小,分配 1
cnt[i] = (ratings[i] > ratings[i - 1]) ? cnt[i - 1] + 1 : 1;
}
// 从右往左,评分比其右侧高的孩子要分得比其右侧多; 由于本身糖果数目有可能已经比右侧多;
// 为了保证局部糖果数目最小,分配 max(candyVec[i + 1] + 1, candyVec[i]) 中得较大值
for(int i = ratings.length - 2; i >= 0; i--) {
if(ratings[i] > ratings[i + 1]) {
cnt[i] = Math.max(cnt[i], cnt[i + 1] + 1);
}
}
return Arrays.stream(cnt).sum();
}
}
执行结果
- 时间复杂度:
O(N) - 空间复杂度:
O(N)
题目 05 - 柠檬水找零
原题链接
题目描述
在柠檬水摊上,每一杯柠檬水的售价为 5 美元。顾客排队购买你的产品,(按账单 bills 支付的顺序)一次购买一杯。
每位顾客只买一杯柠檬水,然后向你付 5 美元、10 美元或 20 美元。你必须给每个顾客正确找零,也就是说净交易是每位顾客向你支付 5 美元。
注意,一开始你手头没有任何零钱。
给你一个整数数组 bills ,其中 bills[i] 是第 i 位顾客付的账。如果你能给每位顾客正确找零,返回 true ,否则返回 false 。
输入示例
思路
-
由于 5 元最通用,因此找零钱的时候找 5 的优先级最低;优先使用 20, 10找零
-
付钱找零情况:
- 客户付 5 元则无需找零
- 付 10 元则需要消耗一张 5 元
- 付 20 元,由于 5 元更加通用;优先找零 1 张 10 元和 1 张 5元;否则找 3 张 5 元
-
若剩余的 5 元数目 < 0 则无法完成正确的找零
代码实现
class Solution {
public boolean lemonadeChange(int[] bills) {
int five = 0; // 5 元的剩余零钱数目
int ten = 0; // 10 元的剩余零钱数目
for(int b : bills) {
if(b == 5) { // 付 5 块不用找零
five++;
} else if (b == 10) { // 付 10 块
ten++;
five--; // 找一张 5 块
} else { // 付 20
if(ten >= 1) { // 有 10 块零钱先找 10 块
ten--;
five--;
} else {
five -= 3; // 否则找 3 张 5 块
}
}
if(five < 0) { // 不够支付
return false;
}
}
return true;
}
}
执行结果
- 时间复杂度:
O(N) - 空间复杂度:
O(1)