今天分享一個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% 的用戶