算法之贪心

在这里插入图片描述

什么是贪心算法

所谓贪心算法,就是能由局部最优达到全局最优,这样的模型就满足贪心模型,贪心题目一般没有固定的模板,所以需要多刷题多了解。
在遇到一个问题时,贪心应该作为一个策略去考虑,而不是一定要贪心贪出来。
同时贪心问题最重要的是如何贪,往往这才是解决贪心问题最重要的地方。

贪心算法的OJ

分发饼干

  • 假设你是一位很棒的家长,想要给你的孩子们一些小饼干。但是,每个孩子最多只能给一块饼干。

  • 对每个孩子 i,都有一个胃口值 g[i],这是能让孩子们满足胃口的饼干的最小尺寸;并且每块饼干 j,都有一个尺寸 s[j] 。如果 s[j] >= g[i],我们可以将这个饼干 j 分配给孩子 i ,这个孩子会得到满足。你的目标是尽可能满足越多数量的孩子,并输出这个最大数值。

本题局部最优要尽可能将尺寸大的饼干分发给胃口大的小孩,全局最优才能达到满足最多的孩子。

class Solution {
public:
    int findContentChildren(vector<int>& g, vector<int>& s) 
    {
    	// - 排序,好进行后序操作。
        sort(g.begin(), g.end());
        sort(s.begin(), s.end());
        int index = s.size()-1;

        int count = 0;
        for(int i = g.size()-1; i >= 0; i--)
        {
            if(index >= 0 && s[index] >= g[i])
            {
                count++;
                index--;
            }
        }
        return count;

    }
};

本题要注意一点,就是遍历饼干还是遍历小孩,如果遍历饼干的话,则可能卡死在一个胃口很大的小孩上,所以本题要遍历小孩。
在这里插入图片描述


最大子数组和

  • 给你一个整数数组 nums ,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。

  • 子数组 是数组中的一个连续部分。

本题要求的是连续子数组的最大和, 负数对最大和只有降低作用,所以局部最优为如果加到某个数,我的连续和变为负数了,则该连续和不论如何加后面的数一定不是最大连续和,则就不在继续相加,累计设为0,全局最优找到最大子数组和。

class Solution {
public:
    int maxSubArray(vector<int>& nums) 
    {
        int res = INT_MIN;

        int count = 0;
        for(int i = 0; i < nums.size(); i++)
        {
            count+=nums[i];
            if(count > res) res = count;// - 记录最大值
            if(count < 0) count = 0;// - 将累加设为0
        }
        return res;

    }
};

买卖股票的最佳时机II

  • 给你一个整数数组 prices ,其中 prices[i] 表示某支股票第 i 天的价格。

  • 在每一天,你可以决定是否购买和/或出售股票。你在任何时候 最多 只能持有 一股 股票。你也可以先购买,然后在 同一天 出售。

  • 返回 你能获得的 最大 利润 。

本题支持多次交易,求的最大利润,股票有升有降,局部最优只要我在上升区间不断买卖股票,全集最优收集所有上升区间我的所得利润,就能得到最大利润了。
jpg

class Solution {
public:
    int maxProfit(vector<int>& prices) 
    {
        if(prices.size() == 1) return 0;
        //因为交易次数不受限,如果可以把所有的上坡全部收集到,一定是利益最大化的
        int res = 0;
        for(int i = 1; i < prices.size(); i++)
        {
            if(prices[i] > prices[i-1]) res+=(prices[i] - prices[i-1]);
        }
        return res;
    }
};

本题代码简单,但是很难想,本题还能通过动归解决。

跳跃游戏

  • 给你一个非负整数数组 nums ,你最初位于数组的 第一个下标 。数组中的每个元素代表你在该位置可以跳跃的最大长度。

  • 判断你是否能够到达最后一个下标,如果可以,返回 true ;否则,返回 false 。

本题只要求我能否达到,每个元素对应一个覆盖位置,如果覆盖位置超过了最后一个下标,则代表可以到达否则代表到达不了,只要存在下标的覆盖位置比先前的位置大,则就更改覆盖位置并进行判断。

class Solution {
public:
    bool canJump(vector<int>& nums) 
    {
      if(nums.size() == 1) return true;
        int cover = 0;// - 覆盖
        for(int i = 0; i <= cover; i++)
        {
        	// - 每次都要更新覆盖范围
            cover = max(cover, i + nums[i]);
            if(cover >= nums.size()-1) return true;
        }
        return false;
    }
};

跳跃游戏II

  • 给定一个长度为 n 的 0 索引整数数组 nums。初始位置为 nums[0]。

  • 每个元素 nums[i] 表示从索引 i 向前跳转的最大长度。换句话说,如果你在 nums[i] 处,你可以跳转到任意 nums[i + j] 处:

  • 0 <= j <= nums[i]
    i + j < n
    返回到达 nums[n - 1] 的最小跳跃次数。生成的测试用例可以到达 nums[n - 1]。

