贪心算法四

> 作者:დ旧言~
> 座右铭:松树千年终是朽,槿花一日自为荣。

> 目标:了解什么是贪心算法,并且掌握贪心算法。

> 毒鸡汤:有些事情,总是不明白,所以我不会坚持。早安!

> 专栏选自:贪心算法_დ旧言~的博客-CSDN博客

> 望小伙伴们点赞👍收藏✨加关注哟💕💕

一、算法讲解

贪心算法的定义:

贪心算法是指在对问题求解时,总是做出在当前看来是最好的选择。也就是说,不从整体最优上加以考虑,只做出在某种意义上的局部最优解。贪心算法不是对所有问题都能得到整体最优解,关键是贪心策略的选择,选择的贪心策略必须具备无后效性,即某个状态以前的过程不会影响以后的状态,只与当前状态有关。

解题的一般步骤是:

  1. 建立数学模型来描述问题;
  2. 把求解的问题分成若干个子问题;
  3. 对每一子问题求解,得到子问题的局部最优解;
  4. 把子问题的局部最优解合成原来问题的一个解。

如果大家比较了解动态规划,就会发现它们之间的相似之处。最优解问题大部分都可以拆分成一个个的子问题,把解空间的遍历视作对子问题树的遍历,则以某种形式对树整个的遍历一遍就可以求出最优解,大部分情况下这是不可行的。贪心算法和动态规划本质上是对子问题树的一种修剪,两种算法要求问题都具有的一个性质就是子问题最优性(组成最优解的每一个子问题的解,对于这个子问题本身肯定也是最优的)。

动态规划方法代表了这一类问题的一般解法,我们自底向上构造子问题的解,对每一个子树的根,求出下面每一个叶子的值,并且以其中的最优值作为自身的值,其它的值舍弃。而贪心算法是动态规划方法的一个特例,可以证明每一个子树的根的值不取决于下面叶子的值,而只取决于当前问题的状况。换句话说,不需要知道一个节点所有子树的情况,就可以求出这个节点的值。由于贪心算法的这个特性,它对解空间树的遍历不需要自底向上,而只需要自根开始,选择最优的路,一直走到底就可以了。

二、算法习题


2.1、第一题

题目链接:134. 加油站 - 力扣(LeetCode)

题目描述:

算法思路:

  • 我们发现,当从 i 位置出发,⾛了 step 步之后,如果失败了。那么 [i, i + step] 这个区间内任意⼀个位置作为起点,都不可能环绕⼀圈。
  • 因此我们枚举的下⼀个起点,应该是 i + step + 1 。

代码呈现:

class Solution {
public:
    int canCompleteCircuit(vector<int>& gas, vector<int>& cost) 
    {
        int n = gas.size();
        for (int i = 0; i < n; i++) // 依次枚举所有的起点
        {
            int rest = 0; // 标记⼀下净收益
            int step = 0;
            for (; step < n; step++) // 枚举向后⾛的步数
            {
                int index = (i + step) % n; // 求出⾛ step 步之后的下标
                rest = rest + gas[index] - cost[index];
                if (rest < 0)
                    break;
            }
            if (rest >= 0)
                return i;
            i = i + step; // 优化
        }
        return -1;
    }
};

2.2、第二题

题目链接:738. 单调递增的数字 - 力扣(LeetCode)

题目描述:

算法思路:

  • a. 为了⽅便处理数中的每⼀位数字,可以先讲整数转换成字符串;
  • b. 从左往右扫描,找到第⼀个递减的位置;
  • c. 从这个位置向前推,推到相同区域的最左端;
  • d. 该点的值 -1 ,后⾯的所有数统⼀变成 9 。

代码呈现:

class Solution {
public:
    int monotoneIncreasingDigits(int n) 
    {
        string s = to_string(n); // 把数字转化成字符串
        int i = 0, m = s.size();
        // 找第⼀个递减的位置
        while (i + 1 < m && s[i] <= s[i + 1])
            i++;
        if (i + 1 == m)
            return n; // 判断⼀下特殊情况
        // 回推
        while (i - 1 >= 0 && s[i] == s[i - 1])
            i--;
        s[i]--;
        for (int j = i + 1; j < m; j++)
            s[j] = '9';
        return stoi(s);
    }
};

2.3、第三题

题目链接:991. 坏了的计算器 - 力扣(LeetCode)

题目描述:

算法思路:

  1. 当 end <= begin 的时候,只能执⾏「加法」操作;
  2. 当 end > begin 的时候,对于「奇数」来说,只能执⾏「加法」操作;对于「偶数」来说,最好的⽅式就是执⾏「除法」操作

