树状数组整理(4.冒牌平衡树)

算是一种应用吧……水过某些题的时候可以一试?

平衡树一般支持插入、删除、查找、排名、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的速度不慢……

那么全剧终吧?

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值