3.19-贪心

121. 买卖股票的最佳时机 - 力扣(LeetCode)

class Solution {
  public:
      int maxProfit(vector<int>& prices) {
         int res = 0;
         int min = INT_MAX;
         for(int& price : prices){
           if(price < min) {
             min = price;
             continue;
            }            
            res = max(res , price - min);
      }
      return res;
  };

55. 跳跃游戏 - 力扣(LeetCode)

思路:

  • 先看示例 2,nums=[3,2,1,0,4]。我们在遍历数组的同时,维护最右可以到达的位置 mx,如下表:
    从 0 可以跳到 1,2,3,但是无法从 1,2,3 中的任何位置跳到 4。当我们遍历到 i=4 时,发现
i nums[i] i+nums[i] mx
0 3 3 3
1 2 3 3
2 1 3 3
3 0 3 3
4 4 8 失败

i>mx这意味着 i 是无法到达的,返回 false。

  • 然后来看示例 1,nums=[2,3,1,1,4],在遍历数组的同时,维护最远可以到达的位置 mx,如下表:
i nums[i] i+nums[i] mx
0 2 2 2
1 3 4 4
2 1 3 4
3 1 4 4
4 4 8 8

从 0 可以跳到 1,2,最远可以到达的位置 mx=2。能否跳到更远的位置?那就看从 1 能跳到哪些位置,从 2 能跳到哪些位置。

从 1 可以跳到 2,3,4,mx 更新成 4。

从 2 可以跳到 3,mx 不变。

从 3 可以跳到 4,mx 不变。

到达 4,返回 true。


一般地,算法如下:

  1. 从左到右遍历 nums,同时维护能跳到的最远位置 mx,初始值为 0。
  2. 如果 i> mx,说明无法跳到 i,返回 false。
  3. 否则,用 i+nums[i] 更新 mx 的最大值。如果循环中没有返回 false,那么最后返回 true。

另一种理解方式是,把每个 nums[i]看成闭区间 [i,i+nums[i]],问题变成判定这 n 个区间能否合并成一个大区间,这可以用 56. 合并区间 的算法解决。

class Solution {
public:
    bool canJump(vector<int>& nums) {
        int mx = 0;
        for (int i = 0; i < nums.size(); i++) {
            if (i > mx) { // 无法到达 i
                return false;
            }
            mx = max(mx, i + nums[i]); // 从 i 最右可以跳到 i + nums[i]
        }
        return true;
    }
};

45. 跳跃游戏 II - 力扣(LeetCode)

思路:

45-c.png

答疑

问:为什么代码只需要遍历到 n−2?

答:当 i = n−2 时,如果 i < curRight,说明可以到达 n−1;如果 i = curRight,我们会造桥,这样也可以到达 n−1(题目保证一定能到)。所以无论是何种情况,都只需要遍历到 n−2。或者说,n−1 已经是终点了,你总不能在终点还打算造桥吧?

问:我能想出这题的思路,就是代码实现总是写不对,有没有什么建议?

答:清晰的变量名以及一些必要的注释,会对理清代码逻辑有帮助。在出现错误时,可以用一些小数据去运行你的代码,通过 print 或者打断点的方式,查看这些关键变量的值,看看是否与预期结果一致。

问:如果题目没有保证一定能到达 n−1,代码要怎么改?

答:见 1326. 灌溉花园的最少水龙头数目,我的题解。

class Solution {
public:
    int jump(vector<int>& nums) {
        int ans = 0;
        int cur_right = 0; // 已建造的桥的右端点
        int next_right = 0; // 下一座桥的右端点的最大值
        for (int i = 0; i + 1 < nums.size(); i++) {
            // 遍历的过程中,记录下一座桥的最远点
            next_right = max(next_right, i + nums[i]);
            if (i == cur_right) { // 无路可走,必须建桥
                cur_right = next_right; // 建桥后,最远可以到达 next_right
                ans++;
            }
        }
        return ans;
    }
};

1326. 灌溉花园的最少水龙头数目 - 力扣(LeetCode)

和上一题的区别在于,水龙头的覆盖范围为[i - nums[i] , i + nums[i]],意味着我更新桥的时候可以一步从i - nums[i]处走到i + nums[i]处,也即

next_right = max(next_right, right_most[i]);

所以先预处理一下每个下标i能走到的最右端点:

  // 预处理:记录每个左端点能覆盖到的最远右端点
        vector<int> right_most(n + 1);  // 下标为左端点,值为能覆盖的最远右端点
        for (int i = 0; i <= n; i++) {
            int left = max(i - r, 0);   // 覆盖区间的左端点(不能为负数)
            right_most[left] = max(right_most[left], i + ranges[i]); // 更新左端点能覆盖的最远右端点
        }

问:为什么没有初始化 rightMost[i]=i

答:如果可以走到 i,那么 nextRight≥i,此时 rightMost[i]=i 还是 =0 都不会影响 nextRight 的值,也就没必要初始化 rightMost[i]=i 了。

class Solution {
public:
    int minTaps(int n, vector<int>& ranges) {
        // 预处理:记录每个左端点能覆盖到的最远右端点
        vector<int> right_most(n + 1);  // 下标为左端点,值为能覆盖的最远右端点
        for (int i = 0; i <= n; i++) {
            int left = max(i - ranges[i], 0);   // 覆盖区间的左端点(不能为负数)
             right_most[left] = max(right_most[left], i + ranges[i]); // 更新左端点能覆盖的最远右端点
        }

        int ans = 0;            // 需要的水龙头数量
        int cur_right = 0;       // 当前已覆盖的最远右端点
        int next_right = 0;      // 下一步可能覆盖的最远右端点
        // 遍历每个位置(只需到 n-1,因为最终要覆盖到 n)
        for (int i = 0; i < n; i++) {
            next_right = max(next_right, right_most[i]); // 更新下一步能覆盖的最远右端点,这一步和上一题有区别
            if (i == cur_right) { // 走到当前覆盖的最右端,必须新增一个水龙头
                if (i == next_right) { 
                    // 下一步无法扩展覆盖范围,无法到达终点
                    return -1;
                }
                cur_right = next_right; // 更新当前覆盖的最右端
                ans++;                  // 新增一个水龙头
            }
        }
        return ans;
    }
};

763. 划分字母区间 - 力扣(LeetCode)

思路:

「同一字母最多出现在一个片段中」意味着,一个片段若要包含字母 a,那么所有的字母 a 都必须在这个片段中。

例如示例 1,s=ababcbacadefegdehijhklij,其中字母 a 的下标在区间 [0,8] 中,那么包含 a 的片段至少要包含区间 [0,8]。

把所有出现在 s 中的字母及其下标区间列出来:

字母 下标 下标区间
a [0,2,6,8] [0,8]
b [1,3,5] [1,5]
c [4,7] [4,7]
d [9,14] [9,14]
e [10,12,15] [10,15]
f [11] [11,11]
g [13] [13,13]
h [16,19] [16,19]
i [17,22] [17,22]
j [18,23] [18,23]
k [20] [20,20]
l [21] [21,21]

例如字母 d 的区间为 [9,14],片段要包含 d,必须包含区间 [9,14],但区间 [9,14] 中还有其它字母 e,f,g,所以该片段也必须包含这些字母对应的区间 [10,15],[11,11],[13,13],合并后得到区间 [9,15]。

将表格中的区间合并为如下几个大区间:

[0,8],[9,15],[16,23]
这些区间满足「同一字母最多出现在一个片段中」的要求。

由于题目要求划分出尽量多的片段,而我们又无法将上述区间的任何区间划分开,所以合并出的区间长度即为答案。

算法

  1. 遍历 s,计算字母 c 在 s 中的最后出现的下标 last[c]。
  2. 初始化当前正在合并的区间左右端点 start=0, end=0。
  3. 再次遍历 s,由于当前区间必须包含所有 s[i],所以用 last[s[i]] 更新区间右端点 end 的最大值。
  4. 如果发现 end=i,那么当前区间合并完毕,把区间长度 end−start+1 加入答案。然后更新 start=i+1 作为下一个区间的左端点。
  5. 遍历完毕,返回答案。

if (end == i) 这一步与上面题的「不得不建桥」相似

class Solution {
public:
    vector<int> partitionLabels(string s) {
        int n = s.length();
        int last[26];
        for (int i = 0; i < n; i++) {
            last[s[i] - 'a'] = i; // 每个字母最后出现的下标
        }

        vector<int> ans;
        int start = 0, end = 0;
        for (int i = 0; i < n; i++) {
            end = max(end, last[s[i] - 'a']); // 更新当前区间右端点的最大值
            if (end == i) { // 当前区间合并完毕
                ans.push_back(end - start + 1); // 区间长度加入答案
                start = i + 1; // 下一个区间的左端点
            }
        }
        return ans;
    }
};

复杂度分析

  • 时间复杂度:O(n),其中 n 是 s 的长度。
  • 空间复杂度:O(∣Σ∣)。其中 ∣Σ∣ 是字符集合的大小,本题字符均为小写字母,所以 ∣Σ∣=26。
posted @ 2025-03-20 01:35  七龙猪  阅读(1)  评论(0)    收藏  举报
-->