本题要求最小跳跃次数,本题依旧与跳跃范围有关,但是局部最优要达到,条一步后,下一个位置的覆盖范围是最大的,全局最优最小跳跃次数。

class Solution {
public:
    int jump(vector<int>& nums) {
        if (nums.size() == 1) return 0;
        int curDistance = nums[0];    // 当前覆盖最远距离下标
        if(curDistance == nums.size()-1) return 1;// - 只要当前覆盖到最后了就返回
        int ans = 0;            // 记录走的最大步数
        int nextDistance = 0;   // 下一步覆盖最远距离下标
        for (int i = 0; i < nums.size(); i++) {
            nextDistance = max(nums[i] + i, nextDistance);  // 更新下一步覆盖最远距离下标
            if (i == curDistance) {                         // 遇到当前覆盖最远距离下标
                ans++;                                  // 需要走下一步
                curDistance = nextDistance;             // 更新当前覆盖最远距离下标(相当于加油了)
                if (nextDistance >= nums.size() - 1) break;  // 当前覆盖最远距到达集合终点,用做ans++操作了,直接结束
            }
        }
        return ++ans;
    }
};

在这里插入图片描述
本题找到最大覆盖范围的下一步后,不用返回到那一步,因为那一步之后,到上一次覆盖的位置都已经被遍历过了,所以直接从当前下标开始。

k次取反后的最大化的数组和

  • 给你一个整数数组 nums 和一个整数 k ,按以下方法修改该数组:

  • 选择某个下标 i 并将 nums[i] 替换为 -nums[i] 。
    重复这个过程恰好 k 次。可以多次选择同一个下标 i 。

  • 以这种方式修改数组后,返回数组 可能的最大和 。

本题数组中有正有负,局部最优k次取反只要保证k次取反后绝对值最大的数负数都为正数,若k过大,则让去反后数组中最小的数消耗剩余次数,全局最优得到的和就是最大和。

class Solution {
public:
static bool cmp(int a, int b) {
    return abs(a) > abs(b);
}
    int largestSumAfterKNegations(vector<int>& A, int k) 
    {
        sort(A.begin(), A.end(), cmp);
        for (int i = 0; i < A.size(); i++) { // 第二步
        if (A[i] < 0 && k > 0) 
        {
            A[i] *= -1;
            k--;
        }
        }
        if(k%2 == 1) A[A.size()-1]*=-1;// - k过大后,最小的数消耗剩余的数量。
        int sum = 0;
        for(auto i : A) sum+=i;
        return sum;
    }
};

加油站

  • 在一条环路上有 n 个加油站,其中第 i 个加油站有汽油 gas[i] 升。

  • 你有一辆油箱容量无限的的汽车,从第 i 个加油站开往第 i+1 个加油站需要消耗汽油 cost[i] 升。你从其中的一个加油站出发,开始时油箱为空。

  • 给定两个整数数组 gas 和 cost ,如果你可以按顺序绕环路行驶一周,则返回出发时加油站的编号,否则返回 -1 。如果存在解,则 保证 它是 唯一 的。

本题要求从某个加油站绕一圈到达相同的加油站,并求出该加油站的下标,本体如果所有的油量小于消耗量,则就不存在一个加油站可以环绕一圈,同时从一个加油站到另一个加油站有补充有消耗,补充与消耗的差为余油量,局部最优如果某一点之前所有余油量的和都比该点的余油量要小,则该点以前任何一点都无法绕加油站一圈。全局最优我们只要找到最后一个和余油量为负数的序列的下一个加油站就是能绕行一圈的加油站。
关于该分析的数学推导
1.只要某个点的余油量的和为负数了,则该序列任何一点都无法绕一圈:因为该序列除了最后一个余油量为负数外,其余的点的余油量一定大于0,如果最大的大于0的序列和都无法满足最后一点的余油量,则该序列内从任何一点出发也必定无法满足。
2.我们只要找到最后一个和余油量为负数的序列的下一个加油站就是能绕行一圈的加油站:最后一个余油量为负数的序列后,剩余的点的余油量一定是大于0的,那么在所有的油量大于消耗量的前提下(一定有一点能到达),一定能到达的点一定是能收集最多余油量的点,该点就是最后一个余油量为负数的序列的下一个加油站。

