【Leetcode刷题】贪心算法

本篇文章为LeetCode 贪心算法模块的刷题笔记,仅供参考。

目录

  • Leetcode31.下一个排列
  • Leetcode55.跳跃游戏
  • Leetcode45.跳跃游戏 II
  • Leetcode134.加油站
  • Leetcode179.最大数
  • Leetcode213.打家劫舍 II
  • Leetcode334.递增的三元子序列
  • Leetcode376.摆动序列

Leetcode31.下一个排列

Leetcode31. 下一个排列
整数数组的一个 排列 就是将其所有成员以序列或线性顺序排列。
例如,arr = [1,2,3],以下这些都可以视作arr的排列:[1,2,3]、[1,3,2]、[3,1,2]、[2,3,1] 。
整数数组的 下一个排列 是指其整数的下一个字典序更大的排列。更正式地,如果数组的所有排列根据其字典顺序从小到大排列在一个容器中,那么数组的 下一个排列 就是在这个有序容器中排在它后面的那个排列。如果不存在下一个更大的排列,那么这个数组必须重排为字典序最小的排列(即,其元素按升序排列)。
例如,arr = [1,2,3] 的下一个排列是 [1,3,2] 。
类似地,arr = [2,3,1] 的下一个排列是 [3,1,2] 。
而 arr = [3,2,1] 的下一个排列是 [1,2,3] ,因为 [3,2,1] 不存在一个字典序更大的排列。
给你一个整数数组 nums ,找出 nums 的下一个排列。
必须 原地 修改,只允许使用额外常数空间。
示例 1:
输入:nums = [1,2,3]
输出:[1,3,2]
示例 2:
输入:nums = [3,2,1]
输出:[1,2,3]
示例 3:
输入:nums = [1,1,5]
输出:[1,5,1]
提示:
1 <= nums.length <= 100
0 <= nums[i] <= 100

其实很像二进制编码,本质就是找nums[i],思路就是从右向左寻找第一个 i=n 满足 nums[n] 的情况,因为其右边都满足 nums[i]>=nums[i+1],因此将 nums[n] 后第一个比 n 大的数与 nums[n] 交换,然后 nums[n] 后的数升序排列即可:

class Solution {
public:
    void nextPermutation(vector<int>& nums) {
        bool flag=0;
        int i;
        for(i=nums.size()-2;i>=0;i--){
            if(nums[i]<nums[i+1]){
                flag=1;
                break;
            }
        }
        if(!flag){
            sort(nums.begin(),nums.end());
            return;
        }
        for(int j=nums.size()-1;j>=i+1;j--){
            if(nums[j]>nums[i]){
                int tmp=nums[i];
                nums[i]=nums[j];
                nums[j]=tmp;
                break;
            }
        }
        sort(nums.begin()+i+1,nums.end());
        return;
    }
};
【Leetcode刷题】贪心算法_第1张图片

Leetcode55.跳跃游戏

Leetcode55.跳跃游戏
给定一个非负整数数组 nums ,你最初位于数组的 第一个下标 。
数组中的每个元素代表你在该位置可以跳跃的最大长度。
判断你是否能够到达最后一个下标。
示例 1:
输入:nums = [2,3,1,1,4]
输出:true
解释:可以先跳 1 步,从下标 0 到达下标 1, 然后再从下标 1 跳 3 步到达最后一个下标。
示例 2:
输入:nums = [3,2,1,0,4]
输出:false
解释:无论怎样,总会到达下标为 3 的位置。但该下标的最大跳跃长度是 0 , 所以永远不可能到达最后一个下标。
提示:
1 <= nums.length <= 3 * 104
0 <= nums[i] <= 105

法一:本题可以直接 dfs 暴力求解,用时惨淡:

class Solution {
public:
    void dfs(vector<int>& nums,vector<bool>& visit,int start){
        int n=nums.size();
        int next=start+nums[start];
        for(int i=start+1;i<min(n,next+1);i++){
            if(!visit[i]){
                visit[i]=true;
                dfs(nums,visit,i);
            }
        }
        return;
    }
    bool canJump(vector<int>& nums) {
        int n=nums.size();
        vector<bool> visit(n);  //是否访问过
        for(int i=0;i<n;i++)    visit[i]=false;
        visit[0]=true;
        dfs(nums,visit,0);
        return visit[n-1];
    }
};
【Leetcode刷题】贪心算法_第2张图片

