让整棵二叉树都被相机覆盖到,请问要给二叉树放多少台相机?

让整棵二叉树都被相机覆盖到,请问要给二叉树放多少台相机?

提示:本题是二叉树递归套路中比较难的题目,是互联网大厂经常考的题目
不管是常规的二叉树递归套路还是贪心的递归套路,都比较难,但是这个题,是互联网大厂经常考的题目


题目

给定二叉树,head是头结点,假如在任意节点x上放一台相机,则这台相机可以覆盖x自己,覆盖x的父节点,和覆盖x的左右子节点,一共可以覆盖4个节点,请问你想让整个二叉树都被覆盖到的话,总共需要放多少台相机?


一、审题

示例:下面这颗二叉树,头结点上放一台相机,可以覆盖3个节点
在A和B节点各放一台相机,那可以覆盖自己的活力圈
总共3台足矣覆盖整颗二叉树
在这里插入图片描述


二、解题

树形DP(固定的二叉树递归套路,但是对于本题来说比较难理解)

我们常规的二叉树递归套路(也称为树形DP)是这样的:
来到任意一颗以x为头结点的子二叉树,我们讨论结果与x有关还是无关?
在这里插入图片描述
比如求x为头的树,里面的条路径累加和的最大值是多少?
就是节点x到节点y两者value加起来的最大值:1+2+3=6
在这里插入图片描述

这个问题就是看与x有关还是无关?
(1)与x有关
比如下面这个树,最大累加和肯定是1,与x有关
1-1=0,1-2=-1都不是最大的路径和,而1这个路径就一个节点,自然最大1
在这里插入图片描述
(2)与x无关
比如下面这个树,可不能把x算进去,否则就不是最大累加和了
有一条
1,2单独成一条路径,是最大为2
但是不能算x,否则就变小了

在这里插入图片描述
这是常规的树形DP的区分方法
本题暂时不太合适这么分

比如:以x为头的树,应该放多少台相机,能让整个x开头的树全被覆盖。
这么区分:
(1)x上放相机
(2)x上不放相机
显然你看,咱x不放相机的话,你还得看left和right
如果你left和right有一个放了,你x放一台相机岂不是多余了,没必要啊
在这里插入图片描述
显然根据这个定义没法往上面父节点推

咱们来到一个节点x,希望搞清楚,究竟咱是往x上放相机,还是不放,而且要让x的父节点只关心x本身,不要关心x的left和right
【这理所应当也是树形DP的关键所在,一个节点只需要考虑它的子节点,别去关心他们的孙子节点】
我们这么讨论:
(1)x无相机,也未被覆盖时,这棵树目前有几个相机(unCovered)?【这时x的left和right肯定也没有相机,但是left和right必然已经被覆盖了的,因为我不要x的父亲来关心这俩孙子,俩孙子就潜在的条件是必须被覆盖了】
在这里插入图片描述

(2)x无相机,但x已经被覆盖时,这棵树目前有几个相机(coveredNoCamera)?【这时显然是x的left或者right有一台相机,而且,left和right也必然已经被覆盖了,因为x的父节点可不需要考虑孙子节点,潜在就是这俩孙子都被覆盖了的,咱不讨论多的废话】
下面问好就是代表left或者right有一台相机
在这里插入图片描述

(3)x有1台相机,必然就是被覆盖了的,这棵树目前有几个相机(coveredHasCamera)?【x有一台的话,left和right原来有没有相机都无所谓的,他们必然被覆盖,这样x的父节点也不用在乎孙子节点的情况】
在这里插入图片描述

如果每一个节点x都把这三种情况的信息记录好,那x的父节点,就可以讨论自己的信息了。
从叶节点开始,往上推,最终推到head头结点,那就可以计算出自己覆盖了然后放多少相机的状况了。
因此,我们需要每次遍历到节点x时,拿到左右子的上面三种信息,然后计算x自己的这三种信息,返回给父节点用。
那么信息可以单独定义为一个类,方便树形DP使用

public static class Node{
        public int value;
        public Node left;
        public Node right;

        public Node(int v){
            value = v;
        }
    }

    public static class Info{
        public long unCovered;//x没有被覆盖,需要几台相机
        public long coveredNoCam;//x被覆盖,没有相机,需要几台相机
        public long coveredHasCam;//x被覆盖,且x有相机,需要几台相机

        public Info(long un, long no, long has){
            unCovered = un;
            coveredNoCam = no;
            coveredHasCam = has;
        }
    }

好,
现在定义:树形DP的递归函数f(x)代表
x为头的树,至少x的左右子必须被覆盖的情况下,x可以被覆盖,也可以不被覆盖,把上面的三条信息统计好,返回给x的父节点。
(1)注意,遇到叶节点,x=null
第一它不会被覆盖,第二,不可能放相机,所以只有uncovered=0这点信息有意义

如果是非叶节点,有左右子,则先去左右子拿左右子的信息:

//左右树收集信息
        Info left = f(x.left);
        Info right = f(x.right);

(2)x的unCovered信息怎么求?
——要求xunCovered,那left和right都不能有相机

