可持久數據結構主要指的是我們可以查詢歷史版本的情況並支持插入,利用使用之前歷史版本的數據結構來減少對空間的消耗(能夠對歷史進行修改的是函數式)。
在這里只講下比較常用的可持久化線段樹和trie。
對於線段樹我們記錄每個節點的左右兒子,如果空間允許的話我們也可以記錄每個數代表的區間,對於打標簽操作我們則需要新建兩個節點表示新的歷史,比較常用的是用可持久化線段樹來維護權值,然后維護不同區間的權值分布情況,比較經典的例子就是無修改的區間K大值,我們以這個問題為例子來講解可持久化線段樹的操作。
首先如果我們只詢問區間[1,n]的k大值我們可以先將各個點的權值離散化,然后建立一顆權值樹表示權值的分布狀況,那么我們對於每個區間只需要判斷左兒子的節點數是否大於k,然后二分的去找就好了,那么對於區間的查詢我們則需要建立這個區間的權值樹,那么我們先建一顆空樹,表示什么都沒有插入的時候的區間的情況,然后再按照數列的順序不斷的插入這個值,因為權值樹滿足加減的性質(即區間[l,r]中x值的個數為區間[1,r]中x的個數減去區間[1,l-1]中x的個數),所以我們可以處理出所有區間為[1,i]的時候的權值樹,但是空間會消耗很大。那么我們考慮一次插入,這一次插入改變的值只為包含x的區間的這logn個節點,那么剩下的節點我們可以從上一個歷史版本繼承過來。
建空樹比較容易,設t[x].son[0/1]表示x節點的左右兒子,t[x].left,right表示x節點所表示的左右區間,t[x].cnt表示這個節點表示區間的權值個數。那么我們可以得到
void build(int &x,int l,int r) { if (!x) x=tot++; t[x].left=l; t[x].right=r; if (t[x].left==t[x].right) return ; int mid=t[x].left+t[x].right>>1; build(t[x].son[0],l,mid); build(t[x].son[1],mid+1,r); }
下面就是插入操作,表示我們按一個值為key的元素新建一顆線段樹並繼承歷史版本為rot的線段樹
void insert(int &x,int rot,int key) { if (!x) x=tot++; t[x].left=t[rot].left; t[x].right=t[rot].right; if (t[x].left==t[x].right) { t[x].cnt=t[rot].cnt+1; return ; } int mid=t[x].left+t[x].right>>1; if (key>mid) { t[x].son[0]=t[rot].son[0]; insert(t[x].son[1],t[rot].son[1],key); } else { t[x].son[1]=t[rot].son[1]; insert(t[x].son[0],t[rot].son[0],key); } t[x].cnt=t[rot].cnt+1; }
因為需要繼承歷史版本,所以這個節點的所有信息比如left,right都需要繼承,cnt則需要被更新。
那么現在我們有了n棵線段樹,用rot[i]分別表示區間[1,i]的權值樹的根節點(表示權值區間為[1,max_sum]的節點),那么對於詢問[l,r]我們就可以用rot[r]-rot[l-1]來確定表示區間為[l,r]的權值樹的個點的cnt值。
下面是幾道比較簡單的權值樹的題。
還有一些其他可持久化線段樹的用法,比如
下面介紹一下可持久化trie。
可持久化trie中最簡單的一類就是存儲二進制的二叉trie樹,由於xor運算為一個群,所以滿足之前加法滿足的性質,那么我們可以根據這個性質來維護區間[1,i]的xor值從而維護區間[l,r]的xor值,這樣我們就可以處理給定一個數列,詢問區間[l,r]中所有數與x的xor值最大值的問題了。
假設需要維護的數的大小在1<<31左右的時候,最開始我們就要建一顆深度為31的滿二叉樹,這樣的空間消耗肯定是不允許的,那么我們可以在開始的時候不建空樹,而是直接插入各個節點。