427. 建立四叉树 : 递归与前缀和优化

简介: 427. 建立四叉树 : 递归与前缀和优化

网络异常,图片无法展示
|


题目描述



这是 LeetCode 上的 427. 建立四叉树 ,难度为 中等


Tag : 「递归」、「前缀和」


给你一个 n * nnn 矩阵 grid ,矩阵由若干 0011 组成。请你用四叉树表示该矩阵 grid


你需要返回能表示矩阵的 四叉树 的根结点。


注意,当 isLeafFalse 时,你可以把 True 或者 False 赋值给节点,两种值都会被判题机制 接受 。


四叉树数据结构中,每个内部节点只有四个子节点。此外,每个节点都有两个属性:


  • val:储存叶子结点所代表的区域的值。11 对应 True00 对应 False
  • isLeaf: 当这个节点是一个叶子结点时为 True,如果它有 44 个子节点则为 False


class Node {
    public boolean val;
    public boolean isLeaf;
    public Node topLeft;
    public Node topRight;
    public Node bottomLeft;
    public Node bottomRight;
}
复制代码


我们可以按以下步骤为二维区域构建四叉树:


  1. 如果当前网格的值相同(即,全为 00 或者全为 11),将 isLeaf 设为 True ,将 val 设为网格相应的值,并将四个子节点都设为 Null 然后停止。
  2. 如果当前网格的值不同,将 isLeaf 设为 False, 将 val 设为任意值,然后如下图所示,将当前网格划分为四个子网格。
  3. 使用适当的子网格递归每个子节点。

网络异常,图片无法展示
|

四叉树格式:


输出为使用层序遍历后四叉树的序列化形式,其中 null 表示路径终止符,其下面不存在节点。


它与二叉树的序列化非常相似。唯一的区别是节点以列表形式表示 [isLeaf, val][isLeaf,val]


如果 isLeaf 或者 val 的值为 True ,则表示它在列表 [isLeaf, val][isLeaf,val] 中的值为 11 ;如果 isLeaf 或者 val 的值为 False ,则表示值为 00


示例 1:


网络异常,图片无法展示
|


输入:grid = [[0,1],[1,0]]
输出:[[0,1],[1,0],[1,1],[1,1],[1,0]]
解释:请注意,在下面四叉树的图示中,0 表示 false,1 表示 True 。
复制代码


示例 2:


网络异常,图片无法展示
|


输入:grid = [[1,1,1,1,0,0,0,0],[1,1,1,1,0,0,0,0],[1,1,1,1,1,1,1,1],[1,1,1,1,1,1,1,1],[1,1,1,1,0,0,0,0],[1,1,1,1,0,0,0,0],[1,1,1,1,0,0,0,0],[1,1,1,1,0,0,0,0]]
输出:[[0,1],[1,1],[0,1],[1,1],[1,0],null,null,null,null,[1,0],[1,0],[1,1],[1,1]]
解释:网格中的所有值都不相同。我们将网格划分为四个子网格。
topLeft,bottomLeft 和 bottomRight 均具有相同的值。
topRight 具有不同的值,因此我们将其再分为 4 个子网格,这样每个子网格都具有相同的值。
复制代码


示例 3:


输入:grid = [[1,1],[1,1]]
输出:[[1,1]]
复制代码


示例 4:


输入:grid = [[0]]
输出:[[1,0]]
复制代码


示例 5:


输入:grid = [[1,1,0,0],[1,1,0,0],[0,0,1,1],[0,0,1,1]]
输出:[[0,1],[1,1],[1,0],[1,0],[1,1]]
复制代码


提示:


  • n == grid.length == grid[i].lengthn==grid.length==grid[i].length
  • n == 2^xn==2x 其中 0 <= x <= 60<=x<=6


递归



假定我们存在函数 Node dfs(int a, int b, int c, int d),其能够返回「以 (a, b)(a,b) 为左上角,(c, d)(c,d) 为右下角」所代表的矩阵的根节点。