法二:利用贪心法则,遍历数组维护数组能够到达的最远的点 furthest。对于数组的某一个位置 x,它所能到达的点的范围为 [x,x+nums[x]]。因此遍历数组,持续更新 furthest:

  1. 若 i>furthest,则i超出了furthest能够到达的范围,直接返回 false;
  2. 若 i<=furthest,计算 i 能够到达的范围并更新 furthest;
  3. 若 i+nums[i]>=n-1,直接返回 true
class Solution {
public:
    bool canJump(vector<int>& nums) {
        int furthest=0;
        int n=nums.size();
        for(int i=0;i<n;i++){
            if(i<=furthest){
                int tmp=i+nums[i];
                if(tmp>=n-1)    return true;
                furthest=max(furthest,tmp);
            }else{
                return false;
            }
        }
        return true;
    }
};
【Leetcode刷题】贪心算法_第3张图片

Leetcode45.跳跃游戏 II

Leetcode45.跳跃游戏 II
给你一个非负整数数组 nums ,你最初位于数组的第一个位置。
数组中的每个元素代表你在该位置可以跳跃的最大长度。
你的目标是使用最少的跳跃次数到达数组的最后一个位置。
假设你总是可以到达数组的最后一个位置。
示例 1:
输入: nums = [2,3,1,1,4]
输出: 2
解释: 跳到最后一个位置的最小跳跃数是 2。
从下标为 0 跳到下标为 1 的位置,跳 1 步,然后跳 3 步到达数组的最后一个位置。
示例 2:
输入: nums = [2,3,0,1,4]
输出: 2
提示:
1 <= nums.length <= 104
0 <= nums[i] <= 1000

法一:本题也可以 bfs 求解,记数组 step[i] 表示从起点到达 i 的最少步数,遍历 nums 数组,将 step[i+1] 到 step[i+nums[i]] 标记为 min(step[i],step[i]+1) 即可:

class Solution {
public:
    int jump(vector<int>& nums) {
        int n=nums.size();
        vector<int> step(n);
        for(int i=0;i<n;i++){
            int tmp=nums[n];
            for(int j=i+1;j<=i+nums[i] && j<n;j++){
                if(step[j]==0)  step[j]=step[i]+1;
            }
        }
        return step[n-1];
    }
};
【Leetcode刷题】贪心算法_第4张图片

法二:法一属于暴力算法,求解了所有点的最短距离,下采用贪心思想:遍历数组 nums ,同时维护3个变量:下一步内可达的最远距离 furthest、当前 step 的边界 bound、当前步数 step。遍历数组的过程中,实际上用 bound 进行划分每一步,如果当前位置还在 bound 范围内,则可以:

  1. 若 i<=bound,即当前位置 i 还在 bound 内(当前 step 内可达),则只需要更新 furthest;
  2. 若 i>bound,则当前 step 内不可达,因此要将 bound 更新为 furthest,并将 step++.
class Solution {
public:
    int jump(vector<int>& nums) {
        int n=nums.size();
        int furthest=0;
        int step=0;
        int bound=0;
        for(int i=0;i<n;i++){ 
            if(bound>=n-1)   return step;
            if(i<=bound){
                int tmp=i+nums[i];
                furthest=max(furthest,tmp);
            }else{
                bound=furthest;
                step++;
                int tmp=i+nums[i];
                furthest=max(furthest,tmp);
            }
        }
        return step;
    }
};
【Leetcode刷题】贪心算法_第5张图片

Leetcode134.加油站

