基本题目

455. 分发饼干

贪心原则:将胃口大小从小到大排序,先给胃口最小的人尝试分配最小的饼干

class Solution {
public:
    int findContentChildren(vector<int>& g, vector<int>& s) {
        sort(g.begin(), g.end());
        sort(s.begin(), s.end());

        int i = 0, j = 0, r = 0;
        while (i < g.size() && j < s.size()) {
            if (g[i] <= s[j]) {
                i++;
                j++;
                r++;
            }
            else {
                j++;
            }
        }
        return r;
    }
};

376. 摆动序列

贪心原则:将数组分为若干段单调子序列,每次只取子序列的两端,也即判断有几个转折点

class Solution {
public:
    int wiggleMaxLength(vector<int>& nums) {
        if (nums.size() <= 1) return 1;

        int up = 0;  // 0-equal, 1-up, -1-down
        int count = 1;
        for (int i = 1; i < nums.size(); i++) {
            if (nums[i] == nums[i - 1]) continue;
            if (!up || (nums[i] - nums[i - 1]) * up < 0) {
                count++;
                up = nums[i] - nums[i - 1];
            }
        }
        return count;
    }
};

也可以使用动态规划的思路求解,分别追踪最长的、末尾趋势为递增/递减的摆动序列

53. 最大子序和

经典解法:动态规划(以此为末尾元素的最大子序和)

class Solution {
public:
    int maxSubArray(vector<int>& nums) {
        int curr = 0, max = INT_MIN, n = nums.size();
        for (const int n : nums) {
            curr = curr > 0 ? curr + n : n;
            if (curr > max) max = curr;
        }
        return max;
    }
};

本题也可以使用贪心思想:什么时候舍弃负数?当且仅当它前面的正数不足以弥补该负数。因此可以从头开始累加,每次累加到负数,就将累加器清零,重新开始求和

此方法的证明过程见后文 [[#134. 加油站]]。

class Solution {
public:
    int maxSubArray(vector<int>& nums) {
        int sum = 0, maxSum = INT_MIN;
        for (const int n : nums) {
            sum += n;
            if (sum > maxSum) maxSum = sum;
            if (sum < 0) sum = 0;
        }
        return maxSum;
    }
};

122. 买卖股票的最佳时机 II

将持有股票的时间段划分为长度为 1 的小区间,这样只需要找到那些递增的相邻价格即可。也可以这样考虑:每天都买入股票,可以选择当天卖出,也可以第二天卖出

class Solution {
public:
    int maxProfit(vector<int>& prices) {
        if (prices.size() == 1) return 0;
        int sum = 0;
        for (int i = 1; i < prices.size(); i++) {
            if (prices[i] > prices[i - 1])
                sum += (prices[i] - prices[i - 1]);
        }
        return sum;
    }
};

也可以使用动态规划解决,具体参考后续章节

55. 跳跃游戏

贪心:维护最远到达的位置

class Solution {
public:
    bool canJump(vector<int>& nums) {
        int n = nums.size(), maxId = 0;
        for (int i = 0; i < n && i <= maxId; i++) {
            if (i + nums[i] > maxId) {
                maxId = i + nums[i];
            }
        }
        return maxId >= n - 1;
    }
};

45. 跳跃游戏 II

从前往后扩展:维护一个可以达到的范围,每次遍历其中的元素,将该范围向后延申,这样每次延伸部分所需要的跳跃次数就增加 1。直到达到最后一个元素为止

class Solution {
public:
    int jump(vector<int>& nums) {
        int n = nums.size(), prev = 0, maxId = 0, maxStep = 1;
        if (n == 1) return 0;
        for (int i = 0; i < n && maxId < n - 1; i++) {
            if (i > prev) {
                // Reach another extension
                prev = maxId;
                maxStep++;
            }
            if (nums[i] + i > maxId) {
                // Extend
                maxId = nums[i] + i;
            }
        }
        return maxStep;
    }
};

1005. K 次取反后最大化的数组和

朴素贪心思想:每次取所有数中最小的,反转符号再放回数组中

class Solution {
public:
    int largestSumAfterKNegations(vector<int>& nums, int k) {
        priority_queue<int, vector<int>, greater<int>> q;
        for (const int n : nums) q.push(n);

        int v;
        for (int i = 0; i < k; i++) {
            v = -q.top();
            q.pop();
            q.push(v);
        }

        v = 0;
        while (!q.empty()) {
            v += q.top();
            q.pop();
        }

        return v;
    }
};

注意到,当 k 的值超过负数的数量时,在反转所有负数的符号之后,可以直接考虑剩余 k 的奇偶性,再操作最小的数即可

class Solution {
public:
    int largestSumAfterKNegations(vector<int>& nums, int k) {
        sort(nums.begin(), nums.end());

        int sum = 0, n = nums.size();
        int minNum = INT_MAX;
        for (int num : nums) {
            if (k > 0 && num < 0) {
                num = -num;
                k--;
            }
            if (num < minNum) minNum = num;
            sum += num;
        }

        if (k % 2) sum -= 2 * minNum;
        return sum;
    }
};

134. 加油站

与最大子序和类似,本题要找一个起始点,从此开始的任意前缀和都非负。可以环形遍历数组,累加元素,遇到和为负则清零,直到区间首尾相接(满足题意)或区间起点超限。

class Solution {
public:
    int canCompleteCircuit(vector<int>& gas, vector<int>& cost) {
        int n = gas.size();
        vector<int> nums(n);
        for (int i = 0; i < n; i++) nums[i] = gas[i] - cost[i];

        int i = -1, j = 0, sum = 0;
        while (i < n - 1) {
            // (i, j] sum >= 0
            sum += nums[j % n];
            if (sum < 0) {
                sum = 0;
                i = j;
            }
            if (j >= i + n) return i + 1;
            j++;
        }
        return -1;
    }
};

在官方题解中给出了具体证明,解释了为何遇到和为负时直接从下一个加油站开始检查:

假设有数组 $a[1..n]$,其中按照上述方法找到了一个局部最长的和非负子序列 $a[i..j]$,则 \(\sum\limits_{t=i}^{k}a_{t} \ge 0,\forall i \le k \le j\quad \sum\limits_{t=i}^{j+1}a_{t} <0\) 因此对于起始位置为 $k$ 的子序列,若 $i< k \le j$,则 \(\sum\limits_{t=k}^{j+1}a_{t}=\sum\limits_{t=i}^{j+1}a_{t} - \sum\limits_{t=i}^{k-1}a_{t} < 0\) 因此其满足条件的子序列一定更短,不再需要考虑。因此接下来考虑从 $j+1$ 开始的序列即可。