让整棵二叉树都被相机覆盖到,请问要给二叉树放多少台相机?
提示:本题是二叉树递归套路中比较难的题目,是互联网大厂经常考的题目
不管是常规的二叉树递归套路还是贪心的递归套路,都比较难,但是这个题,是互联网大厂经常考的题目!
题目
给定二叉树,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+贪心的解法,更简单,也非常容易理解,要重点学习。