【動態主席樹】ZOJ 2112【樹狀陣列+主席樹】
題意:
給定一個區間,求這個區間第k小的數,支援單點修改。
思路:
動態主席樹裸題。
我們先來回顧一下靜態主席樹的做法,對於陣列中每一個位置都維護一棵權值線段樹,該權值線段樹儲存的是區間 [1,x] 的資訊。因此我想要求區間 [l,r] 之間第k大的時候,只需要將root[r]-root[l-1]就是維護區間 [l,r] 資訊的權值線段樹,因此就可以快速直接求出這個區間中第k大的元素是多少。
現在我們來看看單點修改的操作。
如果我現在要將a[pos]修改為x,那麼最暴力的做法就是對於root[pos]~root[n]中的每一顆權值線段樹都進行修改,即將a[pos]這個點的值減1,將x這個點的值+1。
最暴力的做法顯然是無法通過此題的,因此我們可以想到有沒有一種logn的方法,可以只修改logn個節點,就可以對於每一個線段樹記錄修改資訊,於是我們想到了樹狀陣列。
我們來回憶一下樹狀陣列,每一個節點記錄區間 [x-lowbit(x)+1, x] 的所有資訊,因此當需要求[1,x]內維護的資訊的時候,只需要從節點x出發,每次進行 x-=lowbit(x) 的操作,即可求出[1,x]內維護的所有資訊。
每次對x節點進行修改的時候,只需要不斷進行x+=lowbit(x)的操作,就可以訪問到所有儲存x節點資訊的節點,因此實現了logn的查詢。
因此本題也維護一個樹狀陣列。
節點x維護的是區間 [x-lowbit(x)+1, x] 的權值線段樹,因此當我需要訪問 [l, r] 區間資訊的時候,只需要將root[r]-root[l-1]+getsum(r)-getsum(l-1)這裡面維護的便是區間 [l, r] 的權值線段樹。
總結:
樹狀陣列是一種很優秀的思想,他令每一個節點維護一個區間內的資訊,於是在空間複雜度為n的情況下實現了logn的查詢和修改操作,非常偉大!
程式碼:
#include <cstdio>
#include <iostream>
#include <cstring>
#include <algorithm>
#define rep(i,a,b) for(int i = a; i <= b; i++)
using namespace std;
const int N = 60010;
int n,m,tot,cnt,idx; //tot記錄節點數 cnt記錄離散化陣列
int a[N],dis[N]; //原陣列 離散化陣列
int root[N],lc[N*40],rc[N*40],sum[N*40]; //root為靜態主席樹的根節點
int s[N],use[N];
//s為動態主席樹的根節點,use是用來儲存更新時,沿著lowbit上升時經過的樹
//s[i]代表一棵權值線段樹,這顆權值線段樹統計的是[i-lowbit(i)+1,i]的區間修改資訊
struct Node{
int kind;
int l,r,k;
}que[10010];
int build(int l,int r)
{
int rt = ++tot;
sum[rt] = 0;
if(l != r)
{
int mid = (l+r)>>1;
lc[rt] = build(l,mid);
rc[rt] = build(mid+1,r);
}
return rt;
}
//以last這棵樹為參照,新建一棵樹,即新建的樹有一部分節點共用last這棵樹
int update(int last,int pos,int val)
{
int rt = ++tot, tmp = rt;
int l = 1, r = cnt;
sum[rt] = sum[last]+val;
while(l < r)
{
int mid = (l+r)>>1;
if(pos <= mid)
{
lc[rt] = ++tot, rc[rt] = rc[last]; //對左右兒子賦值
rt = lc[rt], last = lc[last]; //進入左兒子部分進行更新
r = mid;
}
else
{
rc[rt] = ++tot, lc[rt] = lc[last];
rt = rc[rt], last = rc[last];
l = mid+1;
}
sum[rt] = sum[last]+val;
}
return tmp;
}
int lowbit(int x)
{
return x&(-x);
}
void add(int x,int pos,int val)
{
while(x <= n)
{
s[x] = update(s[x],pos,val);
x += lowbit(x);
}
}
int getSum(int x)
{
int ret = 0;
while(x > 0)
{
ret += sum[lc[use[x]]];
x -= lowbit(x);
}
return ret;
}
int query(int left,int right,int k)
{
int left_rt = root[left-1];
int right_rt = root[right];
int l = 1, r = cnt;
for(int i = left-1; i ; i-= lowbit(i)) use[i] = s[i];//使用use陣列的目的是將樹狀陣列求和路徑記錄下來
for(int i = right; i ; i -= lowbit(i)) use[i] = s[i]; //由於是從根節點往下走,一開始是s,後來是lc,所以需要開一個use陣列
while(l < r) //用迴圈模擬遞迴,減小常數
{
int mid = (l+r)>>1;
int tmp = getSum(right)-getSum(left-1)+sum[lc[right_rt]]-sum[lc[left_rt]];
if(tmp >= k)
{
r = mid;
for(int i = left-1; i ; i -= lowbit(i)) use[i] = lc[use[i]];
for(int i = right; i ; i -= lowbit(i)) use[i] = lc[use[i]];
left_rt = lc[left_rt];
right_rt = lc[right_rt];
}
else{
l = mid+1;
k -= tmp;
for(int i = left-1; i ; i -= lowbit(i)) use[i] = rc[use[i]];
for(int i = right; i ; i -= lowbit(i)) use[i] = rc[use[i]];
left_rt = rc[left_rt];
right_rt = rc[right_rt];
}
}
return l; //此處return l或者r 都是可以的
}
int getID(int x) //二分查詢這個數離散化之後的位置
{
return lower_bound(dis+1,dis+1+cnt,x)-dis;
}
int main()
{
int T;
scanf("%d",&T);
while(T--)
{
scanf("%d%d",&n,&m); //n個人,m條操作
tot = cnt = idx = 0;
rep(i,1,n)
{
scanf("%d",&a[i]);
dis[++cnt] = a[i]; //離散化陣列
}
char op[10];
//至於為什麼要先把所有的修改的節點找出來,才進行建樹,那是因為只有知道序列的範圍才可以建樹,無他
//因此需要離線操作,主要原因是離散化
rep(i,1,m)
{
scanf("%s",op);
if(op[0] == 'Q')
{
que[i].kind = 0;//查詢[l,r]第k大
scanf("%d%d%d",&que[i].l,&que[i].r,&que[i].k);
}
else{
que[i].kind = 1;//將第x個點改為y
scanf("%d%d",&que[i].l,&que[i].r);
dis[++cnt] = que[i].r;
}
}
sort(dis+1,dis+1+cnt);
cnt = unique(dis+1,dis+1+cnt)-dis-1;//離散化
root[0] = build(1,cnt);//建立空樹
rep(i,1,n)
root[i] = update(root[i-1],getID(a[i]),1);//建立靜態主席樹
rep(i,1,n)
s[i] = root[0];//為每個樹狀陣列根節點初始化
rep(i,1,m)
{
if(que[i].kind == 0)
printf("%d\n",dis[query(que[i].l,que[i].r,que[i].k)]);
else{
add(que[i].l,getID(a[que[i].l]),-1);//先消除影響
add(que[i].l,getID(que[i].r),1);//再新建影響
a[que[i].l] = que[i].r;
}
}
}
return 0;
}