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 原题链接和其他优选题解。

相关文章
|
11月前
|
数据安全/隐私保护 开发者
Racket 语言在局域网上网控制软件中的潜力挖掘
在数字化时代,局域网上网控制软件对企业、学校和家庭至关重要。Racket 语言凭借其多范式特性,在开发此类软件中展现出巨大潜力。本文介绍了 Racket 在网络连接检测、访问控制和流量监测方面的应用,并提供了代码示例。
126 0
|
关系型数据库 MySQL 索引
mysql索引失效的原因以及解决办法
该内容列举了索引失效的五个原因,包括:条件表达式中的函数使用、不等于操作符、列类型不匹配、LIKE操作的模糊匹配和数据量过小。并提供了对应的解决办法:避免函数操作索引列、使用合适条件、保证类型匹配、选择合适索引、优化表结构和使用索引提示。
1073 1
|
定位技术 API 开发者
【Godot引擎开发】简单基础,外加一个小游戏DEMO
【Godot引擎开发】简单基础,外加一个小游戏DEMO
417 0
|
JavaScript 前端开发 Java
CocosCreator 面试题(十)Cocos Creator 内存管理
CocosCreator 面试题(十)Cocos Creator 内存管理
760 0
|
JavaScript 前端开发 算法
游戏物理系统 - 介绍一下Box2D或其他物理引擎在JS小游戏中的使用。
Box2D, a popular 2D physics engine, simulates rigid body dynamics, collision detection, and constraints for JavaScript games via WebAssembly. It offers realistic physics, efficient collision handling, and customizable APIs.
179 4
|
Oracle 关系型数据库 MySQL
关于MySQL的分区索引
前段时间有同事问MySQL 分区索引是全局索引还是本地索引。全局索引和本地索引是Oracle的功能,MySQL(包括PostgreSQL)只实现了本地索引,并且因为有全局约束的问题,MySQL分区表明确不支持外键,并且主键和唯一键必须要包含所有分区列,否则报错。
6270 1
|
存储 人工智能 机器人
AI 协助办公 |记一次用 GPT-4 写一个消息同步 App
GPT-4 最近风头正劲,作为 NebulaGraph 的研发人员的我自然是跟进新技术步伐。恰好,现在有一个将 Slack channel 消息同步到其他 IM 的需求,看看 GPT-4 能不能帮我完成这次的信息同步工具的代码编写工作。
181 0
AI 协助办公 |记一次用 GPT-4 写一个消息同步 App
|
JSON 前端开发 Java
OpenFeign教程
由浅入深拿下OpenFeign
1712 0
OpenFeign教程
|
Web App开发 缓存 前端开发
前端获取微信头像 base64 数据的踩坑实践
应用场景 前端生成一张图片, 一般是基于页面的内容(DOM)生成一张用于分享的海报形式的图片(例如通过 html2canvas)。不过当分享的图片要包含微信用户的头像时(图片位于 thirdwx.qlogo.cn 域名,没有转存到自己的域名下),微信用户的头像图片相当于页面是跨域的。我们如何解决此场景下获取微信头像的问题。
|
消息中间件 网络协议 Java
ActiveMQ系列:结合Spring,基于配置文件的使用ActiveMQ
从activemq脚本可以看出启动ActiveMQ实际是启动,bin文件夹下的其实activemq.jar 包中有一个类为Main,这就是active的启动入口,Main主要是加载lib目录和ClassPath,初始化 类加载器,委托给ShellCommand,由ShellCommand根据命令描述去执行,如果是Version和HELP, 则打印信息,若是启动命令,则通过XBeanBrokerFactory创建BrokerService
238 0
ActiveMQ系列:结合Spring,基于配置文件的使用ActiveMQ