贪心算法深化 I

134. 加油站

解题思路

这道题的贪心核心有两种思路,重点掌握方法二(全局贪心)

核心逻辑(方法二):
  1. 总油量判断 :如果所有加油站的总油量 totalGas < 总消耗 totalCost,则一定无法绕一圈,直接返回 -1。
  2. 局部贪心 :遍历每个加油站,维护当前剩余油量 curRest
    • curRest + gas[i] - cost[i] < 0,说明从当前起点到第 i 站无法到达,将起点更新为 i+1,并重置 curRest 为 0。
    • 否则,累加 curRest
  3. 最终若总油量足够,起点即为答案(贪心保证了这个起点是唯一可行的)。

完整 Java 代码

java 复制代码
class Solution {
    public int canCompleteCircuit(int[] gas, int[] cost) {
        int totalGas = 0; // 总油量
        int totalCost = 0; // 总消耗
        int curRest = 0;   // 当前剩余油量
        int start = 0;     // 起始加油站索引
        
        for (int i = 0; i < gas.length; i++) {
            totalGas += gas[i];
            totalCost += cost[i];
            // 计算从当前起点到i站后的剩余油量
            curRest += gas[i] - cost[i];
            
            // 若当前剩余油量<0,说明start到i站无法到达,起点更新为i+1
            if (curRest < 0) {
                start = i + 1;
                curRest = 0; // 重置当前剩余油量
            }
        }
        
        // 总油量 < 总消耗,直接返回-1
        if (totalGas < totalCost) {
            return -1;
        }
        
        // 题目保证唯一解,返回start
        return start;
    }
}

代码解析

  1. 变量初始化totalGas/totalCost 统计全局油量和消耗,curRest 统计当前起点的剩余油量,start 记录候选起点。
  2. 遍历加油站
    • 累加总油量和总消耗,判断全局是否可行。
    • 计算当前起点到 i 站的剩余油量 curRest
    • curRest < 0,说明当前起点不可行,更新起点为 i+1,并重置 curRest(因为从 i+1 开始重新计算)。
  3. 全局判断 :若总油量 < 总消耗,返回 -1;否则返回 start(题目保证唯一解)。

135. 分发糖果

解题思路

这道题的贪心核心是:分两次遍历,先处理一边再处理另一边,不两头兼顾

  1. 第一次遍历(从左到右):保证每个孩子如果比左边孩子评分高,糖果数比左边多 1(满足 "左规则")。
  2. 第二次遍历(从右到左):保证每个孩子如果比右边孩子评分高,糖果数取 "当前值" 和 "右边孩子糖果数 + 1" 的最大值(满足 "右规则")。
  3. 最终累加所有糖果数即为答案。

完整 Java 代码

java 复制代码
class Solution {
    public int candy(int[] ratings) {
        int n = ratings.length;
        int[] candies = new int[n];
        
        // 步骤1:初始化每个孩子至少1颗糖果
        for (int i = 0; i < n; i++) {
            candies[i] = 1;
        }
        
        // 步骤2:从左到右遍历,处理左规则
        for (int i = 1; i < n; i++) {
            // 如果当前孩子评分 > 左边,糖果数=左边+1
            if (ratings[i] > ratings[i-1]) {
                candies[i] = candies[i-1] + 1;
            }
        }
        
        // 步骤3:从右到左遍历,处理右规则
        for (int i = n-2; i >= 0; i--) {
            // 如果当前孩子评分 > 右边,取当前值和右边+1的最大值
            if (ratings[i] > ratings[i+1]) {
                candies[i] = Math.max(candies[i], candies[i+1] + 1);
            }
        }
        
        // 步骤4:累加所有糖果数
        int total = 0;
        for (int num : candies) {
            total += num;
        }
        
        return total;
    }
}

代码解析

  1. 初始化 :创建与评分数组等长的 candies 数组,每个元素初始化为 1(每个孩子至少 1 颗糖果)。
  2. 左到右遍历:从索引 1 开始,若当前孩子评分 > 左边,糖果数 = 左边糖果数 + 1(保证左规则)。
  3. 右到左遍历 :从索引 n-2 开始,若当前孩子评分 > 右边,糖果数取 "当前值" 和 "右边 + 1" 的最大值(既满足左规则,又满足右规则)。
  4. 累加求和 :遍历 candies 数组,累加得到总糖果数。

860. 柠檬水找零

解题思路

这道题的贪心核心是:优先用大面额找零,减少小面额的消耗(因为 10 元只能找零 20 元,而 5 元能找零 10/20 元,优先级更高):

  1. 维护两个变量:count5(5 元数量)、count10(10 元数量)(20 元无需统计,因为无法找零)。
  2. 遍历账单数组:
    • 收到 5 元:count5++
    • 收到 10 元:需要找零 5 元,若 count5 > 0,则 count5--count10++;否则返回 false。
    • 收到 20 元:优先用 10+5 找零(贪心),若没有则用 3 张 5 元;都没有则返回 false。
  3. 遍历结束后返回 true。

