应当说这段时间学习了很多的数据结构,也到了一个总结的时候。fotile96的这篇Blog非常值得推荐,我达不到这个高度,只能给自己和队友做些简单的归纳。
树状数组
非常简单的数据结构,只需要一个数组,一切操作基于如下的函数。需要注意的是,树状数组的下标必须从1开始(从0开始会死循环)。它可以做到在O(logn)时间内完成下述的操作。
int bit[MAXN],n;
inline int lowbit(int x) {
return x&-x;
}
最常见的是增减单点的值,询问前缀和。
void add(int x,int val) {
for(int i=x; i<=n; i+=lowbit(i))
bit[i]+=val;
}
int sum(int x) {
int ret=0;
for(int i=x; i>0; i-=lowbit(i))
ret+=bit[i];
return ret;
}
我们注意到,只要维护原序列的差分序列,就可以用上述方式实现对前缀的每一位增减同一个值,询问单点值。不过,其实我们不需要手动将原数组转化为差分序列,只需对函数做一个简单的转化。
void add(int x,int val) {
for(int i=x; i>0; i-=lowbit(i))
bit[i]+=val;
}
int get(int x) {
int ret=0;
for(int i=x; i<=n; i+=lowbit(i))
ret+=bit[i];
return ret;
}
树状数组其实是可以实现区间增减,区间求和的。但这个一般还是用线段树来实现。
//修改区间:add(r,val);
// if(l>1) add(l-1,-val);
//查询区间:sum(r)-sum(l-1);
int bit1[MAXN],bit2[MAXN],n;
void add(int x,int val) {
for(int i=x; i>0; i-=lowbit(i))
bit1[i]+=val;
for(int i=x; i<=n; i+=lowbit(i))
bit2[i]+=x*val;
}
int sum(int x) {
if(!x)
return 0;
int ret1=0,ret2=0;
for(int i=x; i<=n; i+=lowbit(i))
ret1+=bit1[i];
for(int i=x-1; i>0; i-=lowbit(i))
ret2+=bit2[i];
return ret1*x+ret2;
}
树状数组也可以维护最值,但复杂度上升一个log,所以不如用线段树来维护。
void modify(int x,int val) {
num[x]=val;
for(int i=x; i<=n; i+=lowbit(i)) {
bit[i]=max(bit[i],val);
for(int j=1; j<lowbit(i); j<<=1)
bit[i]=max(bit[i],bit[i-j]);
}
}
int query(int L,int R) {
int ret=num[R];
while(true) {
ret=max(ret,num[R]);
if(L==R)
break;
for(R-=1; R-L>=lowbit(R); R-=lowbit(R))
ret=max(ret,bit[R]);
}
return ret;
}
线段树
现在最常见的线段树实现大概有两个版本,一个是NotOnlySuccess的风格,也是我最常使用的风格;它的特点是利用二叉树的数学关系来维护根节点信息。这种写法有一点不足是必须开4倍内存。支持在O(1)时间内将子树信息合并的序列信息理论上都可以用线段树来维护(如最大子段和等),区间维护依靠lazy标记完成以维持O(logn)的单次操作复杂度。
#define lson l,m,rt<<1
#define rson m+1,r,rt<<1|1
int sum[MAXN<<2],add[MAXN<<2];
void push_up(int rt) {
sum[rt]=sum[rt<<1]+sum[rt<<1|1];
}
void push_down(int rt,int len) {
if(add[rt]) {
add[rt<<1]+=add[rt];
add[rt<<1|1]+=add[rt];
sum[rt<<1]+=add[rt]*(len-(len>>1));
sum[rt<<1|1]+=add[rt]*(len>>1);
add[rt]=0;
}
}
void build(int l,int r,int rt) {
add[rt]=0;
if(l==r) {
scanf("%d",&sum[rt]);
return;
}
int m=l+r>>1;
build(lson);
build(rson);
push_up(rt);
}
void update(int p,int val,int l,int r,int rt) {
if(l==r) {
sum[rt]+=val;
return;
}
push_down(rt,r-l+1);
int m=l+r>>1;
if(p<=m)
update(p,val,lson);
else
update(p,val,rson);
push_up(rt);
}
void update(int L,int R,int val,int l,int r,int rt) {
if(L<=l&&r<=R) {
add[rt]+=val;
sum[rt]+=val*(r-l+1);
return;
}
push_down(rt,r-l+1);
int m=l+r>>1;
if(L<=m)
update(L,R,val,lson);
if(m<R)
update(L,R,val,rson);
push_up(rt);
}
int query(int L,int R,int l,int r,int rt) {
if(L<=l&&r<=R)
return sum[rt];
push_down(rt,r-l+1);
int m=l+r>>1,ret=0;
if(L<=m)
ret+=query(L,R,lson);
if(m<R)
ret+=query(L,R,rson);
return ret;
}
另外一种有些类似,与上面不同的是,它使用下面这个神奇的ID函数组织每个节点在数组中的位置。具体实现不再赘述。
inline int ID(int l,int r) {
return l+r|l!=r;
}
可持久化线段树
如果我们想在用数据结构维护信息时,留下所有操作的历史记录,以便回退和查询,我们就需要对数据结构进行可持久化。可持久化的理念非常简单,就是把每次修改节点的操作变成新建新节点的操作,留下了原节点就留下了历史记录。
线段树是天生利于可持久化的数据结构,原因在于形态的一致性。具体地说,对于以固定方式建立的固定大小的线段树,其形态是完全一致的,且不会因修改而产生变动。为了节约空间,采用了自顶向下的函数式风格,保证了每次更新节点个数在O(logn)的级别,这个做法由黄嘉泰(fotile96)首创,故又称主席树。更具体的介绍可以看我以前的Blog,里面有很多例题,这里不再赘述。
实时开节点的线段树
我们注意到可持久化线段树常常被用来维护集合信息而非序列信息,换言之,经常以权值线段树的形式存在,其大小不由数据量决定,而是由数据的范围决定。这样我们便遇到一个困境,对于可持久化线段树这一在线维护信息的利器,却常常需要先离散化才可以使用,从实际上变成了离线做法,面对强制在线的题目非常尴尬。对此,陈立杰(WJMZBMR)在13年国家集训队论文中提到实时开节点的权值线段树;陈立杰的解释有一点抽象,我们可以这样理解,可持久化线段树是先建立初始版本的完整的树,在创建新版本时对修改的节点实时开新节点;而我们现在变成一棵初始状态为一棵空树,从一开始就实时开新节点。
即使这样说,还是显得不够具体,难以据此实现出实时开节点的线段树,所以这里以BZOJ1901为例,给出一个没有离散化,而是采用实时开节点的可持久化线段树的实现,与这里的代码做一个对比;可以发现实现起来并不麻烦。唯一美中不足是线段树部分单次操作复杂度由O(logn)升为O(logV),其中n是数据量,V是数据范围上界。
#include<cstdio>
#include<cstring>
using namespace std;
#define lson l,m,ls[rt]
#define rson m+1,r,rs[rt]
const int MAXN=60005;
const int MAXM=2500005;
const int INF=0x3f3f3f3f;
int ls[MAXM],rs[MAXM],cnt[MAXM],root[MAXN],tot;
int new_node() {
++tot;
ls[tot]=rs[tot]=cnt[tot]=0;
return tot;
}
void update(int p,int val,int l,int r,int &rt) {
if(!rt)
rt=new_node();
if(l==r) {
cnt[rt]+=val;
return;
}
int m=l+r>>1;
if(p<=m)
update(p,val,lson);
else
update(p,val,rson);
cnt[rt]=cnt[ls[rt]]+cnt[rs[rt]];
}
int use[MAXN],n;
inline int lowbit(int x) {
return x&-x;
}
void modify(int x,int p,int val) {
for(int i=x; i<=n; i+=lowbit(i))
update(p,val,0,INF,root[i]);
}
int sum(int x) {
int ret=0;
for(int i=x; i>0; i-=lowbit(i))
ret+=cnt[ls[use[i]]];
return ret;
}
int query(int ss,int tt,int l,int r,int k) {
for(int i=ss; i>0; i-=lowbit(i))
use[i]=root[i];
for(int i=tt; i>0; i-=lowbit(i))
use[i]=root[i];
while(l<r) {
int m=l+r>>1,tmp=sum(tt)-sum(ss);
if(k<=tmp) {
r=m;
for(int i=ss; i>0; i-=lowbit(i))
use[i]=ls[use[i]];
for(int i=tt; i>0; i-=lowbit(i))
use[i]=ls[use[i]];
} else {
l=m+1;
k-=tmp;
for(int i=ss; i>0; i-=lowbit(i))
use[i]=rs[use[i]];
for(int i=tt; i>0; i-=lowbit(i))
use[i]=rs[use[i]];
}
}
return l;
}
int a[MAXN];
int main() {
int m,l,r,k;
char op;
while(~scanf("%d%d",&n,&m)) {
tot=0;
memset(root,0,sizeof(root));
for(int i=1; i<=n; ++i) {
scanf("%d",&a[i]);
modify(i,a[i],1);
}
while(m--) {
scanf(" %c%d%d",&op,&l,&r);
switch(op) {
case 'Q':
scanf("%d",&k);
printf("%d\n",query(l-1,r,0,INF,k));
break;
case 'C':
modify(l,a[l],-1);
a[l]=r;
modify(l,a[l],1);
break;
}
}
}
}
SkipList(跳表)
没什么用,这是我对它的评价;它能实现的一切操作用平衡二叉树都可以实现。这里放一个我测过可用,但没花心思修改的版本。
const int MAX_LEVEL=18;
struct SkipList {
struct node {
int key,val;
node *next[1];
};
int level;
node *head;
SkipList() {
level=0;
head=NewNode(MAX_LEVEL-1,0,0);
for(int i=0; i<MAX_LEVEL; ++i)
head->next[i]=NULL;
}
node* NewNode(int level,int key,int val) {
node *ns=(node *)malloc(sizeof(node)+level*sizeof(node*));
ns->key=key;
ns->val=val;
return ns;
}
int randomLevel() {
int k=1;
while(rand()&1)
++k;
return k<MAX_LEVEL?k:MAX_LEVEL;
}
int find(int key) {
node *p=head,*q=NULL;
for(int i=level-1; i>=0; --i)
while((q=p->next[i])&&q->key<=key) {
if(q->key==key)
return q->val;
p=q;
}
return -INF;
}
bool insert(int key,int val) {
node *update[MAX_LEVEL],*p=head,*q=NULL;
for(int i=level-1; i>=0; --i) {
while((q=p->next[i])&&q->key<key)
p=q;
update[i]=p;
}
if(q&&q->key==key)
return false;
int k=randomLevel();
if(k>level) {
for(int i=level; i<k; ++i)
update[i]=head;
level=k;
}
q=NewNode(k,key,val);
for(int i=0; i<k; ++i) {
q->next[i]=update[i]->next[i];
update[i]->next[i]=q;
}
return true;
}
bool erase(int key) {
node *update[MAX_LEVEL],*p=head,*q=NULL;
for(int i=level-1; i>=0; --i) {
while((q=p->next[i])&&q->key<key)
p=q;
update[i]=p;
}
if(q&&q->key==key) {
for(int i=0; i<level; ++i)
if(update[i]->next[i]==q)
update[i]->next[i]=q->next[i];
free(q);
for(int i=level-1; i>=0; --i)
if(head->next[i]==NULL)
--level;
return true;
}
return false;
}
node* getMin() {
return head->next[0];
}
node* getMax() {
node *p=head,*q=NULL;
for(int i=level-1; i>=0; --i)
while((q=p->next[i])&&q)
p=q;
return p==head?NULL:p;
}
};
平衡二叉树
平衡二叉树是维护集合有序性的常用数据结构。我会的平衡二叉树并不多,基本以实用为原则,有Treap、Size Balanced Tree(以下简称SBT)和Splay。它们的很多操作的实现方式完全一致或大同小异,这里为节省篇幅一并放出;注意这些操作适用于不允许重复值的平衡二叉树(set而非multiset),对于允许重复值(拥有cnt域)的实现,只要在一些+1的地方稍作修改(改成cnt[x])即可。一些使用的例题可以在这篇Blog里找到,不再赘述。
bool find(int v) {
for(int x=root; x; x=ch[x][key[x]<v])
if(key[x]==v)
return true;
return false;
}
int get_kth(int k) {
int x=root;
while(size[ch[x][0]]+1!=k)
if(k<size[ch[x][0]]+1)
x=ch[x][0];
else {
k-=size[ch[x][0]]+1;
x=ch[x][1];
}
return key[x];
}
int get_rank(int v) {
int ret=0,x=root;
while(x)
if(v<key[x])
x=ch[x][0];
else {
ret+=size[ch[x][0]]+1;