代码随想录算法训练营第三十五天 | 860.柠檬水找零、406.根据身高重建队列、452. 用最少数量的箭引爆气球
860.柠檬水找零
贪心算法
思路:本题因为只需维护三种面额的钱,因此较简单
- 局部最优:遇到20,优先消耗一张10和一张5,没有10再消耗三张5,因为5更万能。
- 全局最优:完成全部账单的找零。
// 代码随想录版本,更符合思想
class Solution {
public boolean lemonadeChange(int[] bills) {
int five = 0;
int ten = 0;
for (int i = 0; i < bills.length; i++) {
if (bills[i] == 5) {
five++;
} else if (bills[i] == 10) {
five--;
ten++;
} else if (bills[i] == 20) {
if (ten > 0) {
ten--;
five--;
} else {
five -= 3;
}
}
if (five < 0 || ten < 0) return false;
}
return true;
}
}
// 个人版本
class Solution {
public boolean lemonadeChange(int[] bills) {
if(bills[0] == 10 || bills[0] == 20) return false;
int count_5 = 0; // 五元的数量
int count_10 = 0; // 十元的数量
for(int i = 0; i < bills.length; i++) {
if(bills[i] == 5) {
count_5++;
}
if(bills[i] == 10) {
if(count_5 == 0) return false;
count_5--;
count_10++;
}
if(bills[i] == 20) {
if(count_5 == 0) return false;
if(count_10 == 0) {
if(count_5 < 3) {
return false;
} else {
count_5 -= 3;
}
} else {
count_5--;
count_10--;
}
}
}
return true;
}
}
406.根据身高重建队列
贪心算法
思路:本题有两个维度(h 和 k)。对于此类题目一定要想如何确定一个维度,然后再按照另一个维度重新排列(类似之前分糖果题目,两个维度一起考虑容易顾此失彼)。
如果按照 k 来从小到大排序,排完之后会发现 k 的排列并不符合条件,身高也不符合条件,两个维度哪一个都没确定下来。
那么按照身高 h 来排序呢,身高一定是从大到小排(身高相同的话则 k 小的站前面),此时就可以确定身高这个维度了——前面的节点一定都比本节点高。那么只需要按照 k 为下标重新插入队列即可,原因如下:
所以在按照身高从大到小排序后:
- 局部最优:优先按身高较高的 people 的 k 值来插入。插入操作过后的 people 满足队列要求;
- 全局最优:最后都做完插入操作,整个队列满足题目队列属性;
class Solution {
public int[][] reconstructQueue(int[][] people) {
// 身高从大到小排(身高相同 k 小的站前面)
Arrays.sort(people, (a, b) -> {
// a - b 是升序排列,故在 a[0] == b[0] 的情况下,会根据 k 值升序排列
if (a[0] == b[0]) return a[1] - b[1];
return b[0] - a[0]; // b - a 是降序排列,在a[0] != b[0] 时会根据 h 降序排列
});
LinkedList<int[]> que = new LinkedList<>();
for (int[] p : people) {
//Linkedlist.add(index, value),会将 value 插入到指定 index 处。
que.add(p[1],p);
}
return que.toArray(new int[people.length][]);
}
}
注:对于 Arrays.sort() 中的 b[0] - a[0],若需要考虑溢出情况,优先使用Integer.compare(a[0], b[0]),如下图所示:
452. 用最少数量的箭引爆气球
贪心算法
思路:本题贪心思路较简单
- 局部最优:当气球出现重叠,一起射,所用弓箭最少。
- 全局最优:引爆所有气球所必须射出的最小弓箭数。
关键在于模拟射箭的过程。直观的思考是,射穿一个区间,将该区间从数组中删除即可。但进一步思考,如果把气球排序之后,从前到后遍历气球,被射过的气球跳过就行了,没有必要让气球数组 remove 气球,只要记录一下箭的数量就可以了。
对原数组按起始位置排序,从前向后遍历气球数组,靠左尽可能让气球重复。如果气球重叠了,重叠气球中右边边界的最小值之前的区间一定需要一个弓箭,如下图所示:
/**
* 时间复杂度 : O(NlogN) 排序需要 O(NlogN) 的复杂度
* 空间复杂度 : O(logN) Java所使用的内置函数用的是快速排序需要 logN 的空间
*/
class Solution {
public int findMinArrowShots(int[][] points) {
// 根据气球直径的开始坐标从小到大排序
// 使用 Integer 内置比较方法,不会溢出
Arrays.sort(points, (a, b) -> Integer.compare(a[0], b[0]));
// return a[0] - b[0]; // 溢出错误
int count = 1; // 至少需要一支箭
for(int i = 1; i < points.length; i++) {
if(points[i][0] > points[i - 1][1]) { // 区间无重叠
count++;
} else { // 区间重叠,则取两区间右边界的最小值
points[i][1] = Math.min(points[i][1], points[i - 1][1]);
}
}
return count;
}
}