C++算法学习心得七.贪心算法(3)

1.根据身高重建队列(406题)

题目描述:

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

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

示例 1:

  • 输入:people = [[7,0],[4,4],[7,1],[5,0],[6,1],[5,2]]
  • 输出:[[5,0],[7,0],[5,2],[6,1],[4,4],[7,1]]
  • 解释:
    • 编号为 0 的人身高为 5 ,没有身高更高或者相同的人排在他前面。
    • 编号为 1 的人身高为 7 ,没有身高更高或者相同的人排在他前面。
    • 编号为 2 的人身高为 5 ,有 2 个身高更高或者相同的人排在他前面,即编号为 0 和 1 的人。
    • 编号为 3 的人身高为 6 ,有 1 个身高更高或者相同的人排在他前面,即编号为 1 的人。
    • 编号为 4 的人身高为 4 ,有 4 个身高更高或者相同的人排在他前面,即编号为 0、1、2、3 的人。
    • 编号为 5 的人身高为 7 ,有 1 个身高更高或者相同的人排在他前面,即编号为 1 的人。
    • 因此 [[5,0],[7,0],[5,2],[6,1],[4,4],[7,1]] 是重新构造后的队列。

贪心算法: 

有两个维度,h和k,看到这种题目一定要想如何确定一个维度,然后再按照另一个维度重新排列

按照身高h来排序呢,身高一定是从大到小排(身高相同的话则k小的站前面),让高个子在前面。

此时我们可以确定一个维度了,就是身高,前面的节点一定都比本节点高。

按照身高排序之后,优先按身高高的people的k来插入,后序插入节点也不会影响前面已经插入的节点,最终按照k的规则完成了队列

局部最优:优先按身高高的people的k来插入。插入操作过后的people满足队列属性

全局最优:最后都做完插入操作,整个队列满足题目队列属性

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;//申请一个队列
        //去遍历整个数组,
        for(int i = 0;i < people.size();i++){
            int postion = people[i][1];//找到k的位置
            que.insert(que.begin() + postion,people[i]);//去插入
        }
        return que;
    }
};
  • 时间复杂度:O(nlog n + n^2)
  • 空间复杂度:O(n)

使用vector(动态数组)来insert,是费时的,插入再拷贝的话,单纯一个插入的操作就是O(n^2)了,甚至可能拷贝好几次,就不止O(n^2)了。

改成链表之后

class Solution {
public:
    // 身高从大到小排(身高相同k小的站前面)
    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);
        list<vector<int>> que; // list底层是链表实现,插入效率比vector高的多
        for (int i = 0; i < people.size(); i++) {
            int position = people[i][1]; // 插入到下标为position的位置
            std::list<vector<int>>::iterator it = que.begin();
            while (position--) { // 寻找在插入位置
                it++;
            }
            que.insert(it, people[i]);
        }
        return vector<vector<int>>(que.begin(), que.end());
    }
};
  • 时间复杂度:O(nlog n + n^2)
  • 空间复杂度:O(n)

2.用最少数量的箭引爆气球( 452题)

题目描述:

在二维空间中有许多球形的气球。对于每个气球,提供的输入是水平方向上,气球直径的开始和结束坐标。由于它是水平的,所以纵坐标并不重要,因此只要知道开始和结束的横坐标就足够了。开始坐标总是小于结束坐标。

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

给你一个数组 points ,其中 points [i] = [xstart,xend] ,返回引爆所有气球所必须射出的最小弓箭数。

示例 1:

  • 输入:points = [[10,16],[2,8],[1,6],[7,12]]
  • 输出:2
  • 解释:对于该样例,x = 6 可以射爆 [2,8],[1,6] 两个气球,以及 x = 11 射爆另外两个气球

贪心算法: 为了让气球尽可能的重叠,需要对数组进行排序,如果气球重叠了,重叠气球中右边边界的最小值 之前的区间一定需要一个弓箭。注意=也可以射中气球,

class Solution {
static bool cmp(const vector<int>& a,const vector<int>& b){
    return a[0] < b[0];//自定义的一个排序规则
}
public:
    int findMinArrowShots(vector<vector<int>>& points) {
        if(points.size() == 0)return 0;//如果没有气球返回0
        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++;//当前的左边界大于上一个右边界,此时+1
            }else{
                points[i][1] = min(points[i-1][1],points[i][1]);//否则更新边界,右边界,取前一个和现在右边界最小值
            }
        }
        return result;
    }
};
  • 时间复杂度:O(nlog n),因为有一个快排
  • 空间复杂度:O(1)

 3.无重叠区间(435题)

