個人對主席樹演算法的理解
首先借主席樹發明人的一段話:
..這個東西是當初我弱不會劃分樹的時候寫出來替代的一個玩意..被一小撮別有用心的人取了很奇怪的名字> <
想法是對原序列的每一個字首[1..i]建立出一顆線段樹維護值域上每個數的出現次數,然後發現這樣的樹是可以減的,然後就沒有然後了
轉載請註明出處,謝謝。
首先定義:
a[MAXN],a2[MAXN]; struct node { node *ch[2]; int siz; node(){ch[0]=ch[1]=NULL;siz=0;} node(node *_ch0,node *_ch1,int _siz):siz(_siz){ch[0]=_ch0,ch[1]=_ch1;} void update() { if (ch[0]) siz+=ch[0]->siz; if (ch[1]) siz+=ch[1]->siz; } }*null=new node(),*root[MAXN]={NULL};
自己理解:
1:前提條件:有N個數字,size個不同的數字。
2:用a[N]儲存原來的數字,a2[size]儲存排好序的數字;
3:也就是每一個點root【i】都維護著size大小的線段樹,而該線段樹維護的資訊為:在a[1] 到a[i]數字集合中,size種數字分別出現的次數。
4:root[i] - > ch[0] 儲存的是排好序,前size/2種數字分別出現的次數,root[i] - > ch[1] 儲存的是排好序,後size/2種數字分別出現的次數;
值得注意的是,並不是每個root【i】都必須重新開闢size * log2(size)的空間,比如當新加入的a【i】非常小,那麼root[i] ->ch[1]後size/2種數字分別出現的次數
相對於root[i - 1] -> ch[1]是不會變。所以可以只用有孩子指標指向root[i - 1] -> ch[1]所指的節點就可以共用。當新加入的a【i】非常大的時候,那麼ch【0】就可以共用。
建樹的關鍵程式碼:
void make_node(node *&y,node *&x,int l,int r,int t) { if (x==NULL) x=null; y=new node(); int m=(l+r)>>1; if (l==r)//已經到達葉子節點, { *y=*x; y->siz++; return; } if (t<=a2[m]) { make_node(y->ch[0],x->ch[0],l,m,t); y->ch[1]=x->ch[1]; y->update(); } else { make_node(y->ch[1],x->ch[1],m+1,r,t); y->ch[0]=x->ch[0]; y->update(); } }
為了防止new爆T,那可以先用陣列存好。
一: 先分析空間複雜度:
每加入一個a【i】時,就會增加log2(size)個新節點,也就是每一層會產生一個新節點。所以空間複雜度為:N * log2(size)
二:當檢視一個一個區間中的 第K大值時
在第一層時:
1.先檢視這段區間在前size/2個的出現次數是否大於k,如果是,那麼第k個,肯定在前size/2中,否則在後size/2中,
那麼第k大,就得減去前size/2個出現的次數之和,變為在後size/2種數字中求第(k - 前size/2個出現的次數之和)大
2. 而前size/2個的出現次數可以直接相減可得,比如要看區間L到R,前size/2種數字出現的次數之和,
等於(root[R] - > ch[0] -> size) - (root[L - 1] -> ch[0] - >size)
3.往下層走,只不過就是size變為了size/2,遞迴下去就可以了。
很容易看出查詢一次的時間複雜度為log2(size);
關鍵程式碼如下:
void find(node *&x1,node *&x2,int l,int r,int k)
{
if (x1==NULL) x1=null;
if (x2==NULL) x2=null;
if (l==r) {printf("%d\n",a2[l]);return;}
int m=(l+r)>>1;
int ls=0;
if (x2->ch[0]) ls+=x2->ch[0]->siz;
if (x1->ch[0]) ls-=x1->ch[0]->siz;
if (ls>=k) find(x1->ch[0],x2->ch[0],l,m,k);
else find(x1->ch[1],x2->ch[1],m+1,r,k-ls);
}
下面是網上轉載的主席樹完整程式碼: