【C/C++】427. 建立四叉树

298 阅读7分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第29天,点击查看活动详情


题目链接:427. 建立四叉树

题目描述

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

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

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

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

  • val:储存叶子结点所代表的区域的值。1 对应 True0 对应 False
  • isLeaf: 当这个节点是一个叶子结点时为 True,如果它有 4 个子节点则为 False
class Node {
    public boolean val;
    public boolean isLeaf;
    public Node topLeft;
    public Node topRight;
    public Node bottomLeft;
    public Node bottomRight;
}

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

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

new_top.png

如果你想了解更多关于四叉树的内容,可以参考 wiki 。

四叉树格式:

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

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

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

提示:

  • n==grid.length==grid[i].lengthn == grid.length == grid[i].length
  • n==2xn == 2^x 其中 0x60 \leqslant x \leqslant 6

代码注释部分 (在代码中注释掉的提示部分,包括类 Node 的相关介绍)

// Definition for a QuadTree node.
class Node {
public:
    bool val;
    bool isLeaf;
    Node* topLeft;
    Node* topRight;
    Node* bottomLeft;
    Node* bottomRight;
    
    Node() {
        val = false;
        isLeaf = false;
        topLeft = NULL;
        topRight = NULL;
        bottomLeft = NULL;
        bottomRight = NULL;
    }
    
    Node(bool _val, bool _isLeaf) {
        val = _val;
        isLeaf = _isLeaf;
        topLeft = NULL;
        topRight = NULL;
        bottomLeft = NULL;
        bottomRight = NULL;
    }
    
    Node(bool _val, bool _isLeaf, Node* _topLeft, Node* _topRight, Node* _bottomLeft, Node* _bottomRight) {
        val = _val;
        isLeaf = _isLeaf;
        topLeft = _topLeft;
        topRight = _topRight;
        bottomLeft = _bottomLeft;
        bottomRight = _bottomRight;
    }
};

示例 1:

grid1.png

输入:grid = [[0,1],[1,0]]
输出:[[0,1],[1,0],[1,1],[1,1],[1,0]]
解释:此示例的解释如下:
请注意,在下面四叉树的图示中,0 表示 false1 表示 True 。

e1tree.png

示例 2:

e2mat.png

输入: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 个子网格,这样每个子网格都具有相同的值。
解释如下图所示:

e2tree.png

示例 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]]

题意整理

题目给了一个正方形矩阵,按照题目中声明的 topLefttopRightbottomLeftbottomRight 将一个大正方形区域划分为 4 个小区域。要求划分至每个小区域中的 val 全部一致,更形象的来说:如果正方形区域中的 val 全部一致(01 都可以)表示为当前区域表示一个叶子节点,叶子节点的值为当前区域中一致的这个值(01),如果正方形区域中的 val 不一致则需要继续划分该区域至每个小区域中的 val 全部一致,题目给出对应的 四叉树Node,让我们按照上述规则构造一棵四叉树,返回其根节点。

解题思路分析

习惯性动作,先看题目数据范围:n==2xn == 2^x 其中 0x60 \leqslant x \leqslant 6,也就是说 1n26=641 \leqslant n \leqslant 2^6 = 64,最大的正方形边长为 64,数据范围相对较小。

该题难在题目理解上,其次的难点在于如何判断当前区域是否是叶子节点,根据定义,如果当前区域中所有方格的值相同(这里的值只可能是 01),也就是全为 1 或者全为 0 时表示当前区域为叶子节点。

根据定义我们很容易想到暴力遍历当前区域,时间复杂度为 O(n2)O(n^2)n 为正方形边长),由于数据范围较小,这个时间复杂度也是能够承受的。

不过我们很容易想到优化方案,由于矩阵中的值只有可能是 01,那么我们可以用二维前缀和进行预处理来优化每次暴力遍历的时间,从而快速地进行判断当前区域是否是叶子节点。

剩下的操作就是不断递归遍历和判断了。

具体实现

  1. 首先将矩阵进行预处理,求出其二维前缀和,具体求法如下:

