可持久化線段樹總結(可持久化線段樹,線段樹)


最近正在學習一種數據結構——可持久化線段樹。看了網上的許多博客,弄了幾道模板題,思路有點亂了,所以還是來總結整理下吧。

可持久化線段樹

首先要了解此數據結構的基礎——線段樹。百度一下,你就知道!
推薦一下這篇博客,對線段樹的基本操作講得挺詳細的。
為了更好地理清思路,我在這里先放個模板題吧。
洛谷題目傳送門

題目描述

你需要維護這樣的一個長度為\(N\)的數組,支持如下幾種操作

  1. 在某個歷史版本上修改某一個位置上的值
  2. 訪問某個歷史版本上的某一位置的值

此外,每進行一次操作(對於操作2,即為生成一個完全一樣的版本,不作任何改動),就會生成一個新的版本。版本編號即為當前操作的編號(從1開始編號,版本0表示初始狀態數組)

輸入輸出格式

輸入格式:

輸入的第一行包含兩個正整數\(N,M\)分別表示數組的長度和操作的個數。
第二行包含 N N個整數,依次為初始狀態下數組各位的值(依次為\(a_i, 1 \leq i \leq N\))。
接下來\(M\)行每行包含3或4個整數,代表兩種操作之一(\(i\)為基於的歷史版本號):
對於操作1,格式為\(v_i \ 1 \ {loc}_i \ {value}_i v\),即為在版本\(v_i\)的基礎上,將\(a_{{loc}_i}\)修改為\({value}_i\)
對於操作2,格式為\(v_i \ 2 \ {loc}_i\),即訪問版本\(v_i\)中的\(a_{{loc}_i}\)的值

輸出格式:

輸出包含若干行,依次為每個操作2的結果。

輸入輸出樣例

輸入樣例#1:

5 10
59 46 14 87 41
0 2 1
0 1 1 14
0 1 1 57
0 1 1 88
4 2 4
0 2 5
0 2 4
4 2 1
2 2 2
1 1 5 91

輸出樣例#1:

59
87
41
87
88
46

說明

數據規模:

對於30%的數據:\(1 \leq N, M \leq {10}^3\)
對於50%的數據:\(1 \leq N, M \leq {10}^4\)
對於70%的數據:\(1 \leq N, M \leq {10}^5\)
對於100%的數據:\(1 \leq N, M \leq {10}^6, 1 \leq {loc}_i \leq N, 0 \leq v_i < i, -{10}^9 \leq a_i, {value}_i \leq {10}^9\)
經測試,正常常數的可持久化數組可以通過,請各位放心
數據略微凶殘,請注意常數不要過大
另,此題I/O量較大,如果實在TLE請注意I/O優化
詢問生成的版本是指你訪問的那個版本的復制

樣例說明:

一共11個版本,編號從0-10,依次為:
0 : 59 46 14 87 41
1 : 59 46 14 87 41
2 : 14 46 14 87 41
3 : 57 46 14 87 41
4 : 88 46 14 87 41
5 : 88 46 14 87 41
6 : 59 46 14 87 41
7 : 59 46 14 87 41
8 : 88 46 14 87 41
9 : 14 46 14 87 41
10 : 59 46 14 87 91

思路分析

很裸的可持久化線段樹板子題。可持久嘛!就是當出現歷史版本的時候,能夠非常方便地維護一個區間的歷史版本。
自然,我們需要建\(N\)棵線段樹。最粗暴的想法,對每個新版本都把原版本內容復制一遍,然后修改對應的值。這根本不用想,直接MLE+TLE。那維護歷史版本又是怎樣實現的呢?
對於本題,每個版本的序列,我們可以建一棵線段樹來維護它,所有非葉子節點表示的是一段區間,而葉子節點就表示序列的每一個值了。
舉個栗子,樣例中初始版本可以長這樣——

而版本1只是查詢了一下(線段樹基本操作,這里不再贅述),然后跟初始版本一模一樣。這就沒必要復制了嘛!我們設版本\(i\)有一個根節點\(root_i\)(表示整段區間),根節點有左右兒子,那么我們直接讓\(root_1\)的左右兒子指向\(root_0\)的左右兒子就好了,根本不用復制整個線段樹嘛!
那再來看看修改操作。比如從版本1~2。1和0是一樣的,而版本2會長這樣——