那么最终答案为 dfs(0, 0, n-1, n-1),不失一般性考虑「以 (a, b)(a,b) 为左上角,(c, d)(c,d) 为右下角」时如何计算:


  • 判断该矩阵是否为全00或全11
  • 如果是则直接创建根节点(该节点四个子节点属性均为空)并进行返回;
  • 如果不是则创建根节点,递归创建四个子节点并进行赋值,利用左上角 (a,b)(a,b) 和右下角 (c, d)(c,d) 可算的横纵坐标的长度为 c - a + 1ca+1d - b + 1db+1,从而计算出将当前矩阵四等分所得到的子矩阵的左上角和右下角坐标。


由于矩阵大小最多为 2^6 = 6426=64 ,因此判断某个子矩阵是否为全 00 或全 11 的操作用「前缀和」或者是「暴力」来做都可以。


代码:


class Solution {
    int[][] g;
    public Node construct(int[][] grid) {
        g = grid;
        return dfs(0, 0, g.length - 1, g.length - 1);
    }
    Node dfs(int a, int b, int c, int d) {
        boolean ok = true;
        int t = g[a][b];
        for (int i = a; i <= c && ok; i++) {
            for (int j = b; j <= d && ok; j++) {
                if (g[i][j] != t) ok = false;
            }
        }
        if (ok) return new Node(t == 1, true);
        Node root = new Node(t == 1, false);
        int dx = c - a + 1, dy = d - b + 1;
        root.topLeft = dfs(a, b, a + dx / 2 - 1, b + dy / 2 - 1); 
        root.topRight = dfs(a, b + dy / 2, a + dx / 2 - 1, d);
        root.bottomLeft = dfs(a + dx / 2, b, c, b + dy / 2 - 1);
        root.bottomRight = dfs(a + dx / 2, b + dy / 2, c, d);
        return root;
    }
}
复制代码


  • 时间复杂度:递归的复杂度分析要根据主定理,假设矩阵大小为 n * nnn,根据主定理 T(n) = aT(\frac{n}{b}) + f(n)T(n)=aT(bn)+f(n),单次递归最多会产生 44 个子问题(由大矩阵递归 44 个小矩阵),因此问题递归子问题数量 a = 4a=4,而子问题规模缩减系数 bb 为原本的一半(子矩阵的大小为 \frac{n}{2} * \frac{n}{2}2n2n),剩余的 f(n)f(n) 为判断全 00 和 全 11 的时间开销,不考虑标识位 okok 带来的剪枝效果,每次判断全 00 或全 11 的复杂度与当前问题规模相等,即 f(n) = O(n^2)f(n)=O(n2),但整个大小为 n * nnn 矩阵每次进行长宽减半的子矩阵拆分,最多会被拆分为 \log{n}logn 次,因此这部分总的计算量为 \log{n} * n^2lognn2 。整体复杂度为 O(n^2 + \log{n} * n^2)O(n2+lognn2)
  • 空间复杂度:忽略递归带来的额外空间开销,复杂度为 O(1)O(1)


递归(前缀和优化)



使用前缀和优化「判断全 00 和全 11」的操作:对矩阵 grid 求前缀和数组 sum,对于一个「以左上角为 (a, b)(a,b),右下角为 (c, d)(c,d) 」的子矩阵而言,其所包含的格子总数为 tot = (c - a + 1) * (d - b + 1)tot=(ca+1)(db+1) 个,当且仅当矩阵和为 00tottot 时,矩阵全 0011


代码:


class Solution {
    static int[][] sum = new int[70][70];   
    int[][] g;
    public Node construct(int[][] grid) {
        g = grid;
        int n = grid.length;
        for (int i = 1; i <= n; i++) {
            for (int j = 1; j <= n; j++) {
                sum[i][j] = sum[i - 1][j] + sum[i][j - 1] - sum[i - 1][j - 1] + g[i - 1][j - 1];
            }
        }
        return dfs(0, 0, n - 1, n - 1);
    }
    Node dfs(int a, int b, int c, int d) {
        int cur = sum[c + 1][d + 1] - sum[a][d + 1] - sum[c + 1][b] + sum[a][b];
        int dx = c - a + 1, dy = d - b + 1, tot = dx * dy;
        if (cur == 0 || cur == tot) return new Node(g[a][b] == 1, true);
        Node root = new Node(g[a][b] == 1, false);
        root.topLeft = dfs(a, b, a + dx / 2 - 1, b + dy / 2 - 1);
        root.topRight = dfs(a, b + dy / 2, a + dx / 2 - 1, d);
        root.bottomLeft = dfs(a + dx / 2, b, c, b + dy / 2 - 1);
        root.bottomRight = dfs(a + dx / 2, b + dy / 2, c, d);
        return root;
    }
}
复制代码


  • 时间复杂度:分析同理,但判断全 00 和全 11 的复杂度下降为 O(1)O(1),整体复杂度为 O(n^2 + \log{n})O(n2+logn)
  • 空间复杂度:忽略递归带来的额外空间开销,复杂度为 O(n^2)O(n2)


