算法学习随笔 8_贪心算法整理总结

本章记录一些有关贪心算法的一些较为经典或者自己第一次做印象比较深刻的算法以及题型,包含自己作为初学者第一次碰到题目时想到的思路以及网上其他更优秀的思路,本章持续更新中......

回溯算法:贪心算法本质是选择每一阶段的局部最优,从而达到全局最优。比如有十个苹果,只能拿5个,要求拿到的苹果是最大的,要怎么拿?每一次都拿当前苹果里面最大的就可以了。如果加上个限制条件,要求放到容量为 N 的背包里,那就不能每次都拿最大的了,需要用到动态规划。

目录

No 455.分发饼干(简单)

No ​​​​​53. 最大子数组和(中等)

No 45.跳跃游戏II(中等)

No 1005.K次取反后最大化的数组和(中等)

No 134. 加油站(中等)

No 406.根据身高重建队列(中等)

No 452. 用最少数量的箭引爆气球(中等)

No 763.划分字母区间(中等)

No 738.单调递增的数字(中等)

No 968.监控二叉树(困难)


No 455.分发饼干(简单)

来源:力扣(LeetCode)
链接:https://leetcode.cn/problems/assign-cookies/

题目描述:

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

示例 1:
输入: g = [1,2,3], s = [1,1]
输出: 1
解释: 
你有三个孩子和两块小饼干,3个孩子的胃口值分别是:1,2,3。
虽然你有两块小饼干,由于他们的尺寸都是1,你只能让胃口值是1的孩子满足。
所以你应该输出1。

示例 2:
输入: g = [1,2], s = [1,2,3]
输出: 2
解释: 
你有两个孩子和三块小饼干,2个孩子的胃口值分别是1,2。
你拥有的饼干数量和尺寸都足以让所有孩子满足。
所以你应该输出2.

思路:用这个题来感受一下贪心算法。如果我们让大饼干满足小胃口的朋友,那么大饼干就浪费了,所以我们要用小饼干满足小胃口,大饼干满足大胃口。因此可以先对饼干数组胃口数组进行排序,然后从前往后遍历饼干数组,如果当前饼干不能满足胃口,那就让饼干往后移动换一个大的;如果能满足那就都往后移动。

class Solution {
public:
    //先对两个数组排序,然后先用小饼干满足胃口小的,也可以先用大饼干满足胃口大的
    //目的就是充分利用饼干尺寸,避免饼干的浪费,注意一个孩子只能有一个饼干
    int findContentChildren(vector& g, vector& s) {
        int count = 0;
        //胃口数组
        int gSize = g.size();
        int gIndex = 0;
        sort(g.begin(), g.end());
        //饼干数组
        int sSize = s.size();
        int sIndex = 0;
        sort(s.begin(), s.end());
        //从前往后遍历遇到不符合的就要让饼干移动,让饼干变大,因为已经排序且从后往前遍历
        for(; sIndex < sSize && gIndex < gSize; sIndex++ ) {
            if(g[gIndex] <= s[sIndex]) {
                gIndex++;
                count++;
            }
        }
        // //从后往前遍历遇到不符合的就要让胃口移动,让胃口变小,因为已经排过序且从后往前遍历
        // int j = s.size() - 1;
        // for(int i = g.size() - 1; i >=0; i--){
        //     if(j >= 0 && s[j] >= g[i]) {
        //         j--;
        //         count++;
        //     }
        // }
        return count;
    }
};

No ​​​​​53. 最大子数组和(中等)

来源:力扣(LeetCode)
链接:https://leetcode.cn/problems/maximum-subarray/

题目描述:

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

示例 1:
输入:nums = [-2,1,-3,4,-1,2,1,-5,4]
输出:6
解释:连续子数组 [4,-1,2,1] 的和最大,为 6 。

示例 2:
输入:nums = [1]
输出:1

示例 3:
输入:nums = [5,4,-1,7,8]
输出:23

思路:如果当前累加的 sum 值已经小于0了,那么如果继续在此基础上累加,那么只会导致最后的总和是减小的,也就是说当前累加的 sum 值只能对最后的结果起到负面作用。所以当遇到累加和为负数的时候,那就从下一个数字开始重新累加。可能会有疑问,如果都是负数怎么办,其实也是一样的道理,如果都是负数,那是不是最后一个数就应该是最大的和呀,所以这样的逻辑是没问题的。

class Solution {
public:
    //整体思路:如果当前累加的和小于等于0,那此时继续在sum上累加,会拖累总结果,因此需要从下一位重新累加
    int maxSubArray(vector& nums) {
        int sum = 0;
        int maxSum = INT_MIN;
        int index = 0;
        for(int i = 0; i < nums.size(); i++) {
            sum += nums[i];
            if(sum > maxSum) {
                maxSum = sum;
            }
            if(sum <= 0) {
                sum = 0;
                continue;
            }
        }
        return maxSum; 
    }
};

No 45.跳跃游戏II(中等)

来源:力扣(LeetCode)
链接:​https://leetcode.cn/problems/jump-game-ii/

题目描述:

给你一个非负整数数组 nums ,你最初位于数组的第一个位置。数组中的每个元素代表你在该位置可以跳跃的最大长度。你的目标是使用最少的跳跃次数到达数组的最后一个位置。假设你总是可以到达数组的最后一个位置。

示例 1:
输入: nums = [2,3,1,1,4]
输出: 2
解释: 跳到最后一个位置的最小跳跃数是 2。
     从下标为 0 跳到下标为 1 的位置,跳 1 步,然后跳 3 步到达数组的最后一个位置。

示例 2:
输入: nums = [2,3,0,1,4]
输出: 2

思路:这个题要求到达末尾的最少步数,那么局部最优解是什么?局部最优解其实是一步尽可能多走,从而达到最小步数,但实际上并不能这样走,因为不知道下一步能不能到达末尾。所以,要从覆盖范围出发,不管怎么跳,覆盖范围内一定是可以跳到的,以最小的步数增加覆盖范围,覆盖范围一旦覆盖了终点,得到的就是最小步数。如果移动下标达到了当前这一步的最大覆盖最远距离了,还没有到终点的话,那么就必须再走一步来增加覆盖范围,直到覆盖范围覆盖了终点。

class Solution {
public:
    int jump(vector& nums) {
        // 当前覆盖最远距离下标
        int curArea = 0;
        // 下一步覆盖最远距离下标
        int nextArea = 0;
        // 记录走的最大步数
        int jumpRes = 0;
        for(int i = 0; i < nums.size(); i++) {
            // 更新下一步覆盖最远距离下标
            if(nums[i] + i > nextArea){
                nextArea = nums[i] + i;
            }
            // 遇到当前覆盖最远距离下标,只要第一个数字不是0就一定会至少跳一步
            if(i == curArea ){
                // 如果当前覆盖最远距离下标不是终点
                if(curArea != nums.size() - 1){
                    jumpRes++;
                    // 更新当前覆盖最远距离下标
                    curArea = nextArea;
                    // 下一步的覆盖范围已经可以达到终点,结束循环
                    if(nextArea >= nums.size() - 1){
                        break;
                    }
                }
                // 当前覆盖最远距离下标是集合终点,不用做ans++操作了,直接结束
                else{
                    break;
                }
            }
        }
        return jumpRes;

    }
};

No 1005.K次取反后最大化的数组和(中等)

来源:力扣(LeetCode)
链接:https://leetcode.cn/problems/maximize-sum-of-array-after-k-negations

题目描述:

给你一个整数数组 nums 和一个整数 k ,按以下方法修改该数组:选择某个下标 i 并将 nums[i] 替换为 -nums[i] 。重复这个过程恰好 k 次。可以多次选择同一个下标 i 。以这种方式修改数组后,返回数组 可能的最大和 。

示例 1:
输入:nums = [4,2,3], k = 1
输出:5
解释:选择下标 1 ,nums 变为 [4,-2,3] 。

示例 2:
输入:nums = [3,-1,0,2], k = 3
输出:6
解释:选择下标 (1, 2, 2) ,nums 变为 [3,1,0,2] 。

示例 3:
输入:nums = [2,-3,-1,5,-4], k = 2
输出:13
解释:选择下标 (1, 4) ,nums 变为 [2,3,-1,5,4] 。

思路:这一题的思路是,如果我们可以尽可能的把所有的负数都变成正数,那不就可以做到和最大了嘛。可是如果已经把左右的负数都变成正数了,K 还没用完,这时候就需要改变正数了,而且此时要反复变化最小的正数,才能保证和最大化。要想做到以上操作,我们要对数字按照绝对值的大小排序,注意不是变成绝对值,只是按照绝对值的大小排序。然后我们把前 K 个负数变成正数,然后如果此时 K 还没用完,那就找到最小的正数,必定是在数组的末尾,对这个数反复取反,最后得到的累加和就是最大的和。

class Solution {
public:
    static bool cmp(int a, int b) {
        return abs(a) > abs(b);
    }
    //按照绝对值从大到小排序,把最前面的前 k 个负数变为正数
    //如果都变为正数了 k 还是大于0,那就找最小的正数反复变,直到 k 用完
    int largestSumAfterKNegations(vector& nums, int k) {
        sort(nums.begin(), nums.end(), cmp);
        for(int i = 0; i < nums.size(); i++) {
            if(nums[i] < 0 && k > 0) {
                nums[i] *= -1;
                k--;
            }
        }
        if(k > 0){
            while(k--){
                nums[nums.size() - 1] *= -1;
            }
        }
        int sum = 0;
        for(int i = 0; i < nums.size(); i++){
            sum += nums[i];
        }
        return sum;
    }
};

No 134. 加油站(中等)

来源:力扣(LeetCode)
链接:https://leetcode.cn/problems/gas-station

题目描述:

在一条环路上有 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 升汽油。
因此,无论怎样,你都不可能绕环路行驶一周。

思路:这个题目用暴力法来从每一个起点进行遍历可以实现,但是会超时,所以要想另外的解决思路。可以尝试使用贪心来解决,局部最优就是不断累加当前加油站可以加的汽油和到达下一个加油站需要消耗的汽油的差值,如果这个累加值小于 0 了,说明从这次累加开始的位置到当前的位置都不能作为起点,因为如果选择这其中的位置作为起点,一定会经历累加和小于 0 的情况。此时就要从下一个位置作为起点重新开始累加。如果所有加的汽油小于总消耗的汽油,那么一定完成一整圈。

class Solution {
public:
    //贪心:计算当前剩余油量的累加值,如果小于0 那就说明截止到当前位置,所有的位置都不能是起点,要从下一个位置开始
    int canCompleteCircuit(vector& gas, vector& cost) {
        int curGasSum = 0;
        int totalGasSum = 0;
        int start = 0;
        for(int i = 0; i < cost.size(); i++){
            //记录总的剩余油量
            totalGasSum += gas[i] - cost[i];
            //记录累加和
            curGasSum += gas[i] - cost[i];
            //当前累加和小于0,说明从0到i之间的所有位置都不能作为起点
            //因为所有在这之间的起点,都会走到这里,都会变得油量不够
            //说明起始位置只能在 i+1 到 末尾之间
            if(curGasSum < 0){
                start = i + 1;
                curGasSum = 0;
            }
        }
        //总的剩余油量小于0,怎么都不可能走完一圈
        if(totalGasSum < 0) {
            return -1;
        }
        return start;
    }
};

No 406.根据身高重建队列(中等)

来源:力扣(LeetCode)
链接:https://leetcode.cn/problems/queue-reconstruction-by-height

题目描述:

假设有打乱顺序的一群人站成一个队列,数组 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]] 是重新构造后的队列。

示例 2:
输入:people = [[6,0],[5,0],[4,0],[3,2],[2,2],[1,4]]
输出:[[4,0],[5,0],[2,2],[3,2],[1,4],[6,0]]

思路:这一题需要从两个维度进行处理,和题目135. 分发糖果 是一个类型。但是先按照身高排序还是先按照序号排序呢?其实要深刻理解这个二元组的含义,第一个是身高,第二个是前面比当前身高高或者相同的人的个数。所以如果我们先处理身高最高的人,那么后面身高低的人哪怕放到了身高高的人的前面也不会造成影响,所以我们得知需要对身高从高到低进行排序,如果身高一样,那就按第二个参数从小到大排序。排序以后,先拿到身高高的人,然后根据第二个数字来插入到新数组的指定位置,这样最后的结果就是符合要求的结果。

class Solution {
public:
    static bool cmp(const vector& a, const vector& b) {
        //如果身高相同,按照第二个数字从小到大排序
        if(a[0] == b[0]){
            return a[1] < b[1];
        }
        // 优先按照身高从大到小排序
        return a[0] > b[0];
    }
    vector> reconstructQueue(vector>& people) {
        //优先按身高高的 people 的 k 来插入,后序插入节点也不会影响前面已经插入的节点
        //理解[h, k]表示身高为 h 的人的前面有 k 个身高相同或更高的人在前面,所以后操作的身高小的技即使插在了前面也不影响
        sort(people.begin(), people.end(), cmp);
        vector> queueRes;
        for(int i = 0; i < people.size(); i++){
            //按照 k 的位置插入 people[i], 因为已经按身高排过序所以先拿到的是身高高的,不会影响结果
            queueRes.insert(queueRes.begin() + people[i][1], people[i]);
        }
        return queueRes;
    }
};

No 452. 用最少数量的箭引爆气球(中等)

来源:力扣(LeetCode)
链接:https://leetcode.cn/problems/minimum-number-of-arrows-to-burst-balloons

题目描述:

有一些球形气球贴在一堵用 XY 平面表示的墙面上。墙面上的气球记录在整数数组 points ,其中points[i] = [xstart, xend] 表示水平直径在 xstart 和 xend之间的气球。你不知道气球的确切 y 坐标。一支弓箭可以沿着 x 轴从不同点 完全垂直 地射出。在坐标 x 处射出一支箭,若有一个气球的直径的开始和结束坐标为 xstart,xend, 且满足  xstart ≤ x ≤ xend,则该气球会被 引爆 。可以射出的弓箭的数量 没有限制 。 弓箭一旦被射出之后,可以无限地前进。给你一个数组 points ,返回引爆所有气球所必须射出的 最小 弓箭数 。

示例 1:
输入:points = [[10,16],[2,8],[1,6],[7,12]]
输出:2
解释:气球可以用2支箭来爆破:
-在x = 6处射出箭,击破气球[2,8]和[1,6]。
-在x = 11处发射箭,击破气球[10,16]和[7,12]。

示例 2:
输入:points = [[1,2],[3,4],[5,6],[7,8]]
输出:4
解释:每个气球需要射出一支箭,总共需要4支箭。

示例 3:
输入:points = [[1,2],[2,3],[3,4],[4,5]]
输出:2
解释:气球可以用2支箭来爆破:
- 在x = 2处发射箭,击破气球[1,2]和[2,3]。
- 在x = 4处射出箭,击破气球[3,4]和[4,5]。

思路:这一题其实本质上是找交集和挨着的集合。首先为了便于处理,让气球按照起始位置进行排序。起码要有一支箭,如果当前区间的起始位置前一个区间的结束位置还要小或者相等,则说明他们用一支箭就可以,否则就得多加一支箭。注意如果重叠,需要把这两个区间的结束位置进行更新,把当前区间的结束位置更新为这两个区间中较小的那个,这样才能使用更少的箭。

class Solution {
public:
    //按照起始位置升序排序
    static bool cmp(vector& a, vector& b) {
        return a[0] < b[0];
    }
    //本质上就是找交集
    int findMinArrowShots(vector>& points) {
        sort(points.begin(), points.end(), cmp);
        //起码要有一只箭
        int shots = 1;
        for(int i = 1; i < points.size(); i++) {
            //不重叠,则需要增加一只箭
            if(points[i][0] > points[i - 1][1]) {
                shots++;
            }
            // 重叠,更新重叠起球的最小右边界,相当于取交集,只不过只需要右边界
            else{
                //直接在当前气球上更改就可以
                points[i][1] = min(points[i][1], points[i - 1][1]);
            }
        }
        return shots;
    }
};

No 763.划分字母区间(中等)

来源:力扣(LeetCode)
链接:https://leetcode.cn/problems/partition-labels/

题目描述:

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

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

思路:这一题很巧妙,乍一看没有什么好的思路,初次接触该类型的题也是比较难想到解决办法的。首先把每一个字母出现的最后位置记录下来,然后用 for 循环遍历字符串,在遍历时首先更新当前字母的最后出现位置,这一步是关键。然后如果遇到了当前字母的位置 等于 最后出现的位置,那么这就是一个合理的分段。其中红色部分是关键,通过这一步,可以把当前已经遍历到的字母的最大位置记录下来,相当于这是一个区间,区间内的所有字母都必须是一个分段才符合题目要求。

class Solution {
public:
    vector partitionLabels(string s) {
        //先统计所有字母出现的最后的位置
        int lastIndexOfLetter[27] = {-1};
        for(int i = 0; i < s.size(); i++) {
            lastIndexOfLetter[s[i] - 'a'] = i;
        }
        vector res;
        int leftIndex = 0;
        int rightIndex = 0;
        //如果这个字符当前的位置等于之前记录的最后出现的位置,那就是分割点
        for(int i = 0; i < s.size(); i++) {
            //获取当前字符出现的最远位置,这个rightIndex是不断更新的
            //比如对于字符串 "ababcbacabefegdehijhklij",当遍历到 b 的时候,rightIndex 就是 9 了
            //此时所有最后出现位置小于 9 的字符都将属于一个片段
            rightIndex = max(rightIndex, lastIndexOfLetter[s[i] - 'a']);
            //如果当前位置等于最远位置那就是一个分割点
            if(i == rightIndex){
                res.push_back(rightIndex - leftIndex + 1);
                leftIndex = i + 1;
            }
        }
        return res;
    }
};

No 738.单调递增的数字(中等)

来源:力扣(LeetCode)
链接:https://leetcode.cn/problems/monotone-increasing-digits

题目描述:

当且仅当每个相邻位数上的数字 x 和 y 满足 x <= y 时,我们称这个整数是单调递增的。给定一个整数 n ,返回 小于或等于 n 的最大数字,且数字呈 单调递增 。

示例 1:
输入: n = 10
输出: 9

示例 2:
输入: n = 1234
输出: 1234

示例 3:
输入: n = 332
输出: 299

思路:这一题其实是一个贪心的比较经典的题目。但是这题有有一个点需要注意就是需要从后往前遍历,这样才能用已经处理过的数字,其实有好多题目都有这样的特点。如果从前向后遍历的话,遇到strNum[i - 1] > strNum[i]的情况,让strNum[i - 1]减一,但此时如果strNum[i - 1]减一了,可能又小于strNum[i - 2]。局部最优解是遇到strNum[i - 1] > strNum[i]的情况,让strNum[i - 1]--,然后strNum[i]给为9,可以保证这两位变成最大单调递增整数。但是有一点需要注意,并不是需要每次都把 i 位置的值变成9,而是要记录这个位置,从这个位置往后都变成9。因为会遇到101这样的情况。

class Solution {
public:
    //遇到strNum[i - 1] > strNum[i]的情况,让strNum[i - 1]--,
    //然后strNum[i]给为9,可以保证这两位变成最大单调递增整数。
    int monotoneIncreasingDigits(int n) {
        string num = to_string(n);
        // 用于标记从哪里开始后面的都要变成 9
        int indexOf_9 = num.size();
        //注意从后向前遍历,才能使用已经处理过的数字
        for(int i = num.size() - 1; i > 0; i--){
            if(num[i - 1] > num[i]){
                //注意要标记从哪里开始后面的都变成9, 而不是把 num[i] 变成 9
                //因为可能出现 当前位置不符合条件,但是继续遍历的时候出现了符合条件的位置 eg:101
                indexOf_9 = i;
                num[i - 1] --;
            }
        }
        for(int i = indexOf_9; i < num.size(); i++){
            num[i] = '9';
        }
        return stoi(num);
    }
};