Leetcode134.加油站
在一条环路上有 n 个加油站,其中第 i 个加油站有汽油 gas[i] 升。
你有一辆油箱容量无限的的汽车,从第 i 个加油站开往第 i+1 个加油站需要消耗汽油 cost[i] 升。你从其中的一个加油站出发,开始时油箱为空。
给定两个整数数组 gas 和 cost ,如果你可以绕环路行驶一周,则返回出发时加油站的编号,否则返回 -1 。如果存在解,则 保证 它是 唯一 的。
示例 1:
输入: gas = [1,2,3,4,5], cost = [3,4,5,1,2]
输出: 3
解释:
从 3 号加油站(索引为 3 处)出发,可获得 4 升汽油。此时油箱有 = 0 + 4 = 4 升汽油
开往 4 号加油站,此时油箱有 4 - 1 + 5 = 8 升汽油
开往 0 号加油站,此时油箱有 8 - 2 + 1 = 7 升汽油
开往 1 号加油站,此时油箱有 7 - 3 + 2 = 6 升汽油
开往 2 号加油站,此时油箱有 6 - 4 + 3 = 5 升汽油
开往 3 号加油站,你需要消耗 5 升汽油,正好足够你返回到 3 号加油站。
因此,3 可为起始索引。
示例 2:
输入: gas = [2,3,4], cost = [3,4,3]
输出: -1
解释:
你不能从 0 号或 1 号加油站出发,因为没有足够的汽油可以让你行驶到下一个加油站。
我们从 2 号加油站出发,可以获得 4 升汽油。 此时油箱有 = 0 + 4 = 4 升汽油
开往 0 号加油站,此时油箱有 4 - 3 + 2 = 3 升汽油
开往 1 号加油站,此时油箱有 3 - 3 + 3 = 3 升汽油
你无法返回 2 号加油站,因为返程需要消耗 4 升汽油,但是你的油箱只有 3 升汽油。
因此,无论怎样,你都不可能绕环路行驶一周。
提示:
gas.length = n
cost.length = n
1 <= n <= 105
0 <= gas[i], cost[i] <= 104

最直接但是错误的想法是:如果可以走完一周,开始节点一定是 (gas[i]-cost[i]) 最大的节点。 反例:gas=[5,8,2,8],cost=[6,5,6,6]。

其实只要 Σgas ≥ Σ cost,总存在开始节点使得汽车成功环绕一周,这可以作为判断前提。不妨记 remain[i] = gas[i] - cost[i],表示走完每段路的剩余油量(负数表示要求走这段路之前就有库存)。如果某节点剩余油量为负,那么肯定不能作为开始节点;如果某节点开始的连续累计和一直非负,则可以作为开始节点。因此遍历 remain 数组,对 remain 计算累计和并记录开始节点,如果某节点的累计和为负,则重置初始节点和累计和。

class Solution {
public:
    int canCompleteCircuit(vector<int>& gas, vector<int>& cost) {
        int n=gas.size();
        int n_gas=0;
        int n_cost=0;
        vector<int> remain(n);
        for(int i=0;i<n;i++){
            n_gas+=gas[i];
            n_cost+=cost[i];
            remain[i]=gas[i]-cost[i];
        }
        if(n_gas<n_cost)    return -1;
        int start=0;
        int sum=0;
        for(int i=0;i<n;i++){
            sum+=remain[i];
            if(sum<0){
                start=(i+1)%n;
                sum=0;
            }
        }

        return start;
    }
};
【Leetcode刷题】贪心算法_第6张图片

Leetcode179.最大数

Leetcode179.最大数
给定一组非负整数 nums,重新排列每个数的顺序(每个数不可拆分)使之组成一个最大的整数。
注意:输出结果可能非常大,所以你需要返回一个字符串而不是整数。
示例 1:
输入:nums = [10,2]
输出:“210”
示例 2:
输入:nums = [3,30,34,5,9]
输出:“9534330”
提示:
1 <= nums.length <= 100
0 <= nums[i] <= 109

一开始考虑的是使用自定义优先级的优先队列存储字符串,每次弹出栈顶元素加入字符串。但当遇到 “31”、“3” 这种情况时,虽然下一字符串开头不会大于 3,但无法判断是 0,1 还是 2,因此无法判断使用哪个字符串进行拼接。

因此考虑比较 s1+s2 和 s2+s1,将大的设置为更高的优先级。提交时发现卡在测试点 [0, 0],“0” 上,提前判断即可:

class Solution {
public:
    struct cmp{
        bool operator() (string s1, string s2){
            return (s1+s2)<(s2+s1);
        }
    };

    string largestNumber(vector<int>& nums) {
        priority_queue<string,vector<string>,cmp> p;
        int n=nums.size();
        for(int i=0;i<n;i++){
            p.push(to_string(nums[i]));
        }
        string ans="";
        for(int i=0;i<n;i++){
            if(!(ans=="0" && p.top()=="0")){	// 防止 “00”
                ans+=p.top();
            }
            p.pop();
        }
        return ans;
    }
};
【Leetcode刷题】贪心算法_第7张图片