题目描述:

给定一个区间的集合,找到需要移除区间的最小数量,使剩余区间互不重叠。

注意: 可以认为区间的终点总是大于它的起点。 区间 [1,2] 和 [2,3] 的边界相互“接触”,但没有相互重叠。

示例 1:

  • 输入: [ [1,2], [2,3], [3,4], [1,3] ]
  • 输出: 1
  • 解释: 移除 [1,3] 后,剩下的区间没有重叠。

贪心算法:和上一题类似 ,我来按照左边界排序,从左向右记录非交叉区间的个数。最后用区间总数减去非交叉区间的个数就是需要移除的区间个数了。要求非交叉区间的最大个数

class Solution {
static bool cmp(const vector<int>& a,const vector<int>& b){
    return a[0] < b[0];//自定义规则
}
public:
    int eraseOverlapIntervals(vector<vector<int>>& intervals) {
        if(intervals.size() == 0)return 0;
        sort(intervals.begin(),intervals.end(),cmp);//排序
        int count = 0;
        for(int i = 1;i < intervals.size();i++){
            //如果当前的左边界小于前一个右边界
            if(intervals[i][0] < intervals[i-1][1]){
                count++;//计数
                intervals[i][1] = min(intervals[i][1],intervals[i-1][1]);//更新当前右边界
            }
        }
        return count;
    }
};
  • 时间复杂度:O(nlog n) ,有一个快排
  • 空间复杂度:O(n),有一个快排,最差情况(倒序)时,需要n次递归调用。因此确实需要O(n)的栈空间

 4.划分字母区间(763题)

题目描述:

字符串 S 由小写字母组成。我们要把这个字符串划分为尽可能多的片段,同一字母最多出现在一个片段中。返回一个表示每个字符串片段的长度的列表。

示例:

  • 输入:S = "ababcbacadefegdehijhklij"
  • 输出:[9,7,8] 解释: 划分结果为 "ababcbaca", "defegde", "hijhklij"。 每个字母最多出现在一个片段中。 像 "ababcbacadefegde", "hijhklij" 的划分是错误的,因为划分的片段数较少。

贪心算法:

在遍历的过程中相当于是要找每一个字母的边界,如果找到之前遍历过的所有字母的最远边界,说明这个边界就是分割点了。此时前面出现过所有字母,最远也就到这个边界了。

可以分为如下两步:

  • 统计每一个字符最后出现的位置
  • 从头遍历字符,并更新字符的最远出现下标,如果找到字符最远出现位置下标和当前下标相等了,则找到了分割点
class Solution {
public:
    vector<int> partitionLabels(string s) {
        int hash[27] = {0};//定义一个数组来规定界限
        for (int i = 0; i < s.size(); i++) {
            hash[s[i] - 'a'] = i;//对每一个元素进行记录最远的出现位置,这里i可以覆盖之前出现过的元素
        }
        vector<int> result;
        int left = 0, right = 0;//左和右边界
        //遍历
        for (int i = 0; i < s.size(); i++) {
            right = max(right, hash[s[i] - 'a']);//更新右边最大的边界
            if (right == i) {
                result.push_back(right - left + 1);//如果右边边界等于到了当前下标就把数组长度加入结果
                left = i + 1;//更新左边界
            }
        }
        return result;
    }
};
  • 时间复杂度:O(n)
  • 空间复杂度:O(1),使用的hash数组是固定大小

5.合并区间(56题)

题目描述:

给出一个区间的集合,请合并所有重叠的区间。

示例 1:

  • 输入: intervals = [[1,3],[2,6],[8,10],[15,18]]
  • 输出: [[1,6],[8,10],[15,18]]
  • 解释: 区间 [1,3] 和 [2,6] 重叠, 将它们合并为 [1,6].

 贪心算法:判断区间重叠,区别就是判断区间重叠后的逻辑,本题是判断区间重贴后要进行区间合并,

所以一样的套路,先排序,让所有的相邻区间尽可能的重叠在一起,按左边界,或者右边界排序都可以,处理逻辑稍有不同。

按照左边界从小到大排序之后,如果 intervals[i][0] <= intervals[i - 1][1] 即intervals[i]的左边界 <= intervals[i - 1]的右边界,则一定有重叠。(本题相邻区间也算重贴,所以是<=)

其实就是用合并区间后左边界和右边界,作为一个新的区间,加入到result数组里就可以了。如果没有合并就把原区间加入到result数组。

class Solution {
static bool cmp(const vector<int>& a,const vector<int>& b){
    return a[0] < b[0];//自定义排序规则
}
public:
    vector<vector<int>> merge(vector<vector<int>>& intervals) {
        vector<vector<int>>result;
        sort(intervals.begin(),intervals.end(),cmp);//排序
        result.push_back(intervals[0]);//我们定义一个数组,首先将第一个集合加入进去
        //注意i的取值范围需要从1开始取值,然后遍历整个数组
        for(int i = 1;i < intervals.size();i++){
            //这里数组的最后一个元素的右边界大于下一个元素的左边界说明有重叠部分,需要更新下一个元素的右边界
            if(intervals[i][0] <= result.back()[1]){
                result.back()[1] = max(result.back()[1],intervals[i][1]);//这里需要更新右边界的最大值,
            }else{
                result.push_back(intervals[i]);//如果没有重复将数组加入到结果集中
            }
        }
        return result;
    }
};
  • 时间复杂度: O(nlogn)
  • 空间复杂度: O(logn),排序需要的空间开销

6. 单调递增的数字(738题)

题目描述:

给定一个非负整数 N,找出小于或等于 N 的最大的整数,同时这个整数需要满足其各个位数上的数字是单调递增。

(当且仅当每个相邻位数上的数字 x 和 y 满足 x <= y 时,我们称这个整数是单调递增的。)

示例 1:

  • 输入: N = 10
  • 输出: 9

贪心算法:例如:98,一旦出现strNum[i - 1] > strNum[i]的情况(非单调递增),首先想让strNum[i - 1]--,然后strNum[i]给为9,这样这个整数就是89,即小于98的最大的单调递增整数。本题需要从后向前遍历

class Solution {
public:
    int monotoneIncreasingDigits(int n) {
        string strnum = to_string(n);//首先对证书取字符串操作,
        int flag = strnum.size();//设置标志位,再最末尾
        //从后向前遍历,可以对数据进行操作,更方便
        for(int i = strnum.size() - 1;i > 0;i--){
            //如果相当于前一个数据大于后一个数据进行操作
            if(strnum[i-1] > strnum[i]){
                flag = i;//记录标志位
                strnum[i-1]--;//并且对其前一个进行-1操作
            }
        }
        for(int i = flag;i < strnum.size();i++){
            strnum[i] = '9';//我们需要把标志位后面的所有位变成9
        }
        return stoi(strnum);//最后返回将字符串变成整数
    }
};
  • 时间复杂度:O(n),n 为数字长度
  • 空间复杂度:O(n),需要一个字符串,转化为字符串操作更方便

7.监控二叉树(968题)

题目描述:

给定一个二叉树,我们在树的节点上安装摄像头。

节点上的每个摄影头都可以监视其父对象、自身及其直接子对象。

计算监控树的所有节点所需的最小摄像头数量。

​​​​​​​

贪心算法:

要从下往上看,局部最优:让叶子节点的父节点安摄像头,所用摄像头最少,整体最优:全部摄像头数量所用最少!

大体思路就是从低到上,先给叶子节点父节点放个摄像头,然后隔两个节点放一个摄像头,直至到二叉树头结点。

取了左孩子的返回值,右孩子的返回值,即left 和 right, 以后推导中间节点的状态 

来看看这个状态应该如何转移,先来看看每个节点可能有几种状态:

有如下三种:

  • 该节点无覆盖
  • 本节点有摄像头
  • 本节点有覆盖

我们分别有三个数字来表示:

  • 0:该节点无覆盖
  • 1:本节点有摄像头
  • 2:本节点有覆盖

空节点的状态只能是有覆盖,这样就可以在叶子节点的父节点放摄像头了 

  • 情况1:左右节点都有覆盖

左孩子有覆盖,右孩子有覆盖,那么此时中间节点应该就是无覆盖的状态了

  • 情况2:左右节点至少有一个无覆盖的情况

如果是以下情况,则中间节点(父节点)应该放摄像头

  • 情况3:左右节点至少有一个有摄像头

如果是以下情况,其实就是 左右孩子节点有一个有摄像头了,那么其父节点就应该是2(覆盖的状态)

  • 情况4:头结点没有覆盖

以上都处理完了,递归结束之后,可能头结点 还有一个无覆盖的情况,递归结束之后,还要判断根节点,如果没有覆盖,result++,

class Solution {
private:
    int result = 0;
    int traversal(TreeNode* cur){
        if(cur == NULL)return 2;// 空节点,该节点有覆盖
        int left = traversal(cur->left);//左
        int right = traversal(cur->right);//右
        // 情况1
        // 左右节点都有覆盖
        if(left == 2 && right == 2)return 0;
        // 情况2
        // left == 0 && right == 0 左右节点无覆盖
        // left == 1 && right == 0 左节点有摄像头,右节点无覆盖
        // left == 0 && right == 1 左节点有无覆盖,右节点摄像头
        // left == 0 && right == 2 左节点无覆盖,右节点覆盖
        // left == 2 && right == 0 左节点覆盖,右节点无覆盖
        if(left == 0 || right == 0){
            result++;
            return 1;
        }
         // 情况3
        // left == 1 && right == 2 左节点有摄像头,右节点有覆盖
        // left == 2 && right == 1 左节点有覆盖,右节点有摄像头
        // left == 1 && right == 1 左右节点都有摄像头
        // 其他情况前段代码均已覆盖
        if(left == 1 || right == 1)return 2;
        return -1;
    }
public:
    int minCameraCover(TreeNode* root) {
        result = 0;
        // root 无覆盖
        if(traversal(root) == 0){
            result++;
        }
        return result;
    }
};
  • 时间复杂度: O(n),需要遍历二叉树上的每个节点
  • 空间复杂度: O(n)

总结: 

根据身高重建队列:两个维度,一个是身高维度,另一个是前面几个人维度,我们需要先确定一个维度,然后再去确定另一个维度,首先根据身高来进行排序,由大到小排列,如果相同身高的话k小的排列,然后我们根据k来排序,申请一个队列,然后找到该插入的位置,进行Insert操作,更好的方法是使用链表的底层队列实现

用最少数量的箭引爆气球:为了让气球尽可能的重叠,需要对数组进行排序,如果气球重叠了,重叠气球中右边边界的最小值 之前的区间一定需要一个弓箭。注意=也可以射中气球,这样就可以实现,记得更新右边界值

无重叠区间:和上面一样首先根据左边界进行排序,当前左边界小于前一个右边界,更新右边界,需要去除的区间+1,找上述两个值的最小值即可更新右边界。

划分字母区间:在遍历过程中,记录最远下标,hash[s[i] - 'a'] = i这个是定义一个数组来实现,i是下标,每次更新下标最远处,设置两个边界,left,right,满足右边界等于i 时候记录一个区间,再更新左边界

合并区间:贪心算法,先排序,按照左边界右边界排序都可,按照左边界排序,如果当前左边界小于前一个的右边界,说明有重叠区间,首先自定义一个排序规则,对数组排序,然后定义结果集,首先加入一个集合进去,开始遍历,如果出现了上述情况,更新右边界最大值,否则加入结果集数组

单调递增的数字:根据题意遇到前一个数字大于后一个数字情况,这时候需要改变后面的数字,把当前数字减1,后面数字都变为9,首先需要把数字按照字符串进行处理,我们需要从后向前遍历,设置一个标志位,该标志位在末尾,如果当前数字小于前一个数字,就进行记录,并且减一,然后再从该标志位到末尾全部变为9即可,注意最后需要把字符串形式转换成整数

监控二叉树:要从下往上看,局部最优:让叶子节点的父节点安摄像头,所用摄像头最少,整体最优:全部摄像头数量所用最少,大体思路就是从低到上,先给叶子节点父节点放个摄像头,然后隔两个节点放一个摄像头,直至到二叉树头结点,知道每个节点状态,分别用数字代表状态,根据不同情况,根据叶子节点不同状态返回状态。

本图文内容来源于网友网络收集整理提供,作为学习参考使用,版权属于原作者。
THE END
分享
二维码
< <上一篇
下一篇>>