No 968.监控二叉树(困难)

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/sudoku-solver/

题目描述:

给定一个二叉树,我们在树的节点上安装摄像头。节点上的每个摄影头都可以监视其父对象、自身及其直接子对象。计算监控树的所有节点所需的最小摄像头数量。

算法学习随笔 8_贪心算法整理总结_第1张图片

示例 1:
输入:[0,0,null,0,0]
输出:1
解释:如图所示,一台摄像头足以监控所有节点。

算法学习随笔 8_贪心算法整理总结_第2张图片

示例 2:
输入:[0,0,null,0,null,0,null,null,0]
输出:2
解释:需要至少两个摄像头来监视树的所有节点。 上图显示了摄像头放置的有效位置之一。

思路:这个题的思路和操作是有难度的。通过题目我们可以了解到,一个摄像头的位置可以覆盖当前位置,父节点,子节点三个位置。而且可以发现,叶子节点不需要布置摄像头,因为在叶子的父节点布置就可以了。所以我们必须要使用后序遍历从下往上遍历,这样可以保证叶子结点不会放置摄像头,但是头结点可能就会需要放置摄像头了,不过叶子节点的数量肯定是大于等于1的,所以还是保证叶子结点不会布置摄像头是优先选择。

我们给一个节点设置三种状态:1、没被覆盖 2、被覆盖了 3、摄像头安装节点。一个节点只有这三种状态,其余状态都包含在这三种之间了。

然后考虑以下几种情况:

1、两个孩子节点都被覆盖了,那么此时父节点肯定没被覆盖,但是不一定需要安装摄像头

2、孩子节点中有一个是没被覆盖了,那么此时父节点就必须要安装一个摄像头了

3、孩子节点有一个安装了摄像头,那么此时父节点一定被覆盖到了

但是递归的终止条件如何设置?也就是空节点该如何处理。如果把空节点标记为没被覆盖,那么叶子节点就需要安装摄像头了。如果把空节点标记为安装摄像头,那么叶子节点就被覆盖了,父节点就不会安装设摄像头了,也不对。所以空节点只能被标记为被覆盖。

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode() : val(0), left(nullptr), right(nullptr) {}
 *     TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
 *     TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
 * };
 */
class Solution {
public:
    //整体思路:让叶子节点的父节点安装摄像头,然后隔两个再安装一个,通过状态来标记某个节点是否需要添加摄像头
    //关键点:一个节点只有三种可能:1、没被覆盖 2、被覆盖了 3、摄像头安装节点
    int cameraNum = 0;
    int postOrderTraversal(TreeNode* node) {
        //空节点只能是标记为被覆盖的状态:原因如下
        //如果是标记为没被覆盖,那么叶子节点就要放置摄像头了,导致摄像头数量不是最少的
        //如果是标记为安装摄像头,那叶子节点的父节点就不用安装摄像头了,导致叶子节点不会被覆盖到
        if(node == NULL) {
            return 2;
        }
        //左
        int leftNode = postOrderTraversal(node -> left);
        //右
        int rightNode = postOrderTraversal(node -> right);
        //中
        //如果左右孩子都是被覆盖状态,那么父节点一定是无覆盖的状态,但此时不需要装摄像头
        if(leftNode == 2 && rightNode == 2) {
            return 1;
        }
        //如果只有一个孩子没被覆盖或者两个孩子都没被覆盖,那父节点必须要安装摄像头了
        if(leftNode == 1 || rightNode == 1) {
            cameraNum++;
            return 3;
        }
        //如果孩子有一个放置了摄像头,另一个孩子有覆盖,那么父节点就会被覆盖
        //这里不会出现一个放置了摄像头,另一个孩子无覆盖的情况,因为会被上一个 if 截获
        if(leftNode == 3 || rightNode == 3) {
            return 2;
        }

        return -1;
    }
    int minCameraCover(TreeNode* root) {
        //头结点是否被覆盖
        if(postOrderTraversal(root) == 1) {
            cameraNum++;
        }
        return cameraNum;
    }
};

你可能感兴趣的:(算法学习随笔,算法,leetcode,数据结构)