【算法】【学习笔记】贪心

贪心算法是非常朴素的一种思想——每一步都找最好的那个选项。

贪心算法能拿到全局最优的严格前提

  • 贪心选择性质:当前做出的局部最优选择,绝对不会破坏后续找到全局最优解的可能性。
  • 最优子结构:问题的全局最优解包含其子问题的最优解。

❔题目

1. 柠檬水找零

解题思路

这个只要记录身上的面值数量,然后判断是否可以找零,找了就减少对应面值数,并且添加对应面值数即可。

然后因为越小的越可以构成更多的找零,所以优先选择大面值来找零。

实现

function lemonadeChange(bills: number[]): boolean {
    let five = 0;
    let ten = 0;

    for (let i = 0; i < bills.length; i++) {
        const bill = bills[i];
        if (bill === 5) {
            five++;
        } else if (bill === 10) {
            if (five === 0) return false;
            five--;
            ten++;
        } else {
            // 收到 20 元:优先用 10 + 5,没有 10 块再试 3 个 5 块
            if (ten > 0 && five > 0) {
                ten--;
                five--;
            } else if (five >= 3) {
                five -= 3;
            } else {
                return false;
            }
        }
    }
    return true;
}
public class Solution {
    public bool LemonadeChange(int[] bills) {
        int five = 0, ten = 0;
        foreach (int bill in bills) {
            if (bill == 5) {
                five++;
            } else if (bill == 10) {
                if (five == 0) return false;
                five--;
                ten++;
            } else {
                if (ten > 0 && five > 0) {
                    ten--;
                    five--;
                } else if (five >= 3) {
                    five -= 3;
                } else {
                    return false;
                }
            }
        }
        return true;
    }
}
class Solution {
public:
    bool lemonadeChange(vector<int>& bills) {
        int five = 0, ten = 0;
        for (int bill : bills) {
            if (bill == 5) {
                five++;
            } else if (bill == 10) {
                if (five == 0) return false;
                five--;
                ten++;
            } else {
                if (ten > 0 && five > 0) {
                    ten--;
                    five--;
                } else if (five >= 3) {
                    five -= 3;
                } else {
                    return false;
                }
            }
        }
        return true;
    }
};

复杂度分析

  • 时间复杂度\(O(N)\)
  • 空间复杂度\(O(1)\)

2. 盛最多水的容器

解题思路

也就是这个问题是,找2条线a和b,其$(x_b - x_a) \times min(h_a, h_b) $是最大。

按贪心那就是从b离a最远时开始寻找,然后往左或者右收,收的时候,一定要找比本身高的,才停下。

实现

function maxArea(height: number[]): number {
    let l = 0, r = height.length - 1;
    let res = 0;

    while (l < r) {
        const currentArea = (r - l) * Math.min(height[l], height[r]);
        res = Math.max(res, currentArea);

        if (height[l] < height[r]) {
            l++;
        } else {
            r--;
        }
    }

    return res;
}
public class Solution {
    public int MaxArea(int[] height) {
        int l = 0, r = height.Length - 1;
        int res = 0;

        while (l < r) {
            int currentArea = (r - l) * Math.Min(height[l], height[r]);
            res = Math.Max(res, currentArea);

            if (height[l] < height[r]) {
                l++;
            } else {
                r--;
            }
        }

        return res;
    }
}
class Solution {
public:
    int maxArea(vector<int>& height) {
        int l = 0, r = height.size() - 1;
        int res = 0;

        while (l < r) {
            // 1. 计算当前左右边界构成的盛水量
            int currentArea = (r - l) * min(height[l], height[r]);
            res = max(res, currentArea);

            // 2. 核心贪心:舍弃短板,将较矮一侧的指针向内移动
            if (height[l] < height[r]) {
                l++;
            } else {
                r--;
            }
        }

        return res;
    }
};

复杂度分析

  • 时间复杂度\(O(N)\)
  • 空间复杂度\(O(1)\)

3. 分发糖果

解题思路

核心变量与几何含义

  • up(上坡长度):当前处于连续递增序列的步数。
  • down(下坡长度):当前处于连续递减序列的步数。
  • peak(山峰高度):上坡能达到的最高相对高度,即 peak = up
  • res(总糖果数):累加的总需求量,默认第 1 个孩子分 1 个。

