看啥推荐读物
专栏名称: 铜炉
成为这个世界上希望看见的改变
今天看啥  ›  专栏  ›  铜炉

[二叉树] 监控二叉树(困难)

铜炉  · 简书  ·  · 2021-03-09 17:05

前言

今天的题目是一道将贪心和动态规划融合在二叉树的题目。

题目

监控二叉树

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

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

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

题目拆解

这道题目的核心关键点在于如何使用最少的监控,覆盖整个树。在最优化的处理办法上,通常使用动态规划和贪心来处理,今天也采用这两种方式进行解答。

动态规划

使用动态规划的思路主要是注意状态转移,在传统的单线路动态规划上,状态转移还比较好实现,而到了二叉树中,状态转移变得稍微有些繁琐,我们来一步步分析。

状态定义

动态规划的思路其实是站在整体的范围考量,以整颗树的覆盖情况作为一个基本状态,然后将状态转移到父节点上。那么,一棵树有几种状态呢?直观来向,有点难度,我们不妨站在一个节点的立场上,考虑下这个节点需要依赖子树的什么状态吧。

  • 左右子树都被覆盖了,但是左右子树的根节点没有监控。
  • 左右子树都被覆盖了,并且左右子树至少有一颗的根节点有监控。
  • 左右子树的子节点都被覆盖了,但是左右子树的根节点至少有一个没有被覆盖。

1、在第一种状态下,左右子树全部被监控覆盖,但是因为左右子树的根节点都没有监控,所以当前节点要考虑要不要加监控了(看父节点能不能加监控)。
2、在第二种状态下,左右子树全部被覆盖,并且有一棵树的根节点已经有监控存在,可以覆盖到当前节点,所以,本节点可以不加。
3、在第三种状态下,左右子树的子节点全部被覆盖,但是至少有一课子树的根节点没有被覆盖,那么当前节点必须加监控,否则会有节点覆盖不到。

所以,整理一下,一个节点是否要加监控有三种选择,必须加,需要自己加或者父节点加,可以不加,我们将这三种状态映射到树的本身,整理三种状态:

1、状态a: 一棵树全部被监控覆盖,并且根节点有监控。(父节点可以不加)

2、状态b:一棵树全部被监控覆盖,根节点可以有监控也可以没有。(父节加不加要看情况)

3、状态c:一棵树除了根节点以外全部被监控覆盖,根节点可以被覆盖也可以不被覆盖。(父节点必须加)

画三张图来说明一下

状态a
image.png
状态b
image.png
状态c
image.png

以上是三种状态下树的展示形态,因为每一个节点的监控添加与否都取决于子树的这三种状态的对应监控数量,所以,我们就为每一个节点维护这三个状态的存储,然后在递归的时候,把状态向父节点转移。

接下来,我们用Ca,Cb,Cc来表示在不同状态下,需要的监控数量,通过示例图可以看出,
Ca>=Cb>=Cc
我们最终实际需要的,或者说我们想获得的监控数量,是在状态b下的监控数量,状态a冗余了一个监控,状态c可能存在缺失根节点覆盖。

所以,动态规划的第一步,我们完成了,用一个int[]维护三种状态对应的监控数量。

接下来,来进行状态转移。

状态转移

状态定义好了,状态转移其实比较容易。
遍历每一个节点的时候,分别计算Ca,Cb,Cc的值,假设子节点的状态数组为int[] left,右节点的状态数组为int[] right。那么有。

  • Ca = left[2]+right[2] + 1; // 不管子节点根节点是否有监控,直接把当前节点插一个监控。
  • Cb = Math.min(Ca,Math.min(left[0]+right[1],left[1]+right[0])) // 从左右子树分别取一个Ca和一个Cb,最后取最小值,保证根节点被覆盖,然后再和Ca取最小值。
  • Cc = Math.min(Cb,left[1]+right[2])