这样的话,每次的操作都是「固定唯⼀」的。

代码呈现:

class Solution {
public:
    int brokenCalc(int startValue, int target) 
    {
        // 正难则反 + 贪⼼
        int ret = 0;
        while (target > startValue) {
            if (target % 2 == 0)
                target /= 2;
            else
                target += 1;
            ret++;
        }
        return ret + startValue - target;
    }
};

2.4、第四题

题目链接:56. 合并区间 - 力扣(LeetCode)

题目描述:

算法思路:

贪⼼策略:

  1. 先按照区间的「左端点」排序:此时我们会发现,能够合并的区间都是连续的;
  2. 然后从左往后,按照求「并集」的⽅式,合并区间。

如何求并集:由于区间已经按照「左端点」排过序了,因此当两个区间「合并」的时候,合并后的区间

  1. 左端点就是「前⼀个区间」的左端点;
  2. 右端点就是两者「右端点的最⼤值」。

代码呈现:

class Solution {
public:
    vector<vector<int>> merge(vector<vector<int>>& intervals) 
    {
        // 1. 先按照左端点排序
        sort(intervals.begin(), intervals.end());
        // 2. 合并区间
        int left = intervals[0][0], right = intervals[0][1];
        vector<vector<int>> ret;
        for (int i = 1; i < intervals.size(); i++) {
            int a = intervals[i][0], b = intervals[i][1];
            if (a <= right) // 有重叠部分
            {
                // 合并 - 求并集
                right = max(right, b);
            } else // 没有重叠部分
            {
                ret.push_back({left, right}); // 加⼊到结果中
                left = a;
                right = b;
            }
        }
        // 别忘了最后⼀个区间
        ret.push_back({left, right});
        return ret;
    }
};

2.5、第五题

题目链接:435. 无重叠区间 - 力扣(LeetCode)

题目描述:

算法思路:

贪⼼策略:

  1. 按照「左端点」排序;
  2. 当两个区间「重叠」的时候,为了能够「在移除某个区间后,保留更多的区间」,我们应该把「区间范围较⼤」的区间移除。

如何移除区间范围较⼤的区间:

由于已经按照「左端点」排序了,因此两个区间重叠的时候,我们应该移除「右端点较⼤」的区间

代码呈现:

class Solution {
public:
    int eraseOverlapIntervals(vector<vector<int>>& intervals) 
    {
        // 1. 按照左端点排序
        sort(intervals.begin(), intervals.end());
        // 2. 移除区间
        int ret = 0;
        int left = intervals[0][0], right = intervals[0][1];
        for (int i = 1; i < intervals.size(); i++) {
            int a = intervals[i][0], b = intervals[i][1];
            if (a < right) // 有重叠部分
            {
                ret++; // 删掉⼀个区间
                right = min(right, b);
            } else // 没有重叠部分
            {
                right = b;
            }
        }
        return ret;
    }
};

2.6、第六题

题目链接:452. 用最少数量的箭引爆气球 - 力扣(LeetCode)

题目描述:

算法思路:

贪⼼策略:

  • 按照左端点排序,我们发现,排序后有这样⼀个性质:「互相重叠的区间都是连续的」;
  • 这样,我们在射箭的时候,要发挥每⼀⽀箭「最⼤的作⽤」,应该把「互相重叠的区间」统⼀引爆。

如何求互相重叠区间?

  • 由于我们是按照「左端点」排序的,因此对于两个区间,我们求的是它们的「交集」
  • 左端点为两个区间左端点的「最⼤值」(但是左端点不会影响我们的合并结果,所以可以忽略);
  • 右端点为两个区间右端点的「最⼩值」。

代码呈现:

class Solution {
public:
    int findMinArrowShots(vector<vector<int>>& points) 
    {
        // 1. 按照左端点排序
        sort(points.begin(), points.end());
        // 2. 求互相重叠区间的数量
        int right = points[0][1];
        int ret = 1;
        for (int i = 1; i < points.size(); i++) {
            int a = points[i][0], b = points[i][1];
            if (a <= right) // 有重叠部分
            {
                right = min(right, b);
            } else // ⽆重叠部分
            {
                ret++;
                right = b;
            }
        }
        return ret;
    }
};

三、结束语 

今天内容就到这里啦,时间过得很快,大家沉下心来好好学习,会有一定的收获的,大家多多坚持,嘻嘻,成功路上注定孤独,因为坚持的人不多。那请大家举起自己的小手给博主一键三连,有你们的支持是我最大的动力💞💞💞,回见。

评论 21
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值