可持久化資料結構(persistent data structure)就是利用函式式程式設計的思想使其支援詢問歷史版本、同時充分利用它們之間的共同資料來減少時間和空間消耗。
因此可持久化線段樹也叫函式式線段樹又叫主席樹。
在演算法執行的過程中,會發現在更新乙個動態集合時,需要維護其過去的版本。這樣的集合稱為是可持久的。
實現持久集合的一種方法時每當該集合被修改時,就將其整個的複製下來,但是這種方法會降低執行速度並占用過多的空間。
考慮乙個持久集合s。
如圖所示,對集合的每乙個版本維護乙個單獨的根,在修改資料時,只複製樹的一部分。
稱之為可持久化資料結構。
令 t 表示乙個結點,它的左兒子是 left(t),右兒子是 right(t)。
若 t 的範圍是 [l,r],那麼 left(t) 的範圍是 [l,mid],right(t) 的範圍是 [mid+1,r]。
我們要修改乙個葉子結點的值,並且不能影響舊版本的結構。
在從根結點遞迴向下尋找目標結點時,將路徑上經過的結點都複製乙份。
找到目標結點後,我們新建乙個新的葉子結點,使它的值為修改後的版本,並將它的位址返回。
對於乙個非葉子結點,它至多只有乙個子結點會被修改,那麼我們對將要被修改的子結點呼叫修改函式,那麼就得到了它修改後的兒子。
在每一步都向上返回當前結點的位址,使父結點能夠接收到修改後的子結點。
在這個過程中,只有對新建的結點的操作,沒有對舊版本的資料進行修改。
從要查詢的版本的根節點開始,像查詢普通的線段樹那樣查詢即可。
有n個數,多次詢問乙個區間[l,r]中第k小的值是多少。
我們先對資料進行離散化,然後按值域建立線段樹,線段樹中維護某個值域中的元素個數。
那麼要尋找第k小值,從根結點開始處理,若左兒子中表示的元素個數大於等於k,那麼我們遞迴的處理左兒子,尋找左兒子中第k小的數;
若左兒子中的元素個數小於k,那麼第k小的數在右兒子中,我們尋找右兒子中第k-(左兒子中的元素數)小的數。
我們按照從1到n的順序依次將資料插入可持久化的線段樹中,將會得到n+1個版本的線段樹(包括初始化的版本),將其編號為0~n。
可以發現所有版本的線段樹都擁有相同的結構,它們同乙個位置上的結點的含義都相同。
考慮第i個版本的線段樹的結點p,p中儲存的值表示[1,i]這個區間中,p結點的值域中所含的元素個數;
假設我們知道了[1,r]區間中p結點的值域中所含的元素個數,也知道[1,l-1]區間中p結點的值域中所包含的元素個數,顯然用第乙個個數減去第二個個數,就可以得到[l,r]區間中的元素個數。
因此我們對於乙個查詢[l,r],同步考慮兩個根root[l-1]與root[r],用它們同乙個位置的結點的差值就表示了區間[l,r]中的元素個數,利用這個性質,從兩個根節點,向左右兒子中遞迴的查詢第k小數即可。
注意可持久化資料結構的記憶體開銷非常大,因此要注意盡可能的減少不必要的空間開支。
const int maxn=100001;
struct nodetr[maxn*20];
int cur,rt[maxn];
void init()
inline void push_up(int o)
int build(int l,int r)
int mid=(l+r)>>1;
tr[k].ls=build(l,mid);
tr[k].rs=build(mid+1,r);
push_up(k);
return k;
}int update(int o,int l,int r,int pos,int val)
int mid=(l+r)>>1;
if (pos<=mid) tr[k].ls=update(tr[o].ls,l,mid,pos,val);
else tr[k].rs=update(tr[o].rs,mid+1,r,pos,val);
push_up(k);
return k;
}int query(int l,int r,int o,int v,int kth)
一種在常數上減小記憶體消耗的方法:
插入值時候先不要一次新建到底,能留住就留住,等到需要訪問子節點時候再建下去。
這樣理論記憶體複雜度依然是o(nlg^2n),但因為實際上很多結點在查詢時候根本沒用到,所以記憶體能少用一些。
每一棵線段樹是維護每乙個序列字首的值在任意區間的個數,如果還是按照靜態的來做的話,那麼每一次修改都要遍歷o(n)棵樹,時間就是o(2*m*nlogn)->tle。
考慮到字首和,我們通過樹狀陣列來優化,即樹狀陣列套主席樹,每個節點都對應一棵主席樹,那麼修改操作就只要修改logn棵樹,o(nlognlogn+mlognlogn)時間是可以的,但是直接建樹要nlogn*logn(10^7)會mle。
我們發現對於靜態的建樹我們只要nlogn個節點就可以了,而且對於修改操作,只是修改m次,每次改變倆個值(減去原先的,加上現在的)也就是說如果把所有初值都插入到樹狀陣列裡是不值得的,所以我們分兩部分來做,所有初值按照靜態來建,記憶體o(nlogn),而修改部分儲存在樹狀陣列中,每次修改logn棵樹,每次插入增加logn個節點o(m*logn*logn+nlogn)。
入門題,求區間第k小數。
#include #include #include #include using namespace std;
const int maxn=100001;
struct nodetr[maxn*20];
int cur,rt[maxn];
void init()
inline void push_up(int o)
int build(int l,int r)
int mid=(l+r)>>1;
tr[k].ls=build(l,mid);
tr[k].rs=build(mid+1,r);
push_up(k);
return k;
}int update(int o,int l,int r,int pos,int val)
int mid=(l+r)>>1;
if (pos<=mid) tr[k].ls=update(tr[o].ls,l,mid,pos,val);
else tr[k].rs=update(tr[o].rs,mid+1,r,pos,val);
push_up(k);
return k;
}int query(int l,int r,int o,int v,int kth)
int b[maxn];
int sortb[maxn];
int main()
sort(sortb+1,sortb+1+n);
int cnt=1;
for (int i=2;i<=n;i++)
}rt[0]=build(1,cnt);
for (int i=1;i<=n;i++)
for (int i=0;i
求區間內不重複的數的個數。
#include #include #include #include #include using namespace std;
const int maxn=100001;
struct nodetr[maxn*20];
int cur,rt[maxn];
void init()
inline void push_up(int o)
int build(int l,int r)
int mid=(l+r)>>1;
tr[k].ls=build(l,mid);
tr[k].rs=build(mid+1,r);
push_up(k);
return k;
}int update(int o,int l,int r,int pos,int val)
int mid=(l+r)>>1;
if (pos<=mid) tr[k].ls=update(tr[o].ls,l,mid,pos,val);
else tr[k].rs=update(tr[o].rs,mid+1,r,pos,val);
push_up(k);
return k;
}int query(int l,int r,int o,int pos)
int b[maxn];
mapmp;
int main()
rt[0]=build(1,n);
for (int i=1;i<=n;i++)
else
mp[b[i]]=i;
}scanf("%d",&m);
for (int i=0;i
bzu 1901 (zoj 2112)
bzu 2653
hdu 4417
hdu 4348
線段樹,可持久化線段樹,主席樹
線段樹板子題 另乙個板子 線段樹就是對於給定的乙個序列,我們按照不斷取中點的方式建立一顆二叉樹,該樹的每個節點都代表乙個區間,也就是序列的下標,這樣我們就可以借助這些下標來維護某些我們需要的值,例如 某個區間的最大值,某個區間的和等等。同理也可以像差分那樣對某個區間進行操作。可持久化線段樹板子 主席...
主席樹(可持久化線段樹)
我真弱。連主席樹都不會。主席樹相當於多個線段樹,由於相鄰兩棵線段樹的節點的值只有少許不同,因此可以對於和前一棵樹一樣的子樹乙個指標指過去,無需操作,這樣每棵樹o logn 總複雜度o nlogn 以下是區間k大 include include include define n 100005 defi...
主席樹 可持久化線段樹
首先要學會普通的線段樹,然後理解權值線段樹,而主席樹就是多個權值線段樹 我自己的理解 但是這多個權值線段樹之間有公共部分,節約了空間。它一開始是乙個空樹,後來逐個添數,記錄新增的這個數在那個範圍內,並 1,顯然它每次只更新了一條鏈,其他不需要變,這樣就有了多個版本的線段樹。如果求 l,r 範圍內第k...