这样我们就完成了状态转移,有了状态定义和状态转移,我们的代码也就出来了。

动态规划代码

class Solution {


    public int minCameraCover(TreeNode root) {
        int[] res = dfsOne(root);
        return res[1];


    }

    int[] dfsOne(TreeNode node) {
        if (node == null) return new int[]{Integer.MAX_VALUE / 2, 0, 0};

        int[] array = new int[3];
        int[] leftArray = dfsOne(node.left);
        int[] rightArray = dfsOne(node.right);

        int a = leftArray[2] + rightArray[2] + 1;
        int b = Math.min(a, Math.min(leftArray[1] + rightArray[0], leftArray[0] + rightArray[1]));
        int c = Math.min(b, leftArray[1] + rightArray[1]);

        array[0] = a;
        array[1] = b;
        array[2] = c;
        return array;


    }
}

**************************************分割线**************************************

贪心

贪心算法的核心,其实就一个字,贪。

如果说还有一个字,那就是,懒。

相比于动态规划是站在整棵树的立场上触发,贪心就完全是站在节点自身的角度出发。
除非必须加监控,否则绝对不加监控 ,这就贪心算法的核心,在梳理逻辑的时候也要把这句话牢记在心头。

那我们需要做的就是看看遍历到一个节点时这个节点的心路历程。

  • 左右子树的根节点有监控了,那太好了,啥也不用管,告诉老板被覆盖了,不用管我了。
  • 如果子节点被监控覆盖了,但是左右子树的根节点没有监控,这时候咋办呢,自己先不管,把这事先告诉老板,让老板看看咋办。
  • 如果子节点没有被监控覆盖,那么当前节点必须加,不加没人能覆盖子节点了,告诉老板我加监控了,邀功请赏。

这三种状态,与动态规划不同的是,贪心算法中,当前节点只向上级汇报自己的状态,不需要管整棵树的状态。

我们也将该节点的状态定义一下。

  • 状态1,当前节点已经被覆盖
  • 状态2,当前节点没被覆盖
  • 状态3,当前节点有监控

这个思路有了,判断的标准也出来了,我们来看下贪心的代码

贪心代码

class Solution {


    public int minCameraCover(TreeNode root) {


        int resTwo = dfsTwo(root);
        if (resTwo == 2) {
            totalCount++;
        }

        return totalCount;
    }

    private int totalCount = 0;

    int dfsTwo(TreeNode node) {
        // 状态1,当前节点已经被覆盖
        // 状态2,当前节点没被覆盖
        // 状态3,当前节点有摄像头

        if (node == null) {
            // 空节点默认被覆盖
            return 1;
        }

        int left = dfsTwo(node.left);
        int right = dfsTwo(node.right);

        // 自己的两个子节点有一方没有被覆盖,那么自己不加不行了,在当前节点加一个监控
        if (left == 2 || right == 2) {
            totalCount++;
            return 3;
        }

        // 两个子节点有一个加了监控,那当前节点已经被覆盖,没必要加监控了.
        if (left == 3 || right == 3) {
            return 1;
        }
        // 到这里只剩下一种情况 left == 1 right == 1
        // 说明当前节点没有被覆盖,但是子节点已经被覆盖了,自己先不管,把这个结果告诉老板就得了.

        return 2;

    }

总结

今天这道题是一道困难题,所以整理了一下官方题解和其他各路大神的题解,总结了这两种思路来做,也表示一下对困难题目的respect。

实际上,可以看出来,这道题目的代码不多,去掉注释就没几行了,但是思考问题的切入点和思路确实有点复杂,是需要认真琢磨一下的。

此外,动态规划和贪心两种思路上,其实本质都是一种状态的向上传递,只不过动态规划立足于描述整棵树,贪心描述节点自身,动态规划省却了很多状态的判断,贪心没有那么多数据的冗余。

喜欢哪个就用哪个吧~




原文地址:访问原文地址
快照地址: 访问文章快照