[轉載]笛卡爾樹 笛卡爾樹Cartesian Tree
這篇寫的太好了,看了好幾個碼的,都寫的不知所云,就這個思路最清晰
笛卡爾樹Cartesian Tree
前言
最近做題目,已經不止一次用到笛卡爾樹了。這種資料結構極為優秀,但是構造的細節很容易出錯。因此寫一篇文章做一個總結。
笛卡爾樹 Cartesian Tree
引入問題
有N條的長條狀的矩形,寬度都為1,第i條高度為Hi,相鄰的豎立在x軸上,求最大的子矩形面積。
約定
1 ≤ N ≤ 105
1 ≤ Hi ≤ 109
分析
我們只需要求出每條矩形最多可以向兩邊拓展的寬度,就可以算出以這個矩形高度為高的最大子矩形面積。最後我們求一個最大值即可。
下面我們還是回到之前的笛卡爾樹。
概念
笛卡爾樹的樹根是這一子樹中鍵值最小(或最大)的元素;且對於某個序列的笛卡爾樹,其任一子樹的中序遍歷恰好是對應了原序列中的一段連續區間。
性質
我們會發現笛卡爾樹同時滿足二叉樹搜尋和堆的性質:
- 中序遍歷即為原陣列序列
- 父節點的鍵值均小於(或大於)其左右子節點的鍵值
構造
我們可以利用單調棧線上性時間內對給定的陣列序列構造出其笛卡爾樹。
首先,由於笛卡爾樹的中序遍歷為原陣列序列,那麼我們設
Ti為當前序列中[1..i]區間的笛卡爾樹,
那麼,一定有:
第(i+1)個節點屬於Ti
那麼對於有已經構造好的Ti和第(i+1)個節點,我們只需要沿著Ti最右邊的路徑從下往上找,直到發現當前節點可以放的位置。
我們可以用一個單調棧來維護最右邊的這條路徑,當前根節點壓在棧底,對於當前節點,我們將它與棧頂比較,若棧頂不能做它的父節點,則將棧頂彈出。
由於每個點進棧和出棧至多一次,因此這個構造的複雜度為O(n)的。
參考程式碼
節點用結構體定義
(這裡我們用指標會方便許多)
struct Node { int index, val; // index表示原陣列的索引,val為當前節點的鍵值 Node *parent, *lefts, *rights; // 這三個指標分別指向父節點,左子節點,右子節點 Node(int id = 0, int v = 0, Node *l = NULL, Node *r = NULL) { index = id; val = v; lefts = l; rights = r; } };
樹建構函式
(這裡要注意的細節較多)
Node * build(int arr[], int size) { // 這裡構建一個根節點為最小值的笛卡爾樹 std::stack<Node * > S; // 儲存最右邊路徑的棧 Node *now, *next, *last; for (int i = 0; i < size; i++) { next = new Node(i, arr[i]); last = NULL; // last用來指向最後被彈出棧的元素(若有彈出),它的作用後面會寫到 while (!S.empty()) { if (S.top()->val < next->val) { // 若棧頂節點的鍵值比當前節點鍵值小了,那麼當前節點就做棧頂節點的右子節點 now = S.top(); if (now->rights) { // 而棧頂節點的原右子節點要變成當前節點的左子節點(由於前面一定與當前節點比較過了,棧頂節點右子樹的鍵值一定都比當前節點大) now->rights->parent = next; next->lefts = now->rights; } now->rights = next; next->parent = now; break; } last = S.top(); S.pop(); } if (S.empty() && last) { // 這裡為了特判一種可能出現的情況,就是當前節點把棧全部彈空了,就要把原先的根節點作為當前節點的左子節點 next->lefts = last; last->parent = next; } S.push(next); } while (!S.empty()) now = S.top(), S.pop(); return now; }
前言
最近做題目,已經不止一次用到笛卡爾樹了。這種資料結構極為優秀,但是構造的細節很容易出錯。因此寫一篇文章做一個總結。
笛卡爾樹 Cartesian Tree
引入問題
有N條的長條狀的矩形,寬度都為1,第i條高度為Hi,相鄰的豎立在x軸上,求最大的子矩形面積。
約定
1 ≤ N ≤ 105
1 ≤ Hi ≤ 109
分析
我們只需要求出每條矩形最多可以向兩邊拓展的寬度,就可以算出以這個矩形高度為高的最大子矩形面積。最後我們求一個最大值即可。
下面我們還是回到之前的笛卡爾樹。
概念
笛卡爾樹的樹根是這一子樹中鍵值最小(或最大)的元素;且對於某個序列的笛卡爾樹,其任一子樹的中序遍歷恰好是對應了原序列中的一段連續區間。
性質
我們會發現笛卡爾樹同時滿足二叉樹搜尋和堆的性質:
- 中序遍歷即為原陣列序列
- 父節點的鍵值均小於(或大於)其左右子節點的鍵值
構造
我們可以利用單調棧線上性時間內對給定的陣列序列構造出其笛卡爾樹。
首先,由於笛卡爾樹的中序遍歷為原陣列序列,那麼我們設
Ti為當前序列中[1..i]區間的笛卡爾樹,
那麼,一定有:
第(i+1)個節點屬於Ti最右邊的那一條路徑。
那麼對於有已經構造好的Ti和第(i+1)個節點,我們只需要沿著Ti最右邊的路徑從下往上找,直到發現當前節點可以放的位置。
我們可以用一個單調棧來維護最右邊的這條路徑,當前根節點壓在棧底,對於當前節點,我們將它與棧頂比較,若棧頂不能做它的父節點,則將棧頂彈出。
由於每個點進棧和出棧至多一次,因此這個構造的複雜度為O(n)的。
參考程式碼
節點用結構體定義
(這裡我們用指標會方便許多)
struct Node { int index, val; // index表示原陣列的索引,val為當前節點的鍵值 Node *parent, *lefts, *rights; // 這三個指標分別指向父節點,左子節點,右子節點 Node(int id = 0, int v = 0, Node *l = NULL, Node *r = NULL) { index = id; val = v; lefts = l; rights = r; } };
樹建構函式
(這裡要注意的細節較多)
Node * build(int arr[], int size) { // 這裡構建一個根節點為最小值的笛卡爾樹 std::stack<Node * > S; // 儲存最右邊路徑的棧 Node *now, *next, *last; for (int i = 0; i < size; i++) { next = new Node(i, arr[i]); last = NULL; // last用來指向最後被彈出棧的元素(若有彈出),它的作用後面會寫到 while (!S.empty()) { if (S.top()->val < next->val) { // 若棧頂節點的鍵值比當前節點鍵值小了,那麼當前節點就做棧頂節點的右子節點 now = S.top(); if (now->rights) { // 而棧頂節點的原右子節點要變成當前節點的左子節點(由於前面一定與當前節點比較過了,棧頂節點右子樹的鍵值一定都比當前節點大) now->rights->parent = next; next->lefts = now->rights; } now->rights = next; next->parent = now; break; } last = S.top(); S.pop(); } if (S.empty() && last) { // 這裡為了特判一種可能出現的情況,就是當前節點把棧全部彈空了,就要把原先的根節點作為當前節點的左子節點 next->lefts = last; last->parent = next; } S.push(next); } while (!S.empty()) now = S.top(), S.pop(); return now; }