完整 Java 代码

java 复制代码
class Solution {
    public boolean lemonadeChange(int[] bills) {
        int count5 = 0; // 5元纸币数量
        int count10 = 0; // 10元纸币数量
        
        for (int bill : bills) {
            if (bill == 5) {
                // 收到5元,无需找零,直接计数
                count5++;
            } else if (bill == 10) {
                // 收到10元,需要找零5元
                if (count5 > 0) {
                    count5--;
                    count10++;
                } else {
                    return false;
                }
            } else if (bill == 20) {
                // 收到20元,优先用10+5找零(贪心)
                if (count10 > 0 && count5 > 0) {
                    count10--;
                    count5--;
                } else if (count5 >= 3) {
                    // 没有10元,用3张5元
                    count5 -= 3;
                } else {
                    return false;
                }
            }
        }
        
        return true;
    }
}

代码解析

  1. 变量初始化count5count10 初始化为 0,分别记录 5 元、10 元的数量。
  2. 遍历账单
    • 5 元:直接计数,无需找零。
    • 10 元:必须找零 5 元,若没有则返回 false;否则更新计数。
    • 20 元:贪心优先用 10+5 找零(保留更多 5 元应对后续 10 元找零),若不行则用 3 张 5 元,否则返回 false。
  3. 返回结果:遍历完成后说明所有账单都能找零,返回 true。

406. 根据身高重建队列

解题思路

这道题的贪心核心是:先处理高个子,再处理矮个子,不两头兼顾

  1. 排序:将数组按「身高降序、k 值升序」排序(高个子先排,身高相同则 k 小的先排)。
  2. 插入 :遍历排序后的数组,按每个元素的 k 值(前面比自己高 / 等高的人数)插入到结果列表的对应位置。
    • 因为高个子先排,插入矮个子时,不会影响高个子的 k 值(矮个子不参与高个子的计数)。

完整 Java 代码

java 复制代码
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

class Solution {
    public int[][] reconstructQueue(int[][] people) {
        // 步骤1:排序:身高降序,k值升序
        Arrays.sort(people, (a, b) -> {
            if (a[0] != b[0]) {
                return b[0] - a[0]; // 身高降序
            } else {
                return a[1] - b[1]; // k值升序
            }
        });
        
        // 步骤2:按k值插入到对应位置
        List<int[]> queue = new ArrayList<>();
        for (int[] p : people) {
            queue.add(p[1], p); // 插入到索引为p[1]的位置
        }
        
        // 步骤3:转换为数组返回
        return queue.toArray(new int[queue.size()][]);
    }
}

代码解析

  1. 排序规则
    • 身高不同:降序排列(高个子先处理)。
    • 身高相同:k 值升序排列(k 小的先排,避免插入时影响)。
  2. 插入逻辑
    • ArrayList 存储结果,因为其 add(index, element) 方法可以高效插入。
    • 遍历排序后的数组,将每个人插入到索引为 p[1] 的位置(p[1] 表示前面需要有 p[1] 个比他高 / 等高的人,而此时列表中都是更高 / 等高的人,插入位置正好满足要求)。
  3. 结果转换 :将 ArrayList 转换为二维数组返回。

总结

  1. 加油站:贪心核心是 "全局判断总油量是否足够,局部重置起点",总油量足够时,第一个能走完局部的起点就是全局解。
  2. 分发糖果:贪心核心是 "分两次遍历,先左后右(或先右后左)",避免同时兼顾两边导致逻辑混乱。
  3. 柠檬水找零:贪心核心是 "优先用大面额找零",保留更多小面额应对后续需求。
  4. 根据身高重建队列:贪心核心是 "先处理高个子,再按 k 值插入矮个子",高个子的位置不受矮个子影响。
相关推荐
会员源码网6 小时前
使用`mysql_*`废弃函数(PHP7+完全移除,导致代码无法运行)
后端·算法
木心月转码ing6 小时前
Hot100-Day10-T438T438找到字符串中所有字母异位词
算法
HelloReader7 小时前
Wi-Fi CSI 感知技术用无线信号“看见“室内的人
算法
颜酱10 小时前
二叉树分解问题思路解题模式
javascript·后端·算法
qianpeng89711 小时前
水声匹配场定位原理及实验
算法
董董灿是个攻城狮1 天前
AI视觉连载8:传统 CV 之边缘检测
算法
AI软著研究员1 天前
程序员必看:软著不是“面子工程”,是代码的“法律保险”
算法
FunnySaltyFish1 天前
什么?Compose 把 GapBuffer 换成了 LinkBuffer?
算法·kotlin·android jetpack
颜酱1 天前
理解二叉树最近公共祖先(LCA):从基础到变种解析
javascript·后端·算法
地平线开发者2 天前
SparseDrive 模型导出与性能优化实战
算法·自动驾驶