Leetcode213.打家劫舍 II

Leetcode213.打家劫舍 II
你是一个专业的小偷,计划偷窃沿街的房屋,每间房内都藏有一定的现金。这个地方所有的房屋都 围成一圈 ,这意味着第一个房屋和最后一个房屋是紧挨着的。同时,相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警 。
给定一个代表每个房屋存放金额的非负整数数组,计算你在不触动警报装置的情况下 ,今晚能够偷窃到的最高金额。
示例 1:
输入:nums = [2,3,2]
输出:3
解释:你不能先偷窃 1 号房屋(金额 = 2),然后偷窃 3 号房屋(金额 = 2), 因为他们是相邻的。
示例 2:
输入:nums = [1,2,3,1]
输出:4
解释:你可以先偷窃 1 号房屋(金额 = 1),然后偷窃 3 号房屋(金额 = 3)。偷窃到的最高金额 = 1 + 3 = 4 。
示例 3:
输入:nums = [1,2,3]
输出:3
提示:
1 <= nums.length <= 100
0 <= nums[i] <= 1000

本题是在 Leetcode198.打家劫舍 的基础上加上 房屋环形相连 的限制,原本首尾不相连的题目很容易采用动态规划解决,但加上首尾相连的限制后,子问题的不相关性被打破。例如 {1,2,3},计算到 3 时其最优解受 1 影响,不再独立。后来在评论区看到一句评论茅塞顿开:“其实就是 把环拆成两个队列,一个是从 0 到 n-1,另一个是从 1 到 n,然后返回两个结果中最大的。”

class Solution {
public:
    int help(vector<int>& nums) {
        int n=nums.size();
        if(n==1)    return nums[0];

        vector<int> dp(n);      //dp[i]表示盗取前i家能够获得的最高金额
        vector<bool> flag(n);   //是否盗取第i家
        dp[0]=nums[0];
        flag[0]=true;
        if(nums[0]>nums[1]){
            dp[1]=nums[0];
            flag[1]=false;
        }else{
            dp[1]=nums[1];
            flag[1]=true;
        }
        for(int i=2;i<n;i++){
            if(!flag[i-1]){
                dp[i]=dp[i-1]+nums[i];
                flag[i]=true;
            }else{
                if(dp[i-1]>dp[i-2]+nums[i]){
                    dp[i]=dp[i-1];
                    flag[i]=false;
                }else{
                    dp[i]=dp[i-2]+nums[i];
                    flag[i]=true;
                }
            }
        }
        return dp[n-1];
    }
    int rob(vector<int>& nums) {
        int n=nums.size();
        if(n==1)    return nums[0];

        vector<int> v1(nums.begin(),nums.end()-1);
        vector<int> v2(nums.begin()+1,nums.end());

        return max(help(v1),help(v2));
    }
};
【Leetcode刷题】贪心算法_第8张图片

Leetcode334.递增的三元子序列

Leetcode334.递增的三元子序列
给你一个整数数组 nums ,判断这个数组中是否存在长度为 3 的递增子序列。
如果存在这样的三元组下标 (i, j, k) 且满足 i < j < k ,使得 nums[i] < nums[j] < nums[k] ,返回 true ;否则,返回 false 。
示例 1:
输入:nums = [1,2,3,4,5]
输出:true
解释:任何 i < j < k 的三元组都满足题意
示例 2:
输入:nums = [5,4,3,2,1]
输出:false
解释:不存在满足题意的三元组
示例 3:
输入:nums = [2,1,5,0,4,6]
输出:true
解释:三元组 (3, 4, 5) 满足题意,因为 nums[3] = 0 < nums[4] = 4 < nums[5] = 6
提示:
1 <= nums.length <= 5 * 105
-231 <= nums[i] <= 231 - 1
三元子序列可以不连续

很容易想到维护两个变量来记录 min_nummid_num,然后遍历数组寻找合适的 max_num。在遍历数组时,如果:
(1)min_num < mid_num < nums[i],则已经找到长度为 3 的递增子序列。这是函数返回的唯一标准;
(2)min_num < nums[i] < mid_num,则更新 mid_num。此时相当于降低了 mid_num 的标准,后面如果有满足要求的递增子序列更容易满足;
(3)nums[i] < min_num < mid_num,则更新 min_num。此处不细看会认为有问题,因为 nums[i]min_nummid_num 后面出现,不满足序列的顺序。但 min_num 更新为 nums[i] 后隐藏着一个真相:min_nummid_num 之间还存在某个数(即原 min_num),当后面出现大于 mid_num 的数,可以满足(1)直接返回,并且该序列是顺序的;

如果还是对情况(3)存疑,可以再看下一个元素,下面举例证明,无非以下 3 种情况:
【Leetcode刷题】贪心算法_第9张图片

再来看 min_nummid_num 的初值:显然 mid_num 初值必须为 INT_MAX,否则直接进入(1)返回 true。若 min_num 初值为 INT_MIN,则第一次一定会进入(2),一旦 nums[1] > nums[0],就会进入(1)返回 true,不符合要求;若 min_num 初值为 INT_MAX,则第一次一定会进入(3),在 mid_num 被赋实值之前不会结束。

class Solution {
public:
    bool increasingTriplet(vector<int>& nums) {
        int n=nums.size();
        if(n<=2)    return false;
        int min_num=INT_MAX;
        int mid_num=INT_MAX;
        for(int i=0;i<n;i++){
            if(nums[i]<=min_num){
                min_num=nums[i];
            }else if(nums[i]>mid_num){
                return true;
            }else{
                mid_num=nums[i];
            }
        }
        return false;
    }
};
【Leetcode刷题】贪心算法_第10张图片

Leetcode376.摆动序列

Leetcode376.摆动序列
如果连续数字之间的差严格地在正数和负数之间交替,则数字序列称为 摆动序列 。第一个差(如果存在的话)可能是正数或负数。仅有一个元素或者含两个不等元素的序列也视作摆动序列。
例如, [1, 7, 4, 9, 2, 5] 是一个摆动序列,因为差值 (6, -3, 5, -7, 3) 是正负交替出现的。
相反,[1, 4, 7, 2, 5] 和 [1, 7, 4, 5, 5] 不是摆动序列,第一个序列是因为它的前两个差值都是正数,第二个序列是因为它的最后一个差值为零。
子序列 可以通过从原始序列中删除一些(也可以不删除)元素来获得,剩下的元素保持其原始顺序。
给你一个整数数组 nums ,返回 nums 中作为 摆动序列最长子序列的长度
示例 1:
输入:nums = [1,7,4,9,2,5]
输出:6
解释:整个序列均为摆动序列,各元素之间的差值为 (6, -3, 5, -7, 3) 。
示例 2:
输入:nums = [1,17,5,10,13,15,10,5,16,8]
输出:7
解释:这个序列包含几个长度为 7 摆动序列。
其中一个是 [1, 17, 10, 13, 10, 16, 8] ,各元素之间的差值为 (16, -7, 3, -3, 6, -8) 。
示例 3:
输入:nums = [1,2,3,4,5,6,7,8,9]
输出:2
提示:
1 <= nums.length <= 1000
0 <= nums[i] <= 1000

法一:动态规划
这题显然可以用动态规划,开两个二维数组 swing_len 和 sub:swing_len 存储 nums[ i…j ] 的最长摆动子序列的长度;sub 存储 nums[ i…j ] 中的最长摆动子序列的末尾两个元素的增 / 减情况,1 表示子序列末尾是上升的,-1 表示下降的。当数组 nums 持续上升或下降时,可以将其视为同一个点,只保持最高点或最低点作为当前摆动子序列的最后节点。

在动态规划过程中,nums[i…j] 由 nums[i…j-1] 得到:如果 nums[i…j-1] 中的最长递增子序列的末尾是递增或递减的,那么结合 nums[j-1] 和 nums[j] 的增减关系可以很容易判断出 swing_len[i][j] 和 sub[i][j] 的取值;如果 nums[i…j-1] 中的最长递增子序列的末尾是平的(即 nums[j-1] = nums[j-2]),则取 nums[i…j-2] 中的最长摆动子序列的末尾两个元素的增 / 减情况即可。如果开头出现 nums 持平不变的情况,可以单独赋值(本题中赋为 0)单独处理,表明可增可减。