long unCovered = left.coveredNoCam + right.coveredNoCam;

(3)x的coveredNoCamera怎么求?
——可能left有相机,right也有相机
——可能left没有相机,right有相机
——可能left有相机,right没有相机
这三种情况都可以让xcovered,取left+right最小值

long coveredNoCam = Math.min(left.coveredHasCam + right.coveredHasCam,
                Math.min(left.coveredHasCam + right.coveredNoCam, 
                        left.coveredNoCam + right.coveredHasCam));

(3)x的coveredHasCamera怎么求?
——可能left有相机,right也有相机
——可能left没有相机,right有相机
——可能left有相机,right没有相机
——可能left没有相机,right没有相机 只要x有相机就能覆盖left和right,所以left和right有没有相机随便了
这4种情况都可以,取1+ left的最小值+right的最小值

long coveredHasCam = 1 + 
                Math.min(left.unCovered, Math.min(left.coveredNoCam, left.coveredHasCam))
                +
                Math.min(right.unCovered, Math.min(right.coveredNoCam, right.coveredHasCam));

最后将上面三个值,放入Info,返回给父

主函数在调用是,最后核验一下head,取head的coveredNoCam, coveredHasCam;的最小值

public static Info f(Node x){
        //叶节点,它第一不可能不被覆盖,而且它不可能放相机,所以
        //unCovered = 无穷---很多很多相机,这种方案肯定不要的,咱也不用
        //coveredHasCam = 无穷,这里参数也不可能遇到的,不用
        //而coveredNoCam = 0,一台相机都不需要的
        if (x == null) return new Info(Integer.MAX_VALUE, 0, Integer.MAX_VALUE);

        //左右树收集信息
        Info left = f(x.left);
        Info right = f(x.right);

        //整理信息
        //1)x不能被覆盖,自然也就没有相机,【左右子肯定也不能有相机,但是左右子也必须是覆盖的】
        long unCovered = left.coveredNoCam + right.coveredNoCam;

        //2)x被覆盖,但是x上没有相机,【3种子情况,左右子都有相机,左右有相机右子没有,右子有相机左子没有】取最小
        long coveredNoCam = Math.min(left.coveredHasCam + right.coveredHasCam,
                Math.min(left.coveredHasCam + right.coveredNoCam,
                        left.coveredNoCam + right.coveredHasCam));

        //3)x被覆盖,但是x上有一个相机,【我x有相机,管你x左子右子有没有被覆盖,无所谓,当前x有相机+1,你们都会被覆盖】
        //x一台+左边最少相机数+右边最少相机数
        //左边右边都是有2个情况:
        //左边没有被覆盖,左边覆盖了,不过有相机没相机,取最小
        //右边没有被覆盖,右边覆盖了,不过有相机没相机,取最小
        long coveredHasCam = 1 +
                Math.min(left.unCovered, Math.min(left.coveredNoCam, left.coveredHasCam))
                +
                Math.min(right.unCovered, Math.min(right.coveredNoCam, right.coveredHasCam));

        //返回信息
        return new Info(unCovered, coveredNoCam, coveredHasCam);//顺序别乱
    }

    public static long numbersOfCam(Node head){
        if (head == null) return 0;

        Info info = f(head);//先去就信息
        return Math.min(info.coveredNoCam, info.coveredHasCam);//要覆盖到头,而有无相机看最小了
    }

理解起来是挺难的,但是要收集那三条信息,也还挺明确的。
见过本题之后,再遇到这个题,应该也就不难了。

树形DP贪心法(也是固定的二叉树递归套路,但是微微好理解一些)

【之前写文章时:本来还有一个贪心的办法,但是感觉之前人的状态不好,先不放了,回头有兴趣复习到再放】

今天我又来写,学习一下,贪心的做法
贪心的方法更容易理解,讲给人听也更简单

咱们这样来到x时,我们想统计x的两样信息(Data):
(1)x开头的树,左右子一共放了多少台相机:cameras数量【不包含x哦】
(2)x当前的状态:是未被覆盖呢?(uncovered),还是覆盖了没相机(coveredNoCamera)?还是覆盖了有相机(coveredHasCamera)?
这里用enum来枚举这三种状态,避免boolean值

//学一个新的知识点,Java的常数枚举enumerate枚举enum关坚持
    public static enum Status{
        unCovered, coveredNoCam, coveredHasCam;//多种取值情况
    }
    //就避免了Boolean的状况
    //然后我们在x处收集Status和cameras的数量
    public static class Data{
        public Status status;
        public int cameras;//相机数量

        public Data(Status s, int c){
            status = s;
            cameras = c;
        }
    }

而我x到底能不能被覆盖到,让x的父节点来决定,当且仅当x没有被覆盖的时候【由x的left和right共同决定】,x的父节点必须给父节点自己放一个相机,这样就能覆盖到x。
在这里插入图片描述
所以贪心策略为:
(1)遇到x=null,自然可以认为是:x被覆盖了,但是没有相机,返回给父节点这样的信息:

