近期決定把數據結構技能樹繼續開發下去,學習更深入層次的三項技能:可持久化線段樹、動態樹、樹鏈剖分。然後那天看了看動態樹,碎了,然後就去看可持久化線段樹了……
簡單地說,這玩意是一個可以保存修改的歷史版本的線段樹。其本質想法是:對線段樹每改一次,就建一棵新的樹,保留原來的版本。這聽起來挺瘋狂的,但是卻有很令人意想不到的用處:比如POJ 2104這個題,普通線段樹是無力的,但是如果你需要對付的區間永遠只有1~n一個的話,就會輕鬆搞定:對值建樹,然後記錄一下每個節點裏有幾個數,來詢問“第k大的數是幾?”的時候,我就看看我左子樹的數夠不夠k,如果夠k,說明這個數在左子樹裏,你去左子樹找吧;不然就肯定在右子樹裏,因爲左子樹已經佔了sum(假設sum是每個節點有的數的個數)個,所以只需要去右子樹找第k-sum個即可。時間複雜度是很好的n·logn。
可持久化線段樹厲害之處就是在於它可以利用跟前綴和差不多的思想,把所有區間詢問全都變成“從頭到尾”的,只要我們這麼考慮:一開始有一個空線段樹,然後把a[1]加進去,a[2]加進去…… 注意加的時候你不要直接改,而是【弄一棵新線段樹】,這樣我們就有了n+1棵線段樹,當詢問l~r的時候,只需要把 線段樹[r]所有節點的sum值 減去 線段樹[l-1]所有節點的sum值,我們就有了一棵建在[l,r]上的線段樹,就可以很快樂地搞了~
問題在於,真正去建n+1棵線段樹是不現實的,因爲這需要n²級別的內存量…… 但是我們不用真正去建這麼多。你會發現,每次改的時候,真正被修改的節點只有logn個(左右兒子最多隻改一個),所以我們可以在需要修改一個節點的時候,先把上一棵線段樹的這個節點抄過來,然後改這個節點,然後看看左右兒子哪個需要改,需要改的去抄然後改,不需要改的就這麼擱着就行。這個在代碼裏非常清楚。
目前只是這個東西的入門,級別還不夠,之後還會有更厲害的玩意吧……
附上自己的代碼
#include<iostream>
#include<stdio.h>
#include<string.h>
#include<math.h>
#include<algorithm>
using namespace std;
struct ntype
{
int l,r,sum;
} node[3000000]//注意開夠,本身需要3n個,每改一次又會出現logn個
int p_nodes=0;
int n,m;
int a[100010], root[100010];//root注意開夠,每改一次就意味着一棵新線段樹
int numbers[100010];//離散化用的
int p_numbers=0;
void build(int s, int e, int &p)
{
p=++p_nodes;
node[p].l=node[p].r=node[p].sum=0;
if(s>=e)
return;
int mid=(s+e)/2;
build(s,mid, node[p].l);
build(mid+1,e, node[p].r);
}
void update(int pre, int &p, int s, int e, int mir)
{
p=++p_nodes;
node[p]=node[pre];//把上一棵的信息抄過來
node[p].sum++;//改我的信息
if(s>=e)
return;
int mid=(s+e)/2;//看看哪個需要改,抄過來改,正好指針就更新了,不改的那個擱着不動
if(mir<=mid)
update(node[pre].l, node[p].l, s, mid, mir);
else
update(node[pre].r, node[p].r, mid+1, e, mir);
}
int ask(int r1, int r2, int s, int e, int k)
{
if(s>=e)
return s;
int mid=(s+e)/2;
int left_sum=node[ node[r2].l ].sum - node[ node[r1].l ].sum;
if(left_sum>=k)
{
int r=ask(node[r1].l, node[r2].l, s, mid, k);
return r;
}
else
{
int r=ask(node[r1].r, node[r2].r, mid+1, e, k-left_sum);
return r;
}
}
int main()
{
scanf("%d %d", &n, &m);
int i;
for(i=1;i<=n;i++)
{
scanf("%d", &a[i]);
numbers[i]=a[i];
}
sort(numbers+1, numbers+n+1);
p_numbers=unique(numbers+1, numbers+n+1) - numbers - 1;//一一映射的離散化用STL寫很簡潔
build(1,p_numbers, root[0]);
for(i=1;i<=n;i++)
{
int mirror=lower_bound(numbers+1, numbers+n+1, a[i]) - numbers;
update(root[i-1], root[i], 1, p_numbers, mirror);//抄上一棵的信息
}
for(i=1;i<=m;i++)
{
int s,e,k;
scanf("%d %d %d", &s,&e,&k);
int ans_mirror = ask(root[s-1], root[e], 1,p_numbers, k);//在沒有被實際建出來的“新線段樹”上詢問
printf("%d\n", numbers[ans_mirror]);
}
system("pause");
return 0;
}