nums 数组的最长摆动子序列长度为 swing_len[0][n-1]:

class Solution {
public:
    int wiggleMaxLength(vector<int>& nums) {
        int n=nums.size();
        vector<vector<int> > swing_len; //存储最长子序列长度
        vector<int> tmp1(n);
        swing_len.resize(n,tmp1);
        vector<vector<int> > sub;       //存储最长子序列的符号
        vector<int> tmp2(n);
        sub.resize(n,tmp2);

        for(int i=0;i<n;i++){
            for(int j=i;j<n;j++){
                if(i==j){   //初始情况
                    swing_len[i][j]=1;
                    sub[i][j]=0;
                }
                else{       //dp
                    //nums[i][j-1]的最长递增子序列末尾上升或下降
                    if(sub[i][j-1]==0){
                        if(nums[j]==nums[j-1]){
                            swing_len[i][j]=1;
                            sub[i][j]=0;
                        }else if(nums[j-1]<nums[j]){
                            swing_len[i][j]=2;
                            sub[i][j]=1;
                        }else{
                            swing_len[i][j]=2;
                            sub[i][j]=-1;
                        }
                    }
                    //nums[i][j-1]的最长递增子序列末尾上升
                    else if(sub[i][j-1]==1){
                        if(nums[j-1]<=nums[j]){
                            swing_len[i][j]=swing_len[i][j-1];
                            sub[i][j]=1;
                        }else{
                            swing_len[i][j]=swing_len[i][j-1]+1;
                            sub[i][j]=-1;
                        }
                    }
                    //nums[i][j-1]的最长递增子序列末尾下降
                    else if(sub[i][j-1]==-1){
                        if(nums[j-1]>=nums[j]){
                            swing_len[i][j]=swing_len[i][j-1];
                            sub[i][j]=-1;
                        }else{
                            swing_len[i][j]=swing_len[i][j-1]+1;
                            sub[i][j]=1;
                        }
                    }
                }
            }
        }
        return swing_len[0][n-1];
    }
};
【Leetcode刷题】贪心算法_第11张图片

法二:贪心下的动态规划
向本题这种需要划分为两种情况(结尾上升 / 下降)讨论的问题可以维护两个数组:一个用来标记结尾上升,另一个用来标记结尾下降。具有相似思路的还有 Leetcode213.打家劫舍 II。设 up[i] 表示前 i 个元素中的 结尾上升的最长摆动子序列 的长度,down[i] 表示前 i 个元素中的 结尾下降的最长摆动子序列 的长度。

对于 up[i],其结果由 up[i-1] 和 down[i-1] 决定,因为是 up[i] 表示结尾上升,因此只有 nums[i-1] < nums[i] 才有效。如果 nums[i-1] < nums[i],则 up[i] 赋值为 up[i-1](连续上升不增加摆动子序列长度) 和 down[i-1]+1(结尾下降的摆动子序列再上升) 中较大的元素;如果 nums[i-1] >= nums[i],则 up[i] 保持为 up[i-1]。

同理,对于 down[i],只有 nums[i-1] > nums[i] 才有效。如果 nums[i-1] > nums[i],则 down[i] 赋值为 down[i-1](连续下降不增加摆动子序列长度) 和 up[i-1]+1(结尾上升的摆动子序列再下降) 中较大的元素;如果 nums[i-1] <= nums[i],则 down[i] 保持为 down[i-1]。

class Solution {
public:
    int wiggleMaxLength(vector<int>& nums) {
        int n=nums.size();
        vector<int> up(n);
        vector<int> down(n);
        up[0]=1;
        down[0]=1;
        for(int i=1;i<n;i++){
            if(nums[i-1]<nums[i]){
                up[i]=max(up[i-1],down[i-1]+1);
                down[i]=down[i-1];
            }else if(nums[i-1]==nums[i]){
                up[i]=up[i-1];
                down[i]=down[i-1];
            }else{
                up[i]=up[i-1];
                down[i]=max(down[i-1],up[i-1]+1);
            }
        }
        return max(up[n-1],down[n-1]);
    }
};
【Leetcode刷题】贪心算法_第12张图片

你可能感兴趣的:(LeetCode刷题,leetcode,贪心算法)