1 我为什么要写这个总结
1.1 字节笔试题
小明在玩一场通关游戏,初始血量为1,关卡有怪兽或者有血包(正数就是血包可回血数,负数说明是怪兽的伤害值),当捡到血包时会加血量,碰到怪兽时会掉血,现在指定初始血量为x,关卡是一个数组,小明必须按照数组的顺序玩游戏,当碰到一个怪兽时,他可以选择将这个怪兽扔到数组末尾,小明可以无限次地将怪兽移到数组末尾,问小明最少移动几次就能存活,如果无论怎么移动都不能存活则返回-1, 假设关卡是这样的[-200,-300,400],则返回-1,假如是这样的[200,100,-250,-60,-70,100],则返回1,只需要把-250挪到尾部,
思路:当发现自己血量不足时,就从当前已经遍历过的所有关卡中,选择耗费血量最多的那个关卡并且放到最后一关,如果即使这样挪开了耗血量最大的一关自身血量还是为负,则直接返回-1,说明无法通关
2 总结
2.1 什么是局部最优?
贪心关注的是局部最优,这里当前最优(指当前遍历的所有元素中的最优解)也是局部最优的一种,而一般的最有解又涉及到数据的最值,而且随着元素的不断向右扩展,可能这个最优值是不断变化的,所以一般可以使用堆来动态维护它的最值。
2.2 解贪心类题目的一些分类
分为相向双指针,同向双指针以及最值堆等类型
2.2.1 相向双指针和同向双指针的解题思路的区别
-
相向双指针通常用于处理排序数组中的问题,比如找出两个数的和等于特定值(如LeetCode题目167. 两数之和 II - 输入有序数组),或者反转数组(如LeetCode题目344. 反转字符串)。对于这类问题,两个指针分别从数组的头部和尾部出发,根据指向元素的大小关系向中间移动,直到两个指针相遇。
-
同向双指针通常用于处理数组或字符串中需要连续元素满足特定条件的问题,比如找出满足特定和的最小子数组(如LeetCode题目209. 长度最小的子数组),或者找出最长的满足特定条件的子串(如LeetCode题目424. 替换后的最长重复字符)。对于这类问题,一个指针(快指针)用于遍历数组或字符串,另一个指针(慢指针)用于维护满足条件的区间。
2.2.2 在LeetCode上,与同向双指针和贪心策略相关的题目有很多。举两个例子:
-
题目 209. 长度最小的子数组: 在一个正整数数组中寻找长度最小的连续子数组,其和至少为特定值。解决这个问题的方法是维持一个滑动窗口,窗口的左右边界就是同向的双指针。右指针向右移动以增加子数组的和,当子数组的和达到或超过目标值时,尝试将左指针向右移动以减小子数组的长度。
-
题目 424. 替换后的最长重复字符: 给定一个字符串和一个整数k,找出可以通过最多替换k个字符以得到的最长的重复字符子串。解决这个问题的方法同样是维持一个滑动窗口,窗口的左右边界就是同向的双指针。右指针向右移动以增加子串的长度,当子串中最多的字符数量加上k小于子串的长度时,左指针向右移动。
2.3 一部分需要预处理的题目
2.3.1 可能需要排序
- 分发饼干
2.3.2 可能需要hashMap统计字符最后一次出现的位置
- 划分字母区间
3 使用堆解决问题的贪心策略
3.1 leetcode1046. 最后一块石头的重量
3.2 leetcode 1642. 可以到达的最远建筑
标准答案:
java
// 砖块优先,砖块数量不足时考虑将当前替代最小高度的梯子改用砖块替代
public int furthestBuilding(int[] heights, int bricks, int ladders) {
int n=heights.length;
//大根堆
PriorityQueue<Integer>pq=new PriorityQueue<>((o1,o2)->(o2-o1));
int i;
int sum=0;//表示当前需要的梯子数量
int sum_bri=0;
for(i=1;i<n;i++){
int diff=heights[i]-heights[i-1];
if(diff>0){
pq.offer(diff);
sum_bri+=diff;//砖块的数量之和
if(sum_bri>bricks){
sum_bri-=pq.poll();
sum++;
}
if(sum>ladders){
return i-1;
}
}
}
return n-1;
}
//每次先放梯子,当梯子数量不足时,将当前使用过的梯子中,替代阶梯数最少的梯子找出来用对应砖头替换,替换出来的梯子拿到当前使用
public int furthestBuilding(int[] heights, int bricks, int ladders) {
int n=heights.length;
//int[]:
PriorityQueue<Integer>pq=new PriorityQueue<>((o1,o2)->(o1-o2));
int i;
int sum=0;//表示当前需要的砖块数量
for(i=1;i<n;i++){
int diff=heights[i]-heights[i-1];
if(diff>0){
pq.offer(diff);
if(pq.size()>ladders){
sum+=pq.poll();
}
if(sum>bricks){
return i-1;
}
}
}
return n-1;
}
```
我的答案:(有几个特别长的case过不了)
```java
public int furthestBuilding2(int[] heights, int bricks, int ladders) {
int n=heights.length;
PriorityQueue<Integer>pq=new PriorityQueue<>((o1,o2)->(o1-o2));
int i;
for(i=1;i<n;){
int diff=heights[i-1]-heights[i];
// System.out.println("i:"+i+",diff:"+diff+",heights[i-1]:"+heights[i-1]+",heights[i]:"+heights[i]);
if(diff<0){
int diffAbs=-diff;
if(ladders>0){
ladders--;
pq.add(diffAbs);
}else if(bricks>=diffAbs){
bricks-=diffAbs;
}else{
int newd=Integer.MAX_VALUE;
if(!pq.isEmpty()){
newd=pq.peek();
}
if(bricks>=newd){
pq.poll();
bricks-=newd;
ladders++;
i=i-1;
}else{
break;
}
}
}
i++;
}
return i-1;
}
这里还有一个二分查找的方法:
每太整明白:
3.3 121. 买卖股票的最佳时机(虽然使用堆的方法不是最优解,但是方便和其他题目对比得出最优解)
3.3.1 这个题和3.1以及3.2有什么异同呢?
同:都涉及到取最值,而且最值都是可能变化的
不同:3.1以及3.2中的题目一个最值只能使用一次,一旦使用就得poll操作,而本题中的最值可以复用。正式这个特性导致了前者只能使用堆来维护最值(因为可能需要多次取最值,即使用到多个最值,poll操作只能靠堆维护),虽说本题也多次最最值,但是可以重用最值,没有poll操作。
java
class Solution {
// 使用堆维护最小值
public int maxProfit(int[] prices) {
int n=prices.length;
//表示到第i天时,股票的历史最低点价格
PriorityQueue<Integer>pq=new PriorityQueue<>((o1,o2)->(o1-o2));
int max=0;
for(int i=0;i<n;i++){
pq.add(prices[i]);
max=Math.max(max,prices[i]-pq.peek());
}
return max;
}
// 方法:使用dp维护已经遍历值得最小值
public int maxProfit3(int[] prices) {
int n=prices.length;
//表示到第i天时,股票的历史最低点价格
int[]f=new int[n];
f[0]=prices[0];
int max=0;
for(int i=1;i<n;i++){
f[i]=Math.min(f[i-1],prices[i]);
max=Math.max(max,prices[i]-f[i]);
}
return max;
}
// 方法:使用一个变量维护已经遍历值得最小值
public int maxProfit(int[] prices) {
int n=prices.length;
//表示到第i天时,股票的历史最低点价格
int min=prices[0];
int max=0;
for(int i=1;i<n;i++){
min=Math.min(min,prices[i]);
max=Math.max(max,prices[i]-min);
}
return max;
}
// 使用单调栈维护已经遍历序列的最小值
public int maxProfit2(int[] prices) {
int n=prices.length;
Deque<Integer>st=new ArrayDeque<>();
int max=0;
st.add(prices[0]);
for(int i=1;i<n;i++){
int p=prices[i];
if(!st.isEmpty()&&p<=st.peekLast()){
max=Math.max(st.peekLast()-st.peekFirst(),max);
while(!st.isEmpty()&&st.peekLast()>=p){
st.pollLast();
}
}
st.addLast(p);
}
if(!st.isEmpty()){
max=Math.max(st.peekLast()-st.peekFirst(),max);
}
return max;
}
}
3.3 871题最低加油次数和
3.4 630课程表III
4 使用相向双指针解决问题的贪心策略
4.1 167. 两数之和 II - 输入有序数组
思路:双指针解决偏差
java
class Solution {
public int[] twoSum(int[] numbers, int target) {
int n=numbers.length;
int i=0,j=n-1;
while(i<j){
int sum=numbers[i]+numbers[j];
if(sum>target){
j--;
}else if(sum<target){
i++;
}else{
return new int[]{i+1,j+1};
}
}
return new int[]{i+1,j+1};
}
}
4.2 11. 盛最多水的容器
思路:容器能盛的水取决于短板,不断移动双指针中代表较小数值的那个指针,直到两个指针相遇,其中得到的所有可能的盛水量中取最大值即可。
java
class Solution {
public int maxArea(int[] height) {
int n=height.length;
int i=0,j=n-1;
int max=-1;
while(i<j){
int s=(j-i)*Math.min(height[i],height[j]);
max=Math.max(max,s);
if(height[i]>height[j]){
j--;
}else{
i++;
}
}
return max;
}
}
5 同向双指针与贪心策略
5.1 763. 划分字母区间(找出数组中满足特定条件的子序列)
java
class Solution {
public List<Integer> partitionLabels(String s) {
int n=s.length();
int[]mp=new int[26];
for(int i=0;i<n;i++){
mp[s.charAt(i)-'a']=i;
}
List<Integer>res=new ArrayList<>();
int start=0,end=0;
for(int i=0;i<n;i++){
end=Math.max(end,mp[s.charAt(i)-'a']);
if(end==i){
res.add(i-start+1);
start=i+1;
}
}
return res;
}
}