状态转换与计算规则

  • 上坡ratings[i] > ratings[i-1]):

    • 重置 down = 0,令 up++ 并记录顶点 peak = up
    • 当前人领取的糖果数为 1 + up,直接累加至 res
  • 下坡ratings[i] < ratings[i-1]):

    • 重置 up = 0,令 down++
    • 下坡整体抬高:为了保证递减,整个下坡段的孩子都需要被“抬高 1 个糖果”,总和增加 down
    • 山峰高度修正:当下坡长度超过山峰高度(down > peak),说明下坡太长逆向把顶点推高了,必须为顶点补加 1 个糖果(res++)。

为什么只用补一个,而不是整个坡的:
整个下坡的糖果其实已经在 res += down 里全部加过了!唯一被遗漏的,只有“山峰”这单个节点。

  • 平地ratings[i] == ratings[i-1]):
    • 相邻评分相等时无递增递减约束,重置 up = 0, down = 0, peak = 0,给基础 1 个糖果(res += 1)。

实现

function candy(ratings: number[]): number {
    const n = ratings.length;
    if (n <= 1) return n;

    let res = 1;   // 默认第 1 个孩子分 1 个糖果
    let up = 0;    // 当前连续上坡长度
    let down = 0;  // 当前连续下坡长度
    let peak = 0;  // 最近一次上坡达到的山峰高度 

    for (let i = 1; i < n; i++) {
        if (ratings[i] > ratings[i - 1]) {
            // 1. 上坡:增长上坡高度,更新山峰
            up++;
            down = 0;
            peak = up;
            res += 1 + up;
        } else if (ratings[i] < ratings[i - 1]) {
            // 2. 下坡:累加下坡段增量,下坡超长时给山峰补 1
            down++;
            up = 0;
            if (down > peak) {
                res++; // 修正顶点
            }
            res += down;
        } else {
            // 3. 平地:状态重置,给基础 1 个糖果
            up = 0;
            down = 0;
            peak = 0;
            res += 1;
        }
    }

    return res;
}
public class Solution {
    public int Candy(int[] ratings) {
        int n = ratings.Length;
        if (n <= 1) return n;

        int res = 1;   // 默认第 1 个孩子分 1 个糖果
        int up = 0;    // 当前连续上坡长度
        int down = 0;  // 当前连续下坡长度
        int peak = 0;  // 最近一次上坡达到的山峰高度

        for (int i = 1; i < n; i++) {
            if (ratings[i] > ratings[i - 1]) {
                // 1. 上坡情况:累加 1 + up
                up++;
                down = 0;
                peak = up;
                res += 1 + up;
            } else if (ratings[i] < ratings[i - 1]) {
                // 2. 下坡情况:累加 down,超出山峰时补 1
                down++;
                up = 0;
                if (down > peak) {
                    res++; // 顶点修正
                }
                res += down;
            } else {
                // 3. 平地情况:重置坡度,给 1 个糖果
                up = 0;
                down = 0;
                peak = 0;
                res += 1;
            }
        }

        return res;
    }
}
class Solution {
public:
    int candy(vector<int>& ratings) {
        int n = ratings.size();
        if (n <= 1) return n;

        int res = 1;   // 默认第 1 个孩子分 1 个糖果
        int up = 0;    // 当前连续上坡长度
        int down = 0;  // 当前连续下坡长度
        int peak = 0;  // 最近一次上坡达到的山峰高度

        for (int i = 1; i < n; ++i) {
            if (ratings[i] > ratings[i - 1]) {
                // 1. 上坡情况:更新山峰并累加当前上坡高度糖果
                up++;
                down = 0;
                peak = up;
                res += 1 + up;
            } else if (ratings[i] < ratings[i - 1]) {
                // 2. 下坡情况:整体抬高下坡段,必要时修正山峰高度
                down++;
                up = 0;
                if (down > peak) {
                    res++; // 下坡长度反超山峰,顶点必须被迫补 1 个糖果
                }
                res += down; // 下坡段每个人整体增量累加
            } else {
                // 3. 平地情况:无需满足连续递增/递减,重置状态并给基础 1 个糖果
                up = 0;
                down = 0;
                peak = 0;
                res += 1;
            }
        }

        return res;
    }
};

复杂度分析

  • 时间复杂度\(O(N)\)
  • 空间复杂度\(O(1)\)

🤯拓展

引用

[1] 力扣探索模式


注:本文为个人学习与刷题笔记,部分文本结构与排版格式由 AI 辅助整理。

posted @ 2026-09-01 15:59  SEHOD  阅读(5)  评论(0)    收藏  举报