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;
高精运算: typedef struct //为方便处理,用结构体 { int len ; long num [1024] ; } HNum ; //万进制高精加法, 注意输出高位补0, printf ("%04d" …) ; void HPlus (HNum &a, HNum &b, HNum &c) { int i, len = a.len > b.len ? a.len : b.len ; memset (&c, 0, sizeof (HNum)) ; for (i = 1 ; i <= len ; i ++) { c.num [i] += a.num [i] + b.num [i] ; if (c.num [i] >= BASE) { c.num [i+1] += c.num [i] / BASE ; c.num [i] %= BASE ; } } c.len = len ; while (c.num [c.len+1] > 0) c.len ++ ; } //万进制高精乘法 void HMul (HNum &a, HNum &b, HNum &c) { int i, j ; memset (&c, 0, sizeof (HNum)) ; for (i = 1 ; i <= a.len ; i ++) for (j = 1 ; j <= b.len ; j ++) { c.num [i+j-1] += a.num [i] * b.num [j] ; //注意+号 if (c.num [i+j-1] >= BASE) { c.num [i+j] += c.num [i+j-1] / BASE ; //注意+号 c.num [i+j-1] %= BASE ; } } c.len = a.len + b.len - 1 ; while (c.num [c.len+1] > 0) // c.len ++ ; } //万进制高精减法 void HSub (HNum &a, HNum &b, HNum &c) { int i, len = a.len ; //保证a >= b memset (&c, 0, sizeof (HNum)) ; for (i = 1 ; i <= len ; i ++) { c.num [i] += a.num [i] - b.num [i] ; //注意+号 if (c.num [i] < 0) { c.num [i+1] -= 1 ; //注意-号 c.num [i] += BASE ; } } c.len = len ; while (c.len > 0 && c.num [c.len] == 0) c.len -- ; } //万进制高精减法, 直接就 long long…. -------------------------------------------------------------------------------- //Fibonacci, Fibo [i] = Fibo [i-1] + Fibo [i-2], Fibo [3] = 3 ; // Catalan数列S[n] = C(2n,n)/(n+1) long Catalan (long n) { long i, x, y ; x = y = 1 ; for (i = 2 ; i <= n ; i ++) x *= i ; for (i = n ; i <= 2*n ; i ++) y *= i ; return y/x/(n + 1) ; } //最小公倍数 long lcm (long a, long b) { return a*b/gdc (a, b) ; } //最大公约数, 辗转相除法 long gdc (long a, long b) { return (a%b == 0)? b : gdc (b, a%b) ; } ------------------------------------------------------------------------------------------------------------ //堆操作 void In (HeapDT dt) //进堆 { int i ; list [++ len] = dt ; i = len ; while (i > 1) //向上调整 { if (list [i].w < list [i/2].w) Swap (i, i/2) ; else break ; i /= 2 ; } } HeapDT Out () //出堆 { HeapDT ret = list [1] ; Swap (1, len) ; //NOTE: 最重要的一步, 最后(最大)一个元素与第一个元素交换 len -- ; //堆长度减1 int i, pa = 1 ; for (i = pa * 2 ; i <= len ; i *= 2) //向下调整 { if (i < len && list [i+1].w < list [i].w) i ++ ; if (list [i].w < list [pa].w) Swap (pa, i) ; else break ; pa = i ; } return ret ; } ------------------------------------------------------------------------------------------------------------ //二分查找, 注意等号 while (low < high) { mid = (low + high) >> 1 ; if (strcmp (spname, name [mid]) <= 0) high = mid ; else low = mid + 1 ; } ------------------------------------------------------------------------------------------------------------ //快排 void QSort (int low, int high) { int l, r ; Milkcow p = cow [low] ; l = low, r = high ; while (l < r) { while (l < r && cow [r].price >= p.price) r -- ; cow [l] = cow [r] ; while (l < r && cow [l].price <= p.price) l ++ ; cow [r] = cow [l] ; } cow [l] = p ; if (l-1 > low) QSort (low, l-1) ; if (l+1 < high) QSort (l+1, high) ; } -------------------------------------------------------------------------------------------- //优化并查集 int FindSet (int i) { if (Parent [i] != i) //状态压缩 Parent [i] = FindSet (Parent [i]) ; return Parent [i] ; } void UnionSet (int a, int b) { int i, j ; i = FindSet (a) ; j = FindSet (b) ; if (i != j) if (Rank [i] > Rank [j]) //启发式合并:让深度较小的树成为深度较大的树的子树 Parent [j] = i ; else { Parent [i] = j ; if (Rank [i] == Rank [j]) Rank [j] ++ ; } } ------------------------------------------------------------------------------------------- 图论: //MST double Kruscal () { int i, k = 0 ; double s = 0 ; for (i = 0 ; i <= n ; i ++) Parent [i] = i ; for (i = 0 ; i < m && k < n-1; i ++) //m为总边数 { if (FindSet (Edge [i].a) != FindSet (Edge [i].b)) { s += Edge [i].v ; if (s > S) //是否超出范围 return 0 ; UnionSet (Edge [i].a, Edge [i].b) ; k ++ ; //记录合并的边数 } } if (k != n-1) return 0 ;
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值