title : 可持久化線段樹
date : 2021-8-18
tags : 資料結構,ACM

可持久化線段樹

可以用來解決線段樹儲存歷史狀態的問題。

我們在進行單點修改後,線段樹只有logn個(一條鏈)的節點被修改,我們可以讓修改後的樹與修改前的樹共享節點,節省時間和空間。

在學習之前,我們先引入三個前置知識:離散化、動態開點,權值線段樹。

離散化

對於較大的資料範圍,只要將關鍵點記錄下來,記錄下rank,就能把資料縮小到可以接受的範圍,以便建立線段樹或其他資料結構來解決問題。

具體步驟

(1)將所有端點加入輔助陣列; (2)按座標從小到大排序; (3)去重; (4)資料離散化,用hs陣列記錄端點的排名而非具體數字

vector<int>vt;
for(int i=1;i<=n;i++){
   cin>>a[i];
   vt.push(a[i]); //加入輔助容器
}
sort(vt.begin(),vt.end());//排序
vt.erase(unique(vt.begin(),vt.end()),vt.end()); //去重
for(int i=0;i<vt.size();i++){
   hs[vt[i]]=++tot; //儲存為rank
}

動態開點

動態開點線段樹可以避免離散化。

如果權值線段樹的值域較大,離散化比較麻煩,可以用動態開點的技巧。

省略了建樹的步驟,而是在具體操作中加入結點。

權值線段樹

線段樹的葉子節點儲存的是當前值的個數。

每個節點儲存區間左右端點以及所在區間節點的個數。

由於值域範圍通常較大,一般會配合離散化或動態開點等策略優化空間。

應用

查詢一個區間的第k大的值

查詢某個數的排名

查詢整個陣列的排序

查詢前驅和後繼

單點修改
void update(int node,int start,int end,int pos){
   if(start==end) tr[node]++;
   else{
       int mid=start+end>>1;
       if(pos<=mid) update(node<<1,start,mid,pos);
       else update(node<<1|1,mid+1,end,pos);
  }
}//tr[i]表示值為i的元素個數,pos是要查詢的位置
查詢區間中的數出現次數
int query(int node,int start,int end,int ql,int qr){
   if(start==ql&&end==qr) return tr[node];
   int mid=start+end>>1;
   if(qr<=mid) return query(node<<1,start,mid,ql,qr);
   else if(ql>mid) return query(node<<1|1,mid+1,end,ql,qr);
   else return query(node<<1,start,mid,ql,qr)+query(node<<1|1,mid+1,end,ql,qr);
}//對單點查詢同樣適用
查詢所有數的第k大值
int kth(int node,int start,int end,int k){
   if(start==end) return start;
   int mid=start+end>>1;
   int s1=tr[node<<1],s2=tree[node<<1|1];
   if(k<=s2) return kth(node<<2|1,mid+1,end,k);
   else return kth(node<<1,start,mid,k-s2);
} //注意是第k大,從右邊開始減,如果是第k小就減去左邊
查詢前驅(後繼同)
int findpre(int node,int start,int end){ //找這個區間目前最大的
   if(start==end) return start; //找到直接返回
   int mid=start+end>>1;
   if(t[node<<1|1]) return findpre(node<<1|1,mid+1,end);
   return findpre(node<<1,start,mid);
}

int pre(int node,int l,int r,int pos){ //求pos的前驅
   if(r<pos){ //在最右邊
       if(t[node]) return findpre(node,l,r);
       return 0;
  }
   int mid=l+r>>1,res;
   if(mid+1<pos&&t[node<<1|1]&& (res=pre(node<<1|1,mid+1,r,pos))) return res; //在右區間尋找
   return pre(node<<1,l,mid,pos);  //在左區間尋找
}

可持久化線段樹

複雜度分析

建樹O(nlogn)

詢問為O(logn)

空間複雜度O(nlognlogn)。

核心思想

以字首和形式建立,基於動態開點的儲存形式。

兩棵線段樹之間是可減的(每一個節點對應相減)。

儲存

hjt[0]充當NULL,從1開始儲存根節點

struct Node{
   int lc,rc,sum; //左兒子右兒子和值sum
}hjt[maxn*32] //空間一般開到32即可
int cnt,root[maxn];//記憶體池計數器和根節點編號
插入

並不改變原來的樹,而是新開根節點,並向下開闢