class Solution {
public:
    int canCompleteCircuit(vector<int>& gas, vector<int>& cost) 
    {
        int start = 0;
        int res = 0;
        int sum = 0;
        for(int i = 0; i < gas.size(); i++)
        {
            res += (gas[i] - cost[i]);
            sum += (gas[i] - cost[i]);
            // - 如果存在这样的结点,那么一定在最后一个和为负数的序列的后一个结点,否则就没有。
            if(res < 0)
            {
                start = i+1;         
                res = 0;
                continue;
            }

        }
        if(sum < 0) return -1;// - 没有任何一个点能绕行一圈
        return start;
    }
};

分发糖果

  • n 个孩子站成一排。给你一个整数数组 ratings 表示每个孩子的评分。

  • 你需要按照以下要求,给这些孩子分发糖果:

  • 每个孩子至少分配到 1 个糖果。
    相邻两个孩子评分更高的孩子会获得更多的糖果。
    请你给每个孩子分发糖果,计算并返回需要准备的 最少糖果数目 。

本题每个孩子所分发到的糖果与左右两边的孩子都有关系,局部最优对于这类某一个结果与多个结果相联系的问题,我们要将这多个结果单独讨论,否则会顾此失彼, 全局最优最终得到正确答案,先考虑从左到右发糖果,左边比右边小,则右边糖果数+1,每个数组的值不但代表了从左到右遍历的每个孩子的糖果数,还代表了每个孩子的评分。随后在从右向左分发糖果,此时要考虑的东西就比较多了。

class Solution {
public:
    int candy(vector<int>& ratings) 
    {
        // - 2边,先考虑左在考虑右
        vector<int> suger(ratings.size(), 1);
        // - 左
        for(int i = 1; i < ratings.size(); i++)
        {
            if(ratings[i] > ratings[i-1])
            {
                // - 比前一个大
                suger[i] = suger[i-1]+1;
            }
        }
        // - 右
        for(int i = ratings.size()-2; i >= 0; i--)
        {
            if(ratings[i] > ratings[i+1])// - i 位置元素 i就是最大 or i次大
                suger[i] = max(suger[i], suger[i+1]+1);

            // 不大于右边,则该值i 最小,i次大但一定与右边无关,与左边有关。
        }
        int sum = 0;
        for(auto i : suger) sum+=i;
        return sum;
    }
};

在这里插入图片描述

从右向左遍历时当某一点大于后一个时,则要么第i个位置是次大的,要么第i个位置是最大的,但不管怎样,第i个位置的结果一定是max(candy[i], candy[i+1]+1)

柠檬水找零

  • 在柠檬水摊上,每一杯柠檬水的售价为 5 美元。顾客排队购买你的产品,(按账单 bills 支付的顺序)一次购买一杯。

  • 每位顾客只买一杯柠檬水,然后向你付 5 美元、10 美元或 20 美元。你必须给每个顾客正确找零,也就是说净交易是每位顾客向你支付 5 美元。

  • 注意,一开始你手头没有任何零钱。

  • 给你一个整数数组 bills ,其中 bills[i] 是第 i 位顾客付的账。如果你能给每位顾客正确找零,返回 true ,否则返回 false 。

本题要求能给每一个顾客都能找零,局部最优当顾客需要找零时,则有限交付大额钞票。全局最优每个顾客都能被找零。

class Solution {
public:
    bool lemonadeChange(vector<int>& bills) 
    {
        int fiveCount = 0, tenCount = 0;// - 5元钞票个数,10元钞票个数
        for(int i = 0; i < bills.size(); i++)
        {
            if(bills[i] == 10 && fiveCount>0)
            {
                fiveCount--;
                tenCount++;
                continue;
            }else if(bills[i] == 10)return false;// - 不够找零,返回false

            if(bills[i] == 20 && tenCount > 0 && fiveCount > 0)// - 有10元零钱
            {
                tenCount--;
                fiveCount--;
                continue;
            }
            else if(bills[i] == 20 && tenCount <= 0 && fiveCount >= 3)// - 没有十元零钱但是5元零钱足够找零。
            {
                fiveCount-=3;
                continue;
            }else if(bills[i] == 20) return false;// - 不够找零,返回false

            fiveCount++;


        }
        return true;
    }
};

本题的20元零钱没有任何用处,所以本题在记录时没有记录20元零钱个数,能找零的只有5元和10元。

根据身高重建队列

  • 假设有打乱顺序的一群人站成一个队列,数组 people 表示队列中一些人的属性(不一定按顺序)。每个 people[i] = [hi, ki] 表示第 i 个人的身高为 hi ,前面 正好 有 ki 个身高大于或等于 hi 的人。

  • 请你重新构造并返回输入数组 people 所表示的队列。返回的队列应该格式化为数组 queue ,其中 queue[j] = [hj, kj] 是队列中第 j 个人的属性(queue[0] 是排在队列前面的人)。

