今天分享一個LeetCode題,題號是699,標題是掉落的方塊,題目標簽是線段樹,題目難度是困難。
這篇文章寫著寫著,篇幅就變得有點長了,但是這對你很有幫助,因為我在寫Java代碼過程中進行了兩步優化,過程都寫下來了。
后面也會貼Go語言代碼,記得收哦,簡單對比了Java和Go語言的執行分析,對學習Go語言有好處的。
題目描述
在無限長的數軸(即 x 軸)上,我們根據給定的順序放置對應的正方形方塊。
第 i 個掉落的方塊(positions[i] = (left, side_length))是正方形,其中 left 表示該方塊最左邊的點位置(positions[i][0]),side_length 表示該方塊的邊長(positions[i][1])。
每個方塊的底部邊緣平行于數軸(即 x 軸),并且從一個比目前所有的落地方塊更高的高度掉落而下。在上一個方塊結束掉落,并保持靜止后,才開始掉落新方塊。
方塊的底邊具有非常大的粘性,并將保持固定在它們所接觸的任何長度表面上(無論是數軸還是其他方塊)。鄰接掉落的邊不會過早地粘合在一起,因為只有底邊才具有粘性。
返回一個堆疊高度列表 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:
輸入: [[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要少很多。
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% 的用戶
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));
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%多,進步蠻明顯的。
Java代碼最終優化
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% 的用戶
Go語言代碼,對應Java最終優化版本
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% 的用戶