有沒有發現1和2真的很像?其實從前到后只改變了一個節點!那么其他相同的地方,我們可不可以共用一段內存呢?

沒錯,每次創建一個新的版本時,只要新建\(\log_2 n\)個節點,也就是只保存從新版本的根節點到更新的那一個葉子節點的路徑就可以了,不在此路徑上的左/右兒子只要接原版本對應區間的對應兒子就可以啦。我們可以保證,從對應版本的根節點一定能訪問到對應葉子節點的值。
下面是加入新版本的具體實現代碼(我寫的是非遞歸版):

#define R register int
inline void insert(R*t,R u,R l,R r,R k)
//t是當前節點指針,u是原版本對應t的節點,l、r為當前區間,k為修改點的位置
{
	R m;
	while(l!=r)
	{
		*t=++P;//為新節點分配空間,P是個外部變量
		m=(l+r)>>1;//線段樹操作,計算區間中點
		if(k<=m)r=m,rc[*t]=rc[u],t=&lc[*t],u=lc[u];
		else  l=m+1,lc[*t]=lc[u],t=&rc[*t],u=rc[u];
		//上面兩行很關鍵。(if一行)如果k在左子樹中,那么右子樹沒有變,直接連到舊版本的對應右子樹上,t、u更新為當前左子樹繼續。(else一行反之亦然)
	}
	in(val[*t=++P]);//讀入新葉子節點的值
}

整個程序的代碼如下

#include<cstdio>
#include<cstring>
#define R register int
const int N=1000009,M=20000009;
int P,rt[N],lc[M],rc[M],val[M];
char I[M<<1],O[M],*fi=I,*fo=O;
bool nega;
inline void in(R&z)
{
	while(*fi<'-')++fi;
	if(*fi=='-')nega=1,++fi;
	z=*fi++&15;
	while(*fi>'-')z*=10,z+=*fi++&15;
	if(nega)nega=0,z=-z;
}
void oi(R z)
{
    if(z>9)oi(z/10);
    *fo++=z%10|'0';
}
inline void out(R z)
{
    z>0?oi(z):(*fo++='-',oi(-z));*fo++='\n';
}//上面快讀快寫
void build(R&t,R l,R r)//初始化建樹,線段樹基本操作
{
	R m;
	t=++P;
	if(l!=r)
	{
		m=(l+r)>>1;
		build(lc[t],l,m);
		build(rc[t],m+1,r);
	}
	else in(val[P]);
}
inline void insert(R*t,R u,R l,R r,R k)//更新,插入一個新路徑
{
	R m;
	while(l!=r)
	{
		*t=++P;
		m=(l+r)>>1;
		if(k<=m)r=m,rc[*t]=rc[u],t=&lc[*t],u=lc[u];
		else  l=m+1,lc[*t]=lc[u],t=&rc[*t],u=rc[u];
	}
	in(val[*t=++P]);
}
inline int ask(R t,R l,R r,R k)//詢問
{
	R m;
	while(l!=r)
	{
		m=(l+r)>>1;
		if(k<=m)r=m,t=lc[t];
		else  l=m+1,t=rc[t];
	}
	return val[t];
}
int main()
{
	freopen("ct.in","r",stdin);freopen("ct.out","w",stdout);
	fread(I,1,sizeof(I),stdin);
	R n,m,i,v,op,loc;
	in(n);in(m);
	build(rt[0],1,n);
	for(i=1;i<=m;++i)
	{
		in(v);in(op);in(loc);
		if(op&1)insert(&rt[i],rt[v],1,n,loc);
		else
		{
			out(ask(rt[v],1,n,loc));
			rt[i]=++P;//沒錯,這里的版本復制其實很簡單
			lc[P]=lc[rt[v]];
			rc[P]=rc[rt[v]];
		}
	}
	fwrite(O,1,fo-O,stdout);
	fclose(stdin);fclose(stdout);
	return 0;
}


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM