算是一种应用吧……水过某些题的时候可以一试?
平衡树一般支持插入、删除、查找、排名、k小、前驱、后继什么的……
单看插入删除查找,都是离散一下计数就可以做到的;排名可以转化为求比查询元素小的数有多少个;k小也许可以利用排名来搞;至于前驱后继也可以转化为先排名后k小的问题
因为可以离散嘛,不妨直接讨论所有参与操作的数字都在1..n的状况:
前三样加加减减白给的,一个a[1..n]++--搞定;后边几个核心都是排名,而排名是查询比它小的数有多少个,又已经有了下标有序(废话)的a[1..n]来记录每个数的出现次数,那么想要找rank(x)不就是把a[1..x-1]加起来再+1吗?好,BIT的基本模型出现了!
insert很简单,a[x]++之后change(x,+1)
delete也简单,a[x]--再来change(x,-1),如果考虑被删除数不存在的情况就判一下a[x]是否等于0
find更简单,直接return a[x]什么的
rank操作就是sum(x-1)+1,建议把rank和sum分开写,后面频繁调用sum……虽然我的初衷是用rank来着
select的话,用二分法O(logn*logn)找“前缀和大于等于k的最小值”,而且(几乎)不可能错。注意如果sum(n)<k就无解:
int select(int k) {
int l=1,r=n,mid; if (sum(n)<k) return -1;
for (mid=l+r>>1; l<r; mid=l+r>>1)
if (sum(mid)<k) l=mid+1; else r=mid;
return l;
}
注意到返回值是排序离散后的下标,可能不是原来的值。
pred可以直接当成select(rank(x)-1)=select(sum(x-1))
succ差不多,当成select(rank(x+1))=select(sum(x)+1),目测写sum短一点,就写sum吧……
那么再看select,二分O(logn),再来的一个logn出自sum函数,很明显这里存在着重复计算,如何去掉?考虑用c[]数组来拼凑逼近答案:
先换一个逼近思路,可以找出“前缀和小于k的最大值”,这个数+1便是答案,假设这个数叫ans,ans一定能表示成一个二进制数。
令m=log[(n)/log(2)],1<<m就是ans可能的最高位,那么究竟要不要这一段,就看c[1<<m]<k是否成立了,超出范围一定不行,否则必须ans+=1<<m,因为这一位不是1的话剩下位全是1都没它大,不可能比它更靠近k-th;再来第二位,这次要看c[ans+(1<<(m-1))],下次是1<<(m-2),直到1<<0……
喜闻乐见的情况在这里,因为按从高位到低位的顺序来逼近(废话,不然怎么干),每次用来尝试的低位正好是对应下标的lowbit!也就是说每次尝试正好是一个c[i]管辖的范围,这样一位一位跑就可以避免重复计算,得到O(logn)的算法:
int select(int k) {
int p=0,s=0; if (sum(n)<k) return -1;
for (int i=(1<<m); i; i>>=1)
if (p+i<n&&s+c[p+i]<k) {p+=i; s+=c[p];}
return p+1;
}
测试结果表明,算上读入输出(10W条)还有排序离散的时间,前者用时大概是后者的130%(有些操作不会用到select),可以相信实际效率后者比前者会高出不少(而且短,用BIT混平衡树也是为了短啊)最后粘一下全代码,没有套基础BIT:
struct BIT_th {
int n,m,c[N+1],a[N+1];
void init(int s) {
m=(int)(log(n=s)/log(2));
memset(c,0,sizeof(c)); memset(a,0,sizeof(a));
}
void change(int p, int k) {for (; p<=n; p+=p&-p) c[p]+=k;}
int sum(int p) {int s=0; for ( ; p; p-=p&-p) s+=c[p]; return s;}
void ins(int x) {a[x]++; change(x,1);};
void del(int x) {if (a[x]--) change(x,-1); else a[x]=0;}
bool find(int x) {return a[x];}
int rank(int x) {return sum(x-1)+1;}
int select_b(int k) {
int l=1,r=n,mid; if (sum(n)<k) return -1;
for (mid=l+r>>1; l<r; mid=l+r>>1)
if (sum(mid)<k) l=mid+1; else r=mid;
return l;
}
int select(int k) {
int p=0,s=0; if (sum(n)<k) return -1;
for (int i=(1<<m); i; i>>=1)
if (p+i<n&&s+c[p+i]<k) {p+=i; s+=c[p];}
return p+1;
}
int pred(int x) {return select(sum(x-1));}
int succ(int x) {return select(sum(x)+1);}
};
这样看来,SegT也可以这么搞,而且还有额外的好处:首先select部分直接从顶上向下走,lc.size>k直接递归左子树,否则递归右子树并且k-rc.size,不用再想方设法拼凑答案了;其次如果想删掉某个子树(郁闷的出纳员),SegT直接来个区间赋值就行,BIT不支持……
一点补充,BIT做郁闷的出纳员还是很合适的……写完在BZOJ直接能排到rank9,利用正序删除点c[i]=a[i]=0的特性删去a[],再各种恶心的常数优化混到rank4……我自己都没想到。不过至少还是证明了BIT的速度不慢……
那么全剧终吧?