ACM常用数据结构小结与实现

本文总结了ACM竞赛中常用的数据结构,包括树状数组、线段树、可持久化线段树、平衡二叉树(如Treap、Size Balanced Tree、Splay)以及跳表等。详细介绍了每种数据结构的特点、应用场景及其实现原理,帮助读者理解和掌握这些数据结构的使用。
摘要由CSDN通过智能技术生成

  应当说这段时间学习了很多的数据结构,也到了一个总结的时候。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;
  • 1
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
内含资源如下: 1.基本数据结构 1.1.Array ........... 动态数组 1.2.LinkedList ... 链表 1.3.BST .............. 二分搜索树 1.4.MapBST ..... 二分搜索树(用于实现映射) 1.5.AVLTree ...... AVL树 2.接口 2.1.Queue ........... 队列接口 2.2.Stack .............. 栈接口 2.3.Set .................. 集合接口 2.4.Map ............... 映射接口 2.5.Merger .......... 自定义函数接口 2.6.UnionFind ..... 并查集接口 3.高级数据结构 3.1.ArrayQueue .......................... 队列_基于动态数组实现 3.2.LinkedListQueue .................. 队列__基于链表实现 3.3.LoopQueue ........................... 循环队列_基于动态数组实现 3.4.PriorityQueue ....................... 优先队列_基于最大二叉堆实现 3.5.ArrayPriorityQueue ............. 优先队列_基于动态数组实现 3.6.LinkedListPriorityQueue ..... 优先队列_基于链表实现 3.7.ArrayStack ............................. 栈_基于动态数组实现 3.8.LinkedListStack ..................... 栈_基于链表实现 3.9.BSTSet ..................................... 集合_基于二分搜索树实现 3.10.LinkedListSet ....................... 集合_基于链表实现 3.11.BSTMap ................................ 映射_基于二分搜索树实现 3.12.AVLTreeMap ....................... 映射_ 基于AVL树实现 3.13.LinkedListMap .................... 映射_基于链表实现 3.14.MaxHeap ............................. 最大二叉堆 3.15.SegmentTree ...................... 线段树 3.16.Trie ......................................... 字典树 3.17.QuickFind ............................ 并查集_基于数组实现 3.18.QuickUnion ......................... 并查集_基于树思想实现
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值