void insert(inr cur,int pre.int pos,int l,int r){
   if(l==r){ //找到這個點,當前版本點值+1
       t[cur].v=t[pre].v+1;
       return;
  }
   int mid=l+r>>1;
   if(pos<=mid){
       t[cur].lc=++e;
       t[cur].rc=t[pre].rc;
       insert(t[cur].lc,t[pre].lc,pos,l,mid);
  }else{
       t[cur].rc=++e;
       t[cur].lc=t[pre].lc;
       insert(t[cur].rc,t[pre].rc,pos,mid+1,r);
  }
   t[cur].v=t[t[cur].lc].v+t[t[cur].rc].v; //pushup
}
詢問操作

本質上與權值線段樹相同,只需要在區間上作差。

int query(int l,int r,int x,int y,int k){
if (l == r){
return l;
  }
int mid=(l+r)/2;
int sum=tr[tr[y].l].sum-tr[tr[x].l].sum;
if(sum >= k){
return query(l,mid,tr[x].l,tr[y].l,k);
}
else{
       return query(mid+1,r,tr[x].r,tr[y].r,k-sum);
}
}
區間第K小

luoguP3834 【模板】可持久化線段樹 2(主席樹)

#include <bits/stdc++.h>

using namespace std;

typedef long long ll;
const int maxn = 2e5 + 7;

int n, m, cnt, rt[maxn], a[maxn], x, y, k;
//a[i]是原始的序列,rt[i]是第i版本的主席樹

struct node
{
int l, r, sum; //樹的左端、右端和元素個數
} tr[maxn * 32];

vector<int> vt; //輔助容器

void read(int &data)  //快讀優化
{
int x = 0, f = 1;
char ch = getchar();
while (ch < '0' || ch > '9')
{
if (ch == '-')
{
f = f * -1;
}
ch = getchar();
}
while (ch >= '0' && ch <= '9')
{
x = x * 10 + ch - '0';
ch = getchar();
}
data = x * f;
}

int getid(int x)  //返回每個元素的rank
{
return lower_bound(vt.begin(), vt.end(), x) - vt.begin() + 1;
}

void insert(int l, int r, int &x, int y, int pos)
{ //每次修改或插入,新建一個根節點,並向下遞迴新建節點
tr[++cnt] = tr[y];
tr[cnt].sum++;  //區域元素數量+1
x = cnt; //將原來的樹地址指向當前樹
if (l == r)
{ //已經是葉子了,返回
return;
}
int mid = (l + r) / 2;
if (mid >= pos)
{  //向左子樹插入
insert(l, mid, tr[x].l, tr[y].l, pos);
}
else
{  //向右子樹插入
insert(mid + 1, r, tr[x].r, tr[y].r, pos);  
}
}

int query(int l, int r, int x, int y, int k)
{ //l,r表示當前區間,x,y表示舊樹和新樹,k要在該區間找k小
if (l == r)
{
return l;    //找到則直接返回第k小的值l
}
int mid = (l + r) / 2;
int sum = tr[tr[y].l].sum - tr[tr[x].l].sum;
//左子樹相減,其差值為兩個版本之間左邊相差多少個數
if (sum >= k)
{  //結果大於等於k,詢問左子樹
return query(l, mid, tr[x].l, tr[y].l, k);
}
else
{  //否則找右子樹第k-sum小的數
return query(mid + 1, r, tr[x].r, tr[y].r, k - sum);
}
}

signed main()
{
read(n);
read(m);
for (int i = 1; i <= n; i++)
{
read(a[i]);
vt.push_back(a[i]); //輔助容器用於離散化
}
sort(vt.begin(), vt.end()); //排序
vt.erase(unique(vt.begin(), vt.end()), vt.end()); //去重
for (int i = 1; i <= n; i++)
{  //每次插入都從根節點開始建立新樹(形成一條鏈)
insert(1, n, rt[i], rt[i - 1], getid(a[i]));
}

for (int i = 1; i <= m; i++)
{//第y棵數和第x-1棵數做差,就能查出[x,y]區間第k小值
read(x);
read(y);
read(k);
printf("%d\n", vt[query(1, n, rt[x - 1], rt[y], k) - 1]);

}
return 0;
}

參考資料

https://www.cnblogs.com/young-children/p/11787490.html

https://www.cnblogs.com/young-children/p/11787493.html

https://blog.csdn.net/ModestCoder_/article/details/90107874

https://blog.csdn.net/a1351937368/article/details/78884465