本题和分发糖果一样都是多状态问题,对于多状态问题要分开讨论每个状态,本题要求某人的身高为hi,且要求有ki个身高大于等于hi的人在他前面,ki和hi有练习,所以先对身高进行讨论,先将身高按照从大到小排序,然后每个人要求ki个身高大于hi的,所以就将该人插入ki个位置。

class Solution {
public:
    static bool cmp(const vector<int> a, const vector<int> b) {
        if (a[0] == b[0]) return a[1] < b[1];
        return a[0] > b[0];
    }//排序,按照第二个数从大到小排序。
    vector<vector<int>> reconstructQueue(vector<vector<int>>& people) {
        sort (people.begin(), people.end(), cmp);
        vector<vector<int>> que(people.size(), vector<int>(2, -1));
        for (int i = 0; i < people.size(); i++) {
            int position = people[i][1];
            if (position == que.size() - 1) que[position] = people[i];//插入最后一个位置元素。
            else 
            { 
                for (int j = que.size() - 2; j >= position; j--) que[j + 1] = que[j];//插入position位置,且不是最后一个位置,则需要后移所有元素。
                que[position] = people[i];
            }
        }
        return que;
    }
};

用最少数量的箭引爆气球

  • 有一些球形气球贴在一堵用 XY 平面表示的墙面上。墙面上的气球记录在整数数组 points ,其中points[i] = [xstart, xend] 表示水平直径在 xstart 和 xend之间的气球。你不知道气球的确切 y 坐标。

  • 一支弓箭可以沿着 x 轴从不同点 完全垂直 地射出。在坐标 x 处射出一支箭,若有一个气球的直径的开始和结束坐标为 xstart,xend, 且满足 xstart ≤ x ≤ xend,则该气球会被 引爆 。可以射出的弓箭的数量 没有限制 。 弓箭一旦被射出之后,可以无限地前进。

  • 给你一个数组 points ,返回引爆所有气球所必须射出的 最小 弓箭数 。

本题局部最优要求用箭更多刺穿气球,要求刺穿更多气球则要求得到更多重叠气球,全局最优得到最少的箭。

class Solution {
public:
    static bool cmp(vector<int>& e1, vector<int>& e2)
    {
        return e1[0] < e2[0];
    }

    int findMinArrowShots(vector<vector<int>>& points) 
    {
        sort(points.begin(), points.end(), cmp);
        int result = 1;

        for(int i = 1; i < points.size(); i++)
        {
            // - 只要有一个不重叠了,就用箭射前面一组气球。
            if(points[i][0] >= points[i-1][1])
            {
                result++;
            }
            // - 重叠就更新重叠范围。
            else
            {
                points[i][1] = min(points[i][1], points[i-1][1]);
            }
        }
        return result;

    }
};

在这里插入图片描述


无重叠区间

  • 给定一个区间的集合 intervals ,其中 intervals[i] = [starti, endi] 。返回 需要移除区间的最小数量,使剩余区间互不重叠 。

本题和用最少数量的箭引爆气球相似,都需要得到重叠区间。

class Solution {
public:

    static bool cmp(vector<int>& e1, vector<int>& e2)
    {
        return e2[0] > e1[0];
    }//按第0个位置升序。

    int eraseOverlapIntervals(vector<vector<int>>& intervals) 
    {
        sort(intervals.begin(), intervals.end(), cmp);
        
        int res = 0;
        
        for(int i = 1; i < intervals.size(); i++)
        {
            if(intervals[i][0] < intervals[i-1][1])
            {
                res++;
                if(intervals[i][1] < intervals[i-1][1])// - 每次移除的元素一定是最大end区间的。
                    continue;
                intervals[i] = intervals[i-1];
            }
        }
        return res;

    }
};

在这里插入图片描述

本题判断一个区间后要求将区间缩小到最小的位置,在继续判断下一个区间。


划分字母区间

  • 给你一个字符串 s 。我们要把这个字符串划分为尽可能多的片段,同一字母最多出现在一个片段中。

  • 注意,划分结果需要满足:将所有划分结果按顺序连接,得到的字符串仍然是 s 。

  • 返回一个表示每个字符串片段的长度的列表。

本题局部最优记录某以字符最远的位置,全局最优得到每个字符串的长度片段表。

class Solution {
public:
    vector<int> partitionLabels(string s) 
    {
        vector<int> nums;
        int hash[26];
        for(int i = 0; i < s.size(); i++)
        {
            // - 收集每个字母的最大位置
            hash[s[i] - 'a'] = i;
        }// - 用哈希表纪录

        int start = 0, end = 0;
        for(int j = 0; j < s.size(); j++)
        {
            // - 每次更新end的最大值
            end = max(end, hash[s[j] - 'a']);

            if(j == end) 
            {   
                nums.push_back(end-start+1); //记录长度。
                start = j+1;
            }
        }

        return nums;

    }
};

  • 27
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 7
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值