//遇到叶节点,自然是被覆盖了,但是没有相机,相机数为0
        if (x == null) return new Data(Status.coveredNoCam, 0);

(2)当x节点不是叶节点,就有left和right,请先手机left和right的信息:

//然后就是x节点
        //先收集信息
        Data left = process(x.left);
        Data right = process(x.right);

(3)整理x开头的树左右子一共目前拿到了多少相机?

//然后整理我的信息
        int cameras = left.cameras + right.cameras;//目前左右树已经有这么多相机了

(4)整理x的状态:
——第一种情况:当x节点的left和right中,只要有1个没有被覆盖时,那x就必须给自己放一个相机【这就是x作为left和right的父节点,必须决定x自己放不放,才能覆盖其左右子】

//我x要不要加一台呢??
        //1)如果左右有一个没有被覆盖,我x必定放一台
        if (left.status == Status.unCovered || right.status == Status.unCovered)
            return new Data(Status.coveredHasCam, cameras + 1);//一定放一台

——第二种情况:当x节点的left和right中,每一台都有1台相机,则left和right都必然被覆盖了,且x必然被覆盖,但是不放相机了

//2)如果左右都被覆盖了,但是其中一个子节点有相机,我一定不需要放了,儿子找到到了x
        if (left.status == Status.coveredHasCam || right.status == Status.coveredHasCam)
            return new Data(Status.coveredNoCam, cameras);//有一个儿子有相机,我就OK了,我不要放了

——第三种情况:左右都被覆盖了,也就是说左右都是这样的:
left状态为:coveredHasCamera&&right状态为coveredNoCamera
||left状态为:coveredNoCamera&&right状态为coveredHasCamera
也就是两者不管有没有相机,都被覆盖了的
x要不要放相机,不要放,让x的父节点自己决定自己,然后来覆盖x
这样就省了一台相机,不浪费——这就是贪心所在的地方

//3)如果左右都被覆盖了,但是每个儿子都没有相机,让x的父亲决定x放不放,也就是上面的1)2),x父亲放影响力更大---贪心
        return new Data(Status.unCovered, cameras);//我俩儿子都没有相机,我是没法被覆盖的,而且我现在不能放相机

通过只记录x的左右子的相机总量和整理x的状态信息,咱们可以从下往上推
推到head节点,此时head左右子的相机量已经知道了
而且根据head的左右子知道此时x是不是被覆盖了,是的话,咱就不要相机了,如果没被覆盖,那还需要放相机的
因此主函数还需要检查head的状态

看代码:

//递归
    public static Data process(Node x){
        //遇到叶节点,自然是被覆盖了,但是没有相机,相机数为0
        if (x == null) return new Data(Status.coveredNoCam, 0);

        //然后就是x节点
        //先收集信息
        Data left = process(x.left);
        Data right = process(x.right);

        //然后整理我的信息
        int cameras = left.cameras + right.cameras;//目前左右树已经有这么多相机了
        //我x要不要加一台呢??
        //1)如果左右有一个没有被覆盖,我x必定放一台
        if (left.status == Status.unCovered || right.status == Status.unCovered)
            return new Data(Status.coveredHasCam, cameras + 1);//一定放一台

        //2)如果左右都被覆盖了,但是其中一个子节点有相机,我一定不需要放了,儿子找到到了x
        if (left.status == Status.coveredHasCam || right.status == Status.coveredHasCam)
            return new Data(Status.coveredNoCam, cameras);//有一个儿子有相机,我就OK了,我不要放了

        //3)如果左右都被覆盖了,但是每个儿子都没有相机,让x的父亲决定x放不放,也就是上面的1)2),x父亲放影响力更大---贪心
        return new Data(Status.unCovered, cameras);//我俩儿子都没有相机,我是没法被覆盖的,而且我现在不能放相机,让父亲放就行
    }

    //主函数
    public static int minCameraCover(Node head){
        if (head == null) return 0;

        Data data = process(head);
        //处理完头,可能还没被覆盖呢
        return data.cameras + (data.status == Status.unCovered ? 1 : 0);//我真的没被覆盖,必定放一台
    }

    public static Node createTree(){
        Node head = new Node(0);
        Node n1 = new Node(1);
        Node n2 = new Node(2);
        Node n3 = new Node(3);
        Node n4 = new Node(4);
        Node n5 = new Node(5);
        Node n6 = new Node(6);
        head.left = n1;
        head.right = n2;
        n1.left = n3;
        n1.right = n4;
        n2.left = n5;
        n2.right = n6;

        //         head
        //       1        2
        //    3    4    5    6
        //1放一台,2放一台,这样就足够了

        return head;
    }

测试代码:

public static void test(){
        Node head = createTree();
        System.out.println(numbersOfCam(head));
        System.out.println(minCameraCover(head));
    }

    public static void main(String[] args) {
        test();
    }

总结

提示:重要经验:

1)属性DP的递归套路很有用的,关键在考虑:与x无关,还是与x有关的各种情况
2)遇到这种特别难的,熟悉之后再考就不难了
3)本题树形DP+贪心的解法,更简单,也非常容易理解,要重点学习。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

冰露可乐

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值