今天分享一个LeetCode题,题号是699,标题是掉落的方块,题目标签是线段树,题目难度是困难。java
这篇文章写着写着,篇幅就变得有点长了,可是这对你颇有帮助,由于我在写Java代码过程当中进行了两步优化,过程都写下来了。算法
后面也会贴Go语言代码,记得收哦,简单对比了Java和Go语言的执行分析,对学习Go语言有好处的。segmentfault
在无限长的数轴(即 x 轴)上,咱们根据给定的顺序放置对应的正方形方块。数组
第 i 个掉落的方块(positions[i] = (left, side_length))是正方形,其中 left 表示该方块最左边的点位置(positionsi),side_length 表示该方块的边长(positionsi)。app
每一个方块的底部边缘平行于数轴(即 x 轴),而且从一个比目前全部的落地方块更高的高度掉落而下。在上一个方块结束掉落,并保持静止后,才开始掉落新方块。框架
方块的底边具备很是大的粘性,并将保持固定在它们所接触的任何长度表面上(不管是数轴仍是其余方块)。邻接掉落的边不会过早地粘合在一块儿,由于只有底边才具备粘性。ide
返回一个堆叠高度列表 ans 。每个堆叠高度 ans[i] 表示在经过 positions[0], positions[1], ..., positions[i] 表示的方块掉落结束后,目前全部已经落稳的方块堆叠的最高高度。学习
示例 1:优化
输入: [[1, 2], [2, 3], [6, 1]] 输出: [2, 5, 5] 解释: 第一个方块 positions[0] = [1, 2] 掉落: _aa _aa ------- 方块最大高度为 2 。 第二个方块 positions[1] = [2, 3] 掉落: __aaa __aaa __aaa _aa__ _aa__ -------------- 方块最大高度为5。 大的方块保持在较小的方块的顶部, 不论它的重心在哪里,由于方块的底部边缘有很是大的粘性。 第三个方块 positions[1] = [6, 1] 掉落: __aaa __aaa __aaa _aa _aa___a -------------- 方块最大高度为5。 所以,咱们返回结果[2, 5, 5]。
示例 2:this
输入: [[100, 100], [200, 100]] 输出: [100, 100] 解释: 相邻的方块不会过早地卡住,只有它们的底部边缘才能粘在表面上。
注意:
1 <= positions.length <= 1000. 1 <= positions[i][0] <= 10^8. 1 <= positions[i][1] <= 10^6.
仍是老样子,不论是先看题目描述仍是先看题目标签,均可随意安排。
由于我是先选了线段树的标签,而后随机选一个题看看,这样子先看题目标签再看题目描述,没毛病!
想到线段树,天然会想到它的框架,先分治再合并。不过这道题,可不是先分支再合并这么简单了。
咱们看完题目描述以后,假设输入示例是这样的 {{5, 2}, {6, 1}, {4, 1}, {2, 3}}
,按照线段树的框架,天然会变成下面这样的:
咱们获得的树底下的节点以后,怎么拆分是一个问题,怎么合并也是一个问题。
例如咱们获得 {5,2}这个节点,能够设计成 {5,7,2},分别是左边界、有边界和高度,不过咱们设计的高度是向下的,以下面图:
经过左递归获得{5,2}这个节点,变换成{5,7,2};经过右递归获得{6,1}这个节点,变换成{6,7,1};接着进行合并,这个问题就来了,怎么合并也是一个问题。
或许咱们能够设计成下面这样:
由于,题目要求掉落的方块是有顺序性的,不可能随机掉落哪一个方块仍然答案是惟一的。因此咱们按照了每一个节点的左边界进行比较。
若是这个节点的左边界比根节点左边界小的话,那这个节点往根节点的左孩子递归;反之这个节点往根节点的右孩子递归;到下一个孩子节点也是这样比较和递归。
最后,咱们获得了这样的一个图:
最关键的一点来了,接着上面的图,这两个子集合并应该怎样进行呢?
由于咱们要保证方块掉落的顺序,右边子集的根节点要先和左边子集的根节点比较和递归,变成下面这样的:
并且从上面的图能够翻译成下面这样的:
这已经涉及到图论建模了,这图不论是进行深度遍历仍是广度遍历总会找到目前区间的最高的高度。
可是这已经不符合线段树的优化了,咱们知道线段树能够分治吧,分治的目的是下降时间复杂度。
你看,若是掉落的方块变成下面这样的,若是要找到区间【7,8】,就只能经过深度遍历或广度遍历才能找到这区间的最高高度为3。
若是咱们把图论建模成下面这样的:
再复杂点,就变成下面这样的,若是找到【3,5】,遍历的时候能够判断是否知足r <= root.l
这个条件,若是知足,就不必递归这个节点的右孩子了,由于5根本就不可能跑到5后面的坐标,因此我在这个地方进行了剪枝操做,待会看后面代码会有注释。
因此,咱们原本想经过线段树的思路解决此题,到最后变成了图论建模。若是这道题是单纯的使用线段树,忘记了分治算法的优势的话,时间复杂度并无O(log n)这样的,仍须要所有遍历才能找到这个区间的最高高度。
因此,在这道题上,咱们先仍是按顺序一个一个进行合并,如前面两个合并,第三个和前面合并,第四个和前面合并,依次类推。
既然线段树变成图论建模这地步了,咱们就按着图论建模继续优化吧。
咱们知道,我把这每个节点定义成{左边界,右边界,高度},每一次将节点放置的时候是否是先要获取这个区间的最高高度。
若是咱们提早知道最有边界是多少,下一个方块的左边界要是比最有边界大的话,是否是直接获取0了,以下面这样的:
因此,咱们能够把方块定义成{l,r,h,maxR},其中maxR表示目前最优边界。这样下一个节点降落的时候直接跟根节点的maxR比较,若是下一节点的左边界要大于等于maxR的话,能够直接得到这个区间的高度为 0。
最后,按照这个思路使用Java编写逻辑,执行用时也完胜100%的用户:
执行用时 : 6 ms , 在全部 Java 提交中击败了 100.00% 的用户 内存消耗 : 41.2 MB , 在全部 Java 提交中击败了 25.00% 的用户
而使用Go语言也同样。
执行用时 : 12 ms , 在全部 Go 提交中击败了 100.00% 的用户 内存消耗 : 5.6 MB , 在全部 Go 提交中击败了 100.00% 的用户
从执行结果上看,Go语言执行用时比Java耗时一点,可是内存消耗却比Java要少不少。
import java.util.*; class Solution { // 描述方块以及高度 private class Node { int l, r, h; Node left, right; public Node(int l, int r, int h) { this.l = l; this.r = r; this.h = h; this.left = null; this.right = null; } } // 线段树 public List<Integer> fallingSquares(int[][] positions) { // 建立返回值 List<Integer> res = new ArrayList<>(); // 根节点,默认为零 Node root = null; // 目前最高的高度 int maxH = 0; for (int[] position : positions) { int l = position[0]; // 左横坐标 int r = position[0] + position[1]; // 右横坐标 int e = position[1]; // 边长 int curH = query(root, l, r); // 目前区间的最高的高度 root = insert(root, l, r, curH + e); maxH = Math.max(maxH, curH + e); res.add(maxH); } return res; } private Node insert(Node root, int l, int r, int h) { if (root == null) return new Node(l, r, h); if (l <= root.l) root.left = insert(root.left, l, r, h); else root.right = insert(root.right, l, r, h); return root; // 返回根节点 } private int query(Node root, int l, int r) { if (root == null) return 0; // 高度 int curH = 0; if (!(r <= root.l || root.r <= l)) // 是否跟这个节点相交 curH = root.h; // 未剪枝 curH = Math.max(curH, query(root.left, l, r)); curH = Math.max(curH, query(root.right, l, r)); return curH; } }
执行用时 : 48 ms , 在全部 Java 提交中击败了 20.59% 的用户 内存消耗 : 40.9 MB , 在全部 Java 提交中击败了 25.00% 的用户
import java.util.*; class Solution { // 描述方块以及高度 private class Node { int l, r, h; Node left, right; public Node(int l, int r, int h) { this.l = l; this.r = r; this.h = h; this.left = null; this.right = null; } } // public List<Integer> fallingSquares(int[][] positions) { // 建立返回值 List<Integer> res = new ArrayList<>(); // 根节点,默认为零 Node root = null; // 目前最高的高度 int maxH = 0; for (int[] position : positions) { int l = position[0]; // 左横坐标 int r = position[0] + position[1]; // 右横坐标 int e = position[1]; // 边长 int curH = query(root, l, r); // 目前区间的最高的高度 root = insert(root, l, r, curH + e); maxH = Math.max(maxH, curH + e); res.add(maxH); } return res; } private Node insert(Node root, int l, int r, int h) { if (root == null) return new Node(l, r, h); if (l <= root.l) root.left = insert(root.left, l, r, h); else root.right = insert(root.right, l, r, h); return root; // 返回根节点 } private int query(Node root, int l, int r) { if (root == null) return 0; // 高度 int curH = 0; if (!(r <= root.l || root.r <= l)) // 是否跟这个节点相交 curH = root.h; // 剪枝 curH = Math.max(curH, query(root.left, l, r)); if (r > root.l) curH = Math.max(curH, query(root.right, l, r)); return curH; } }
执行用时 : 24 ms , 在全部 Java 提交中击败了 91.18% 的用户 内存消耗 : 41.1 MB , 在全部 Java 提交中击败了 25.00% 的用户
剪枝后提高了百分之56%多,进步蛮明显的。
class Solution { // 描述方块以及高度 private class Node { int l, r, h, maxR; Node left, right; public Node(int l, int r, int h, int maxR) { this.l = l; this.r = r; this.h = h; this.maxR = maxR; this.left = null; this.right = null; } } public List<Integer> fallingSquares(int[][] positions) { // 建立返回值 List<Integer> res = new ArrayList<>(); // 根节点,默认为零 Node root = null; // 目前最高的高度 int maxH = 0; for (int[] position : positions) { int l = position[0]; // 左横坐标 int r = position[0] + position[1]; // 右横坐标 int e = position[1]; // 边长 int curH = query(root, l, r); // 目前区间的最高的高度 root = insert(root, l, r, curH + e); maxH = Math.max(maxH, curH + e); res.add(maxH); } return res; } private Node insert(Node root, int l, int r, int h) { if (root == null) return new Node(l, r, h, r); if (l <= root.l) root.left = insert(root.left, l, r, h); else root.right = insert(root.right, l, r, h); // 最终目标是仅仅须要根节点更新 maxR root.maxR = Math.max(r, root.maxR); return root; // 返回根节点 } private int query(Node root, int l, int r) { // 新节点的左边界大于等于目前的maxR的话,直接获得0,不须要遍历了 if (root == null || l >= root.maxR) return 0; // 高度 int curH = 0; if (!(r <= root.l || root.r <= l)) // 是否跟这个节点相交 curH = root.h; // 剪枝 curH = Math.max(curH, query(root.left, l, r)); if (r > root.l) curH = Math.max(curH, query(root.right, l, r)); return curH; } }
执行用时 : 6 ms , 在全部 Java 提交中击败了 100.00% 的用户 内存消耗 : 41.2 MB , 在全部 Java 提交中击败了 25.00% 的用户
import ( "fmt" ) // 定义方块的结构体 type Node struct { l, r, h, maxR int left, right *Node // 指针类型,难难难(大学没学好C语言的后果,一不当心bu会用) } func fallingSquares(positions [][]int) []int { // 建立返回值 使用切片 (动态数组) var res = make([]int, 0) // 根节点 var root *Node = new(Node) // 初始化,对应类型的零值 // 目前最高的高度 maxH := 0 for _, position := range positions { l := position[0] // 左横坐标 r := position[0] + position[1] // 右横坐标 e := position[1] // 边长 curH := query(root, l, r) // 目前区间的最高的高度 root = insert(root, l, r, curH+e) maxH = max(maxH, curH+e) res = append(res, maxH) } return res } func insert(root *Node, l int, r int, h int) *Node { if root == nil { return &Node{ l: l, r: r, h: h, maxR: r, } } if l <= root.l { root.left = insert(root.left, l, r, h) } else { root.right = insert(root.right, l, r, h) } root.maxR = max(r, root.maxR) return root } func query(root *Node, l int, r int) int { // reflect.ValueOf(root).IsValid() 表示判断root是否为空 // 新节点的左边界大于等于目前的maxR的话,直接获得0,不须要遍历了 if root == nil || l >= root.maxR { return 0 } // 高度 curH := 0 if !(r <= root.l || root.r <= l) { // 是否跟这个节点相交 curH = root.h } // 剪枝 curH = max(curH, query(root.left, l, r)) if r >= root.l { curH = max(curH, query(root.right, l, r)) } return curH } func max(l, r int) int { if l > r { return l } return r }
执行用时 : 12 ms , 在全部 Go 提交中击败了 100.00% 的用户 内存消耗 : 5.6 MB , 在全部 Go 提交中击败了 100.00% 的用户