一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第29天,点击查看活动详情。
题目链接:427. 建立四叉树
题目描述
给你一个 n * n 矩阵 grid ,矩阵由若干 0 和 1 组成。请你用四叉树表示该矩阵 grid 。
你需要返回能表示矩阵的 四叉树 的根结点。
注意,当 isLeaf 为 False 时,你可以把 True 或者 False 赋值给节点,两种值都会被判题机制 接受 。
四叉树数据结构中,每个内部节点只有四个子节点。此外,每个节点都有两个属性:
val:储存叶子结点所代表的区域的值。1对应True,0对应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;
}
我们可以按以下步骤为二维区域构建四叉树:
- 如果当前网格的值相同(即,全为
0或者全为1),将isLeaf设为True,将val设为网格相应的值,并将四个子节点都设为Null然后停止。 - 如果当前网格的值不同,将
isLeaf设为False, 将val设为任意值,然后如下图所示,将当前网格划分为四个子网格。 - 使用适当的子网格递归每个子节点。
如果你想了解更多关于四叉树的内容,可以参考 wiki 。
四叉树格式:
输出为使用层序遍历后四叉树的序列化形式,其中 null 表示路径终止符,其下面不存在节点。
它与二叉树的序列化非常相似。唯一的区别是节点以列表形式表示 [isLeaf, val] 。
如果 isLeaf 或者 val 的值为 True ,则表示它在列表 [isLeaf, val] 中的值为 1 ;如果 isLeaf 或者 val 的值为 False ,则表示值为 0 。
提示:
- 其中
代码注释部分 (在代码中注释掉的提示部分,包括类 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:
输入: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]]
题意整理
题目给了一个正方形矩阵,按照题目中声明的 topLeft、topRight、bottomLeft 和 bottomRight 将一个大正方形区域划分为 4 个小区域。要求划分至每个小区域中的 val 全部一致,更形象的来说:如果正方形区域中的 val 全部一致(0 和 1 都可以)表示为当前区域表示一个叶子节点,叶子节点的值为当前区域中一致的这个值(0 或 1),如果正方形区域中的 val 不一致则需要继续划分该区域至每个小区域中的 val 全部一致,题目给出对应的 四叉树 类 Node,让我们按照上述规则构造一棵四叉树,返回其根节点。
解题思路分析
习惯性动作,先看题目数据范围: 其中 ,也就是说 ,最大的正方形边长为 64,数据范围相对较小。
该题难在题目理解上,其次的难点在于如何判断当前区域是否是叶子节点,根据定义,如果当前区域中所有方格的值相同(这里的值只可能是 0 和 1),也就是全为 1 或者全为 0 时表示当前区域为叶子节点。
根据定义我们很容易想到暴力遍历当前区域,时间复杂度为 ( n 为正方形边长),由于数据范围较小,这个时间复杂度也是能够承受的。
不过我们很容易想到优化方案,由于矩阵中的值只有可能是 0 或 1,那么我们可以用二维前缀和进行预处理来优化每次暴力遍历的时间,从而快速地进行判断当前区域是否是叶子节点。
剩下的操作就是不断递归遍历和判断了。
具体实现
- 首先将矩阵进行预处理,求出其二维前缀和,具体求法如下:
对应的代码实现为:
pre[i][j] = pre[i - 1][j] + pre[i][j - 1] - pre[i - 1][j - 1] + grid[i - 1][j - 1];
这里需要注意边界问题,为了方便在求前缀和时将正方形整体向下和向右移了
1格,多出来的上边界和左边界用0填充。
- 每次递归需要处理的区域即可。
- 利用前缀和快速判断当前区域是否是叶子节点:
利用前缀和快速求得某块区域的值:
对应的代码实现如下:
//当前矩阵和
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);
- 如果当前区域不是叶子节点,则继续将该区域划分为
4个小区域进行递归处理,直至为叶子节点。
复杂度分析
- 不使用前缀和优化:
- 时间复杂度为:,
n为正方形的边长。 - 空间复杂度:,即为递归需要使用的栈空间。
- 时间复杂度为:,
- 使用前缀和优化:
- 时间复杂度:,
n为正方形的边长。 - 空间复杂度:,即为二维前缀和需要使用的空间。
- 时间复杂度:,
代码实现
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;
- 还需要注意是否时指针,如果时指针,在调用成员变量或者方法的时候需要使用的时
->; - 而如果是结构体的话只需要
.操作即可。
结束语
青春不是任性和冲动,而是敢于追梦的勇气和对生活的热爱;成熟不代表圆滑和世故,而应是历经岁月、阅遍世事后对人生的洞察和对理想的坚守。愿我们无论什么年纪,都能让心中的梦想永不凋零。