可持久化數據結構
毒液哥 Fudan University
概論
本文假設讀者對數據結構有一定了解,對此概念不做贅述。
可持久化數據結構是一類數據結構的統稱。若我們能在某一時刻訪問一數據結構的任何歷史版本,則稱該數據結構爲可持久化數據結構。
若一個數據結構是可持久化的,則可以通過修改該數據結構,在各操作時間複雜度不改變的同時,使其成爲一個可持久化數據結構。
由定義可知,可持久化數據結構的任何一個歷史版本都要被保留,因此可持久化數據結構的任何操作都不能直接修改之前存在的節點,必須對每個需要修改的節點創建一個拷貝。因此只有當任何時間的任何操作創建節點拷貝的複雜度都嚴格小於等於其複雜度,這一數據結構才能可持久化。
下文會討論一些常見的數據結構的一般版本,及其可持久化版本,並分析一些不能可持久化的一般版本。
可持久化線段樹
一般線段樹
一般來說,線段樹有兩種實現方法,一種是基於數組的靜態開點的線段樹,另一種是使用指針儲存樹形結構的動態開點(也可靜態開點)版本。其中前者本質上是對後者的一種優化。在線段樹的座標範圍確定的情況下,整顆線段樹的節點數量以及每個節點表示的線段其實是確定的。若將該線段樹的結構完整建立,則它是一棵節點數不超過的正則二叉樹,其中爲座標範圍的長度。因此可以用數組儲存這棵二叉樹,從而省去了記錄左右兒子關係的空間。
因爲線段樹的每個操作會嚴格修改個節點,所以線段樹可持久化。但由於前者用數組存儲了整棵線段樹,但拷貝數組需要的時間,因此前一個版本的線段樹不可持久化,而後一個版本的線段樹可持久化。
可持久化
若表示一棵滿節點線段樹的一個節點,則:
用表示的左兒子;表示的右兒子;表示上儲存的數據;表示(線段)的左端點;表示(線段)的右端點。
考慮一般線段樹的單點遞歸修改過程。
遞歸函數的參數分別表示線段樹的節點、要修改的座標、以及數據的修改量,沒有返回值:
- 判斷是否是葉子節點,若是,直接修改並返回。
- 判斷位於還是. 不妨設是前者。
- 遞歸調用.
- 由和更新.
我們發現,所有被訪問到的節點的信息都會被修改,因此在可持久化版本中,我們應該對所有訪問到的節點製作一份副本,並對副本進行修改。同時需要注意,在第三步中,由於我們爲修改過的創建了一份副本,我們應該將新節點的左兒子指向修改過後的副本。下面給出可持久化線段樹的單點遞歸修改過程。
遞歸函數的參數分別表示線段樹的節點、要修改的座標、以及數據的修改量,返回值爲修改過後的的副本(即):
- 創建的一個拷貝,設其爲.
- 判斷是否是葉子節點,若是,直接修改並返回。
- 判斷位於還是. 不妨設是前者。
- 遞歸調用. 並令更新爲其返回值。
- 由和更新.
- 返回.
每次在外部調用函數都會返回一棵當前版本線段樹的根,並且不會對已有的節點進行任何的修改,即任何歷史版本的線段樹都是可以訪問的。修改操作的時間複雜度爲,和一般版本的線段樹一致。由於每次修改均要新建個節點,所以修改操作的空間複雜度爲嚴格,而一般線段樹修改操作的空間複雜度爲.
核心代碼如下:
Node* Insert(Node *u, int x, int y, int Left, int Right) // Left, Right分別表示L(u), R(u)
{
Node *v = NewNode(u); // 創建一個u的拷貝
if(Left == Right)
{
v -> Data += y; // 直接修改data(v)
return v;
}
int Mid = Left + Right >> 1;
if(x <= Mid) // 判斷x是否在left(u)中
v -> Child[0] = Insert(left(u), x, y, Left, Mid); // 遞歸調用,Child[0]表示左兒子
else
v -> Child[1] = Insert(right(u), x, y, Mid + 1, Right);
v -> Data += data(left(u)) + data(right(u)); // 更新data(v)
return v;
}
區間修改的可持久化和單點修改的可持久化類似,在此略述。
例題
區間K大值
問題描述
給定一個長度爲的序列和個詢問,每個詢問給出,問區間中第大的數是多少。
解法
不妨設元素的大小在中,我們將元素的大小作爲線段樹的下標;線段樹每個節點儲存大小在其區間內的數的個數。依次將序列中的每個元素插入到可持久化線段樹中,注意:將元素大小作爲下標,作爲修改量,則我們可以得到個歷史版本(一開始線段樹爲空),設爲. 設分別爲上表示某相同線段的節點,則表示中值位於範圍內的點的個數,表示中值位於範圍內的點的個數。
那麼,對於每個詢問,通過對和作差(本質上是在訪問節點時對和作差)輕鬆得到這些元素加入後的線段樹。
在線段樹上,我們可以輕鬆知道第大的節點在左兒子還是右兒子中。那麼樹邊從線段樹的根一直走到葉子節點,其表示的單點即爲答案。時空複雜度均爲. 注意:若要在中尋找第大的點,且發現其在中,則要在中尋找第大的點。
可持久化平衡樹
一般平衡樹
在算法競賽中,Splay Tree, 旋轉式Treap, 非旋轉式Treep, 替罪羊樹是幾種選手比較熟悉的平衡樹。
由於Splay Tree和替罪羊樹的複雜度是均攤的,因此對於某一次操作,其複雜度並不是嚴格爲,所以這兩種平衡樹是不可持久化的。
而Treep是可持久化的。由於旋轉式Treap需要記錄節點的父親信息,將其操作改爲可持久化版本會使修改的節點達到O(n)個,因此我們在此討論非旋轉式Treep的可持久化版本。
可持久化
若表示一棵Treap的一個節點,則:
用表示的左兒子;表示的右兒子;表示上儲存的值;表示的隨機權值;表示以爲根的子樹的大小。
非旋轉式Treap有兩個核心操作:
- 將爲根的兩棵有序的樹合併成一棵樹,返回這棵樹。
- 將以爲根的樹分裂爲兩棵有序的樹(即樹1中所有元素均不大於樹2中的任何元素),其中樹1的大小爲,返回這兩棵樹。
在執行操作時,因爲要維護Treap作爲堆(不妨設其爲大根堆)的性質,要對的隨機權值進行判斷,若,則將和合併作爲的右兒子;反之將和合併作爲的左兒子。由於Treap的期望高度爲,因此操作時間複雜度上限爲.
在執行操作時,若, 我們將與斷開後遞歸地執行, 並將返回的第二棵樹與合併作爲新的第二棵樹;反之我們將與斷開後遞歸地執行,並將與返回的第一棵樹合併作爲新的第一棵樹。最後返回得到的兩棵樹。注意:在邊界情況,即和時有一個返回值爲NULL
.
在操作中執行的操作,由於合併點的隨機權值的大小關係是已知的,帶入函數中可以發現其複雜度爲。由於Treap的期望高度爲,因此操作時間複雜度複雜度上限爲.
這兩個操作的可持久化版本和線段樹的可持久化處理方式完全類似,對於每個要修改的節點,都新建一個拷貝,對拷貝進行修改,已有的節點不做任何修改以保存歷史版本,同時空間複雜度從上升到.
核心代碼如下:
Node* Merge(Node *x, Node *y)
{
if(!x)
return y;
if(!y)
return x;
if(key(x) > key(y)) // 維護堆的性質
{
Node *New = NewNode(x); // 創建x的拷貝
New -> Child[1] = Merge(right(x), y); // 合併right(x)和y
New -> Update(); // 更新size(New)
return New;
}
else
{
Node *New = NewNode(y);
New -> Child[0] = Merge(x, left(y));
New -> Update();
return New;
}
}
pair<Node*, Node*> Split(Node *x, int k)
{
if(x -> Size == k) // 處理邊界情況
return make_pair(x, (Node*)NULL);
if(k == 0)
return make_pair((Node*)NULL, x);
Node *New = NewNode(x); // 創建x的拷貝
if(size(left(x)) >= k)
{
auto ret = Split(left(x), k); // 將left(x)分裂
New -> Child[0] = NULL; // 斷開與左子樹的連接
return make_pair(ret.first, Merge(ret.second, New)); // 將返回的第二棵樹與New合併
}
else
{
auto ret = Split(right(x), k - size(left(x)) - 1);
New -> Child[1] = NULL;
return make_pair(Merge(New, ret.first), ret.second);
}
}
區間K大值
問題描述
給定一個長度爲的序列和個詢問,每個詢問給出,問區間中第大的數是多少。
解法
不妨設元素的大小在中.
非旋轉式Treap中的所有操作基本上是基於和兩個操作的。要在其中插入一個權值爲的點,我們要先查詢樹中有多少個點權值小於(設爲操作), 再對原樹進行,之後將第一棵樹,新節點,第二棵樹這三棵樹合併後得到新的樹。
同可持久化線段樹的版本類似,依次將序列中的每個元素插入到可持久化線段樹中,我們可以得到個歷史版本(一開始平衡樹爲空),設爲. 由於不能像可持久化線段樹一樣對兩棵樹作差,我們無法直接在樹內二分得到答案。因此我們只能先二分答案,再判斷可行性。設目前二分的答案爲,我們先執行和操作再對返回值作差,從而得到中權值小於的數的個數,設爲, 二分查找到的最大的, 使得, 即爲答案。
時間複雜度爲, 空間複雜度爲.