最后



这是我们「刷穿 LeetCode」系列文章的第 No.427 篇,系列开始于 2021/01/01,截止于起始日 LeetCode 上共有 1916 道题目,部分是有锁题,我们将先把所有不带锁的题目刷完。


在这个系列文章里面,除了讲解解题思路以外,还会尽可能给出最为简洁的代码。如果涉及通解还会相应的代码模板。


为了方便各位同学能够电脑上进行调试和提交代码,我建立了相关的仓库:github.com/SharingSour…


在仓库地址里,你可以看到系列文章的题解链接、系列文章的相应代码、LeetCode 原题链接和其他优选题解。

相关文章
二维偏序问题应用(二维数点)
二维偏序问题应用(二维数点)
188 0
|
定位技术 API C#
.NET开源、功能强大、跨平台的图表库
.NET开源、功能强大、跨平台的图表库
378 8
|
算法 前端开发 C++
C++基础知识(八:STL标准库 deque )
deque在C++的STL(Standard Template Library)中是一个非常强大的容器,它的全称是“Double-Ended Queue”,即双端队列。deque结合了数组和链表的优点,提供了在两端进行高效插入和删除操作的能力,同时保持了随机访问的特性。
648 1
|
安全 算法 Go
有向图的强联通分量(SCC)Tarjan算法
有向图的强联通分量(SCC)Tarjan算法
755 0
|
算法 定位技术 Python
秒懂算法 | A*算法实现最优路径规划
启发式探索是利用问题拥有的启发信息来引导搜索,达到减少探索范围、降低问题复杂度的目的。A*寻路算法是启发式探索的一个典型实践,在寻路搜索的过程中,给每个节点绑定了一个估计值(即启发式),在对节点的遍历过程中采取估计值优先原则,估计值更优的节点会被优先遍历。
3839 1
秒懂算法  | A*算法实现最优路径规划
|
存储 人工智能 机器人
AI 协助办公 |记一次用 GPT-4 写一个消息同步 App
GPT-4 最近风头正劲,作为 NebulaGraph 的研发人员的我自然是跟进新技术步伐。恰好,现在有一个将 Slack channel 消息同步到其他 IM 的需求,看看 GPT-4 能不能帮我完成这次的信息同步工具的代码编写工作。
282 0
AI 协助办公 |记一次用 GPT-4 写一个消息同步 App
|
编译器 C语言 C++
C/C++中int128的那点事
C/C++中int128的那点事
1281 0
C/C++中int128的那点事
|
存储
【数据结构】线段树(入门)
【数据结构】线段树(入门)
184 0
【数据结构】线段树(入门)
|
消息中间件 网络协议 Java
ActiveMQ系列:结合Spring,基于配置文件的使用ActiveMQ
从activemq脚本可以看出启动ActiveMQ实际是启动,bin文件夹下的其实activemq.jar 包中有一个类为Main,这就是active的启动入口,Main主要是加载lib目录和ClassPath,初始化 类加载器,委托给ShellCommand,由ShellCommand根据命令描述去执行,如果是Version和HELP, 则打印信息,若是启动命令,则通过XBeanBrokerFactory创建BrokerService
319 0
ActiveMQ系列:结合Spring,基于配置文件的使用ActiveMQ
|
存储 算法 安全
什么是 Hash 算法?
散列算法(Hash Algorithm),又称哈希算法,杂凑算法,是一种从任意文件中创造小的数字「指纹」的方法。与指纹一样,散列算法就是一种以较短的信息来保证文件唯一性的标志,这种标志与文件的每一个字节都相关,而且难以找到逆向规律。因此,当原有文件发生改变时,其标志值也会发生改变,从而告诉文件使用者当前的文件已经不是你所需求的文件。
什么是 Hash 算法?