微信截图_20220429141426.png

对应的代码实现为:

pre[i][j] = pre[i - 1][j] + pre[i][j - 1] - pre[i - 1][j - 1] + grid[i - 1][j - 1];

这里需要注意边界问题,为了方便在求前缀和时将正方形整体向下和向右移了 1 格,多出来的上边界和左边界用 0 填充。

  1. 每次递归需要处理的区域即可。
  2. 利用前缀和快速判断当前区域是否是叶子节点:

利用前缀和快速求得某块区域的值: image.png

对应的代码实现如下:

//当前矩阵和
int sum = pre[x2][y2] - pre[x2][y1] - pre[x1][y2] + pre[x1][y1];
//全为0
if(sum == 0) return new Node(false, true);
//全为1
if(sum == ((x2 - x1) * (y2 - y1))) return new Node(true, true);
  1. 如果当前区域不是叶子节点,则继续将该区域划分为 4 个小区域进行递归处理,直至为叶子节点。

复杂度分析

  • 不使用前缀和优化:
    • 时间复杂度为:O(n2logn)O(n^2 \log n)n 为正方形的边长。
    • 空间复杂度:O(logn)O(\log n),即为递归需要使用的栈空间。
  • 使用前缀和优化:
    • 时间复杂度:O(n2)O(n^2)n 为正方形的边长。
    • 空间复杂度:O(n2)O(n^2),即为二维前缀和需要使用的空间。

代码实现

class Solution {
private:
    int n;
    vector<vector<int>> pre;
    Node* dfs(int x1, int y1, int x2, int y2){
        //当前矩阵和
        int sum = pre[x2][y2] - pre[x2][y1] - pre[x1][y2] + pre[x1][y1];
        //全为0
        if(sum == 0) return new Node(false, true);
        //全为1
        if(sum == ((x2 - x1) * (y2 - y1))) return new Node(true, true);
        return new Node(
            true,
            false,
            dfs(x1, y1, (x1 + x2) / 2, (y1 + y2) / 2),
            dfs(x1, (y1 + y2) / 2, (x1 + x2) / 2, y2),
            dfs((x1 + x2) / 2, y1, x2, (y1 + y2) / 2),
            dfs((x1 + x2) / 2, (y1 + y2) / 2, x2, y2)
        );
    }
public:
    Node* construct(vector<vector<int>>& grid) {
        //n为矩阵边长
        n = grid.size();
        pre.resize(n + 1, vector<int>(n + 1));
        //初始化矩阵为0
        for(int i = 0; i <= n; i++){
            for(int j = 0; j <= n; j++) pre[i][j] = 0;
        }
        //矩阵前缀和
        for(int i = 1; i <= n; i++){
            for(int j = 1; j <= n; j++){
                pre[i][j] = pre[i - 1][j] + pre[i][j - 1] - pre[i - 1][j - 1] + grid[i - 1][j - 1];
            }
        }
        return dfs(0, 0, n, n);
    }
};

总结

该题的难点在于题目的理解上;其次在于思考利用前缀和进行优化,这里需要使用 二维前缀和 ,结合图形很好理解二维前缀和,需要注意边界问题的处理;最后是 递归 的思想,当题目给出的定义带有一定的嵌套递归在其中时,往往是可以通过递归来解决。

当题目给的是类对象时我们需要 new 操作,而结构体对象不需要 new 操作:

class Node {
    ...
};
//Node* Now = new Node();
//Now->xxx;

struct node{
    ...
};
//node now;
//now.xxx;
  • 还需要注意是否时指针,如果时指针,在调用成员变量或者方法的时候需要使用的时 ->
  • 而如果是结构体的话只需要 . 操作即可。

结束语

青春不是任性和冲动,而是敢于追梦的勇气和对生活的热爱;成熟不代表圆滑和世故,而应是历经岁月、阅遍世事后对人生的洞察和对理想的坚守。愿我们无论什么年纪,都能让心中的梦想永不凋零。