860.柠檬水找零
题目链接:860.柠檬水找零
文档讲解:代码随想录/柠檬水找零
视频讲解:视频讲解-柠檬水找零
状态:已完成(1遍)
解题过程
看到题目的第一想法
这道题我竟然想不到一丝局部最优全局最优的思路。我只知道按照字面意思的机械的对五块钱和十块钱的数量进行计算。收到5的时候,5库存++;收到10的时候,10库存++,5库存--;收到20的时候,要么10和5库存--,要么5库存-=3。
手搓代码如下:
/**
* @param {number[]} bills
* @return {boolean}
*/
var lemonadeChange = function (bills) {
let isCashFive = 0;
let isCashTen = 0;
for (let i = 0; i < bills.length; i++) {
if (bills[i] == 5) {
isCashFive++;
} else if (bills[i] == 10) {
isCashFive--;
isCashTen++;
} else {
if (isCashTen) {
isCashTen--;
isCashFive--;
}else{
isCashFive-=3;
}
}
if (isCashFive < 0 || isCashTen < 0) return false;
}
return true;
};
提交没有问题。让我来看看代码随想录怎么想。
看完代码随想录之后的想法
我来拷贝一段:
只需要维护三种金额的数量,5,10和20。
有如下三种情况:
- 情况一:账单是5,直接收下。
- 情况二:账单是10,消耗一个5,增加一个10
- 情况三:账单是20,优先消耗一个10和一个5,如果不够,再消耗三个5
此时大家就发现 情况一,情况二,都是固定策略,都不用我们来做分析了,而唯一不确定的其实在情况三。
而情况三逻辑也不复杂甚至感觉纯模拟就可以了,其实情况三这里是有贪心的。
账单是20的情况,为什么要优先消耗一个10和一个5呢?
因为美元10只能给账单20找零,而美元5可以给账单10和账单20找零,美元5更万能!
所以局部最优:遇到账单20,优先消耗美元10,完成本次找零。全局最优:完成全部账单的找零。
局部最优可以推出全局最优,并找不出反例,那么就试试贪心算法!
怪不得说贪心其实很难发现自己用的是贪心,因为大家从小到大生活得来的经验其实就是贪心的本质。
讲解代码如下:
var lemonadeChange = function(bills) {
let fiveCount = 0
let tenCount = 0
for(let i = 0; i < bills.length; i++) {
let bill = bills[i]
if(bill === 5) {
fiveCount += 1
} else if (bill === 10) {
if(fiveCount > 0) {
fiveCount -=1
tenCount += 1
} else {
return false
}
} else {
if(tenCount > 0 && fiveCount > 0) {
tenCount -= 1
fiveCount -= 1
} else if(fiveCount >= 3) {
fiveCount -= 3
} else {
return false
}
}
}
return true
};
总结
这道题一共只有三种情况,全部列出来就很明了了。值得注意的是这里的贪心藏得很深。
406.根据身高重建队列
题目链接:406.根据身高重建队列
文档讲解:代码随想录/根据身高重建队列
视频讲解:视频讲解-根据身高重建队列
状态:已完成(1遍)
解题过程
看到题目的第一想法
这题我的想法是将people数组按照身高从矮到高排列,其中身高一样高的按照Ki从小到大排序。
然后对新数组进行遍历,每拿到一个元素,看他的Ki,是多少就放入当前ans中第几个空数组所在的位置。
手搓代码如下:
/**
* @param {number[][]} people
* @return {number[][]}
*/
var reconstructQueue = function (people) {
let len = people.length;
let ans = Array(len).fill([]);
let newPeople = people.sort(function (a, b) {
if (a[0] !== b[0]) {
return a[0] - b[0]; // 按照第一个元素排序
} else {
return a[1] - b[1]; // 第一个元素相等时,按照第二个元素排序
}
});
for(let i = 0;i<len;i++){
let index = newPeople[i][1];
//这是排好序的数组的Ki,每个人的Ki代表着当前它应该排在第Ki个ans数组里的空数组所在的位置
let j = 0;
//每个元素按照Ki在现在ans里的空数组个数填入,j主要是记录这个位置在ans里的索引
while(index && j<len+1){
if(ans[j].length == 0){
index--;
//已经经过了一个空数组,让index减1
}else if(ans[j][0] == newPeople[i][0]){
//如果遍历到和当前元素的Hi一样的,得算进去,因为身高一样,排序Ki更大的就得在小的后面
index--;
}
j++;
//j负责记录当前索引
}
while(ans[j].length != 0){
//记录完了之后会出现第j个位置已经有元素了,所以让j往后顺延,直到出现空数组
if(ans[j][0]<newPeople[i][0]){
j++;
}
}
ans.splice(j,1,newPeople[i]);
}
return ans;
};
不容易,前后debug了很久。分别是忽视了如果遍历到和当前元素一样高的元素,也得进行相对应的index--;全部遍历完了之后当前ans中 j 所在的位置已经有元素了,那就得往后顺延。
最终是提交成功了。
看完代码随想录之后的想法
神奇,代码随想录的思路和我完全相反。他是按照身高从大到小排序,相同身高的时候按照Ki从小到大排序。然后拿到一个人看他的Ki,再不断地往ans中插入。
讲解代码如下:
/**
* @param {number[][]} people
* @return {number[][]}
*/
var reconstructQueue = function(people) {
let queue = []
people.sort((a, b ) => {
if(b[0] !== a[0]) {
return b[0] - a[0]
} else {
return a[1] - b[1]
}
})
for(let i = 0; i < people.length; i++) {
queue.splice(people[i][1], 0, people[i])
}
return queue
};
总结
按照身高排序之后,优先按身高高的people的k来插入,后序插入节点也不会影响前面已经插入的节点,最终按照k的规则完成了队列。
所以在按照身高从大到小排序后:
局部最优:优先按身高高的people的k来插入。插入操作过后的people满足队列属性
全局最优:最后都做完插入操作,整个队列满足题目队列属性
452. 用最少数量的箭引爆气球
题目链接:452. 用最少数量的箭引爆气球
文档讲解:代码随想录/用最少数量的箭引爆气球
视频讲解:视频讲解-用最少数量的箭引爆气球
状态:已完成(1遍)
解题过程
看到题目的第一想法
按照气球坐标的左端进行排序,然后对后面的能一箭射穿的气球进行分类,只要后面的气球的左端坐标小于等于当前气球的右端坐标,就都是能一箭射穿的气球。分为一类之后再重新开始分类,直到最后。这是我的想法,不知道能不能行。
/**
* @param {number[][]} points
* @return {number}
*/
var findMinArrowShots = function (points) {
let arrow = 1;
let newPoints = points.sort(function (a, b) {
if (a[0] != b[0]) {
return a[0] - b[0];
} else {
return b[1] - a[1];
}
})
let curArrow = [];
for (let i = 0; i < newPoints.length; i++) {
let curLeft = newPoints[i][0],curRight = newPoints[i][1];
if(curArrow.length == 0){
//如果一箭射穿数组是空的,也就是刚开始
curArrow.push(newPoints[i]);
}else if (curLeft <= curArrow[0][1] ) {
// 当前气球左端坐标小于一箭射穿数组的第一个气球的右端坐标
// curArrow.push(points[i]);
curArrow[0][0] == curLeft;
curArrow[0][1] = curRight<=curArrow[0][1]?curRight:curArrow[0][1];
} else if ( curLeft > curArrow[0][1]) {
curArrow = [];
arrow++;
curArrow.push(newPoints[i]);
}
}
return arrow;
};
哈哈哈提交成功,没有问题。
看完代码随想录之后的想法
确实一开始我对气球排序的时候多考虑了,当气球左端坐标相等时,不用去管右端谁更小,反正后面的判断里会判断。
且只用对新气球的左端坐标和之前的右端最小坐标进行对比就可以了,写的简洁很多。
讲解代码如下:
/**
* @param {number[][]} points
* @return {number}
*/
var findMinArrowShots = function(points) {
points.sort((a, b) => {
return a[0] - b[0]
})
let result = 1
for(let i = 1; i < points.length; i++) {
if(points[i][0] > points[i - 1][1]) {
result++
} else {
points[i][1] = Math.min(points[i - 1][1], points[i][1])
}
}
return result
};
总结
我的思路没问题,但是代码写起来过于复杂,确实一个是不用更新一箭射穿数组的左坐标,一个是右坐标更新表达式可以用Math.min替代,i从1开始也可以有效避免我的第一个if判断。