基本题目

135. 分发糖果

贪心:两次遍历,分别确定局部递增与递减情况下的糖果数

正确性证明:第一次遍历一定可以确定局部递增。第二次逆向遍历时,取新数值与原数值的较大值,不仅可以保证最终的数值比原数值大(满足可能存在的递增条件),也可以保证比新数值大(满足可能存在的递减条件)。

class Solution {
public:
    int candy(vector<int>& ratings) {
        int n = ratings.size();
        vector<int> candies(n, 1);

        for (int i = 1; i < n; i++) {
            if (ratings[i] > ratings[i - 1])
                candies[i] = candies[i - 1] + 1;
        }
        
        int sum = 0;
        for (int i = n - 1; i > 0; i--) {
            sum += candies[i];
            if (ratings[i] < ratings[i - 1])
                candies[i - 1] = max(candies[i - 1], candies[i] + 1);
        }

        return sum + candies[0];
    }
};

860. 柠檬水找零

通用解法:从大额到小额找零

class Solution {
public:
    bool change(const vector<int> &billValues, vector<int> &billCount, int value) {
        int m = billValues.size(), tmp;
        for (int i = m - 1; i >= 0; i--) {
            tmp = min(billCount[i], value / billValues[i]);
            billCount[i] -= tmp;
            value -= tmp * billValues[i];
        }
        return value == 0;
    }

    bool lemonadeChange(vector<int>& bills) {
        vector<int> billValues{5, 10, 20}, billCount{0, 0, 0};
        for (const int bill : bills) {
            billCount[bill == 10 ? 1 : (bill < 10 ? 0 : 2)]++;
            if (!change(billValues, billCount, bill - 5)) return false;
        }
        return true;
    }
};

简单情形:直接讨论

class Solution {
public:
    bool lemonadeChange(vector<int>& bills) {
        int five = 0, ten = 0;
        for (const int bill : bills) {
            if (bill == 5) five++;
            else if (bill == 10) {
                if (!five) return false;
                five--;
                ten++;
            }
            else {
                if (!ten) {
                    if (five < 3) return false;
                    five -= 3;
                }
                else {
                    if (!five) return false;
                    five--;
                    ten--;
                }
            }
        }
        return true;
    }
};

406. 根据身高重建队列

贪心:先安排身高矮的再安排高的,这样只需要数空位就可以。空位不包含相等元素,因此在初始排序时,相等元素数值更大的在前(从后向前排)

class Solution {
public:
    vector<vector<int>> reconstructQueue(vector<vector<int>>& people) {
        sort(people.begin(), people.end(), [](const vector<int> &p1, const vector<int> &p2) {
            return p1[0] != p2[0] ? p1[0] < p2[0] : p1[1] > p2[1];
        });

        vector<vector<int>> result(people.size());
        for (const vector<int> p : people) {
            int i = -1, count = p[1];
            while (count >= 0) {
                while (result[++i].size());
                count--;
            }
            result[i] = p;
        }

        return result;
    }
};

也可以先安排身高高的再安排矮的,这样后续插入的人不会影响已插入人的计数,只需要满足新人位置正确即可。对于相等元素,保证数值大的在后插入即可(从前向后排)

452. 用最少数量的箭引爆气球

贪心:按起始坐标排序后,每次只选择有交集的、最多的若干气球一同戳破

正确性证明:若对于连续的气球 a,b,c,若选择跳过 b,那么说明 b 的终止坐标小于 c 的起始坐标,因此这样之后一定会使用单独的一次机会来引爆 b。在这种情况下,b 和 c 一定是分别引爆的,因此让 b 在引爆 a 时一同引爆会取得相同的效果。

class Solution {
public:
    int findMinArrowShots(vector<vector<int>>& points) {
        sort(points.begin(), points.end());
        
        int count = 0;
        int i = 0, n = points.size();
        pair<int, int> interval;
        while (i < n) {
            interval = {points[i][0], points[i][1]};

            // Narrow the interval with consecutive balloons
            while (interval.first <= interval.second && ++i < n) {
                interval.first = max(interval.first, points[i][0]);
                interval.second = min(interval.second, points[i][1]);
            }

            count++;
        }
        return count;
    }
};

另一贪心思路:按终止坐标排序后,每次射剩余气球的最小终止坐标即可,后续跳过已引爆的气球

正确性证明:首先,为了引爆终止坐标最小的气球,显然最好选择最右侧坐标,这是因为选择更小的坐标不可能击中更多气球(已排序)。其次,可以直接将起始坐标不超过上一次射箭坐标的气球直接跳过,这是因为其终止坐标一定大于等于上一次射箭的坐标(否则会基于它选择射箭位置)。

class Solution {
public:
    int findMinArrowShots(vector<vector<int>>& points) {
        sort(points.begin(), points.end(), [](const vector<int> &p1, const vector<int> &p2) {
            return p1[1] < p2[1];
        });
        
        int count = 1;
        int last = points[0][1];
        for (const vector<int> &ballon : points) {
            if (ballon[0] <= last) continue;
            last = ballon[1];
            count++;
        }
        return count;
    }
};

435. 无重叠区间

贪心:按起始点从小到大排序后,依次处理:若终点位于之前的区间内,则使用该区间替代最后的一个,缩小已选择区间的整体范围;若起点位于之前的区间之外,则加入该区间

正确性证明:新加入的区间必然可以替代最后一个选择的区间,这是因为其起点位于后者起点之后。因此只需要考虑何时替代、何时加入即可。

class Solution {
public:
    int eraseOverlapIntervals(vector<vector<int>>& intervals) {
        sort(intervals.begin(), intervals.end());
        
        int count = 0, end = INT_MIN;
        for (const vector<int> &v : intervals) {
            if (v[1] < end) end = v[1];
            else if (v[0] >= end) {
                end = v[1];
                count++;
            }
        }
        return intervals.size() - count;
    }
};

贪心算法通用处理手段:从结果反推

  • 如何选择第一个区间(最左侧)?选择一个区间终点最小的即可,否则总是可以将它替换。
  • 如何选择下一个?忽略区间起点落在所有已选择区间的最大终点左侧的区间(按这种方式选择,其左侧不可能存在完整区间),选择剩余区间中终点最小的。

这种思路与引爆气球的题目十分类似:

  • 如何选择第一次引爆的位置?选择范围终点最小的气球,在其右端点发射。该气球必然要被引爆,而位置越靠右就越可能击中更多的气球。
  • 如何选择下一个?忽略区间起点落在上一次发射位置左侧的气球(按这种方式发射,其左侧不可能存在气球的左端点),选择剩余气球中终点最小的。
class Solution {
public:
    int eraseOverlapIntervals(vector<vector<int>>& intervals) {
        sort(intervals.begin(), intervals.end(), [](const vector<int> &v1, const vector<int> &v2) {
            return v1[1] < v2[1];
        });

        int count = 0, end = INT_MIN;
        for (const vector<int> &v : intervals) {
            if (v[0] >= end) {
                end = v[1];
                count++;
            }
        }
        return intervals.size() - count;
    }
};

763. 划分字母区间

贪心:对于每一个读取到的每一个字母,在其最后一次出现的位置之前,都不会考虑划分

class Solution {
public:
    vector<int> partitionLabels(string s) {
        int lastAppear[26] = {0};
        for (int i = 0; i < s.size(); i++) {
            lastAppear[s[i] - 'a'] = i;
        }

        vector<int> result;
        int begin = -1, end = 0;
        for (int i = 0; i < s.size(); i++) {
            end = max(end, lastAppear[s[i] - 'a']);
            if (i == end) {
                result.push_back(end - begin);
                begin = end;
            }
        }
        return result;
    }
};

56. 合并区间

贪心:按区间起点排序后,每次处理下一区间时延申区间长度,直到区间起点未被覆盖

class Solution {
public:
    vector<vector<int>> merge(vector<vector<int>>& intervals) {
        sort(intervals.begin(), intervals.end());

        vector<vector<int>> result;
        int start = intervals[0][0], end = intervals[0][1];
        for (const vector<int> &interval : intervals) {
            if (interval[0] > end) {
                result.push_back({start, end});
                start = interval[0];
            }
            end = max(end, interval[1]);
        }
        result.push_back({start, end});
        return result;
    }
};

738. 单调递增的数字

贪心:从低到高比较,若出现非法顺序,则将高位减 1,低位全取 9

class Solution {
public:
    int monotoneIncreasingDigits(int n) {
        string digits = to_string(n);

        int highestWild = digits.size();
        for (int i = digits.size() - 2; i >= 0; i--) {
            if (digits[i] > digits[i + 1]) {
                digits[i]--;
                highestWild = i;
            }
        }
        for (int i = digits.size() - 1; i > highestWild; i--) {
            digits[i] = '9';
        }

        return stoi(digits);
    }
};

968. 监控二叉树

贪心思想:叶子节点放监控是浪费的,永远都应当选择叶子节点的父节点(父节点与叶子节点显然需要二选一)。以此为基础,向上递归即可,传递节点的状态

class Solution {
public:
    int minCameraCover(TreeNode* root) {
        int result = 0;
        if (!solve(root, result)) result++;
        return result;
    }

    int solve(TreeNode *root, int &result) {
        if (!root) return 2;  // leaf node would return 0 in this way
        
        // State is returned: 0-Uncovered, 1-Selected, 2-Covered
        int left = solve(root->left, result);
        int right = solve(root->right, result);
        if (left == 0 || right == 0) {
            result++;
            return 1;
        }
        if (left == 1 || right == 1) return 2;
        return 0;
    }
};