数据结构总结

来源:互联网 发布:淘宝网找胶片老相机 编辑:程序博客网 时间:2024/05/21 15:46

一、树链刨分

按照重儿子分就行了,理论复杂度是log^2的,但事实上常数比较小。

我YY了一个优化的方法:如果题目只涉及路径的修改,可以针对每个重链单独建一棵线段树(这样必须用指针表示儿子),然后可以发现除了u,v,lca(u,v)三个点需要深入线段树中,其他的重链在线段树的根节点读了值就直接返回了,这样写复杂度是logn的,操作量特别大的题可以看出明显的差距。

但是如果题目同时涉及路径和子树的修改(NOI2015D1T2),就必须老老实实按dfs序来建树了。因为重链上的点在dfs序中是连续的,所以dfs序既可以实现子树修改(logn),也可以实现路径修改(log^2n),比较巧妙。

理论上如果点数在20w以内,DFS函数内只有两个参数,在Linux下可以写递归的。如果特别大,好像不能用BFS,要用手工栈的DFS,不然没法采集dfs序。

本来链剖是用来维护点权的,如果要维护边权,把它下放到点上即可。

int htp[MAXN], hsn[MAXN], sz[MAXN];int fa[MAXN], dep[MAXN], pp[MAXN], fp[MAXN];void DFS1(int u) //注意最好将fa定义在全局数组,减少DFS的栈空间开销。{sz[u] = 1;for (Ed*p = adj[u]; p; p=p->nxt)if (p->to!=fa[u]) {dep[p->to] = dep[u] + 1;fa[p->to] = u;DFS1(p->to);sz[u] += sz[p->to];if (sz[p->to] > sz[hsn[u]]) hsn[u] = p->to;}}int tmr;void DFS2(int u, int tp){htp[u] = tp;pp[u] = ++tmr; //即dfn,映射到线段树中的节点fp[tmr] = u;   //线段树的节点反馈回原树,实际上并不常用。if (hsn[u]) DFS2(hsn[u], tp); //保证重链有的连续的dfs序for (Ed *p = adj[u]; p; p=p->nxt)if (p->to != fa[u] && p->to != hsn[u])DFS2(p->to, p->to);}

查询和修改,注意题目是边权还是点权,如果是边权的话则不统计lca(u,v),如果是点权则需要。

void paint(int u, int v, int c){for (int f1=htp[u], f2=htp[v]; f1 != f2; ){if (dep[f1] < dep[f2]) swap(f1,f2), swap(u, v);sg.ins(1, pp[f1], pp[u], c);u = fa[f1], f1 = htp[u];}if (dep[u] > dep[v]) swap(u, v);sg.ins(1, pp[u], pp[v], c); //sg为线段树结构体}



二、比较快的二叉查找树。

常见的有SBT,AVL,Treap,这三个作为平衡树的时候功能完全一样,会一个就行了。

维护数值的平衡树由于不需要自底向上的操作,所以不需要维护father指针,旋转的时候是以父节点穿进去的,实际表示用一个节点的左/右节点来替换它自己。

SBT效率比较高。注意插入一个节点之后要Maintain,删除的时候直接用前驱替代就行了,不需要maintain,因为删除一个数不会导致SBT退化。但是删除比较难写,在题目时间比较宽松的情况下,可以用splay来替代这几个平衡树。

void rotate(Node*&t, bool d) //d=0左旋{t->ch[!d] = t->ch[!d]->ch[d];if (t != NIL) pushup(t);t->ch[!d]->ch[d] = t;t = t->ch[!d];if (t != NIL) pushup(t);}void maintain(Node*&t, bool flag){if (t->ch[flag]->ch[flag]->sz > t->ch[!flag]->sz)rotate(t->ch[flag], !flag);else if (t->ch[flag]->ch[!flag]->sz > t->ch[!flag]->sz)rotate(t->ch[flag], flag), rotate(t, !flag);else return;maintain(t->ch[0], 0);maintain(t->ch[1], 1);maintain(t, 0), maintain(t, 1);}


三、splay

作为平衡树特别慢,但是可以拿来维护区间。维护区间的时候记住要再数列最左边和最右边放上虚拟节点,因为执行区间操作的时候需要提取区间的前驱和后继。

节点结构体的定义,注意放哨兵。

struct Node { int sz, sum, mx, w;  bool rev; Node*fa, *ch[2];Node ();} nil, *NIL = &nil;Node::Node() {fa = ch[0] = ch[1] = NIL;sz = 1; rev = 0; sum = w = 0; mx = -inf;}#define lch(x) x->ch[0]#define rch(x) x->ch[1]


pushdown / lazytag:最好是“已修改该节点,待修改子树”,这样在pushup的时候不会出问题,否则在pushup内部要将两个子节点pushdown了再汇总。

pushup:一般要更新size,sum,maxv,minv。如果题目涉及区间信息合并的时候,要像hotel那样保持左端点信息,右端点信息,整个区间的信息,但具体实现和线段树有不同,因为splay中每个节点不仅要表示一个区间,还要考虑他自己的信息。

rotate:用当前节点替代其父亲的位置,和AVL,SBT中的意义稍有不同。

void rotate(Node*x){Node *y = x->fa;pushdown(y);pushdown(x);int d = (x==lch(y));y->ch[!d] = x->ch[d];if (x->ch[d] != NIL) x->ch[d]->fa = y;x->fa = y->fa;if (y->fa != NIL)y->fa->ch[ y->fa->ch[1]==y ] = x;x->pfa = y->pfa; //这两行代码是针对LCT的。平常不需要。y->pfa = NIL;    //用于修改连接splay间的路径(path parent)。x->ch[d] = y;y->fa = x;pushup(y);}


splay:要先pushdown(x),如果rotate里面没写pushup的话最后还要pushup。如果要维护根,需要在这里面修改。
void splay(Node*x, Node*to = NIL) //使x转到to的下面{pushdown(x);for (Node *y, *z; x->fa != to; rotate(x)){y = x->fa; z = y->fa;if (z != NIL) rotate((y==lch(z))^(x==lch(y)) ? x : y); //如果形成一条链就双旋}pushup(x);}

建树:将一开始有的序列建成完全二叉树,可以优化常数。

Node* build(int l, int r, Node* fa){if (l>r) return NIL;int mid = (l+r)>>1;nd[mid].ch[0] = build(l, mid-1, nd+mid); //nd为内存池数组nd[mid].ch[1] = build(mid+1, r, nd+mid);nd[mid].fa = fa;pushup(nd + mid);return nd + mid;}

split / merge:很好写,几行代码就搞定了,但是要注意返回新根节点的指针。

insert / erase:用split和merge实现即可,避免漏掉中间节点的更新。

getkth:非常重要的操作,通过size即可完成。

Node* getkth(Node*&r, int k, Node*to=NIL){Node*t = r;pushdown(t);for (; k != lch(t)->sz + 1; pushdown(t))if (k <= lch(t)->sz) t = lch(t);else k -= lch(t)->sz + 1, t = rch(t);splay(r, t, to);return t;}

区间操作:注意第一个节点是虚拟节点,因此对[l,r]操作时实际需要对[l+1,r+1]进行操作,前驱和后继分别是l和r+2.

void work1(int l, int r){getkth(l);getkth(r+2, root);ope(lch(rch(root)));//修改该节点并给它打标记(注意标记的含义)。pushdown(rch(root));pushup(rch(root)); pushup(root);}



四、link-cut tree(LCT)

注意:LCT不像树链剖分,LCT不支持子树的整体修改。

如果用的是指针实现的splay,在这里就会有点不方便了,需要切换点的编号和下标。

注意,由于LCT很多时候是自底向上的,splay的时候需要将到splay的根的路径上的所有点pushdown,这个步骤可以放在rotate函数里面(其实上面的模板里面已经包含了这个操作),也可以事先用个栈来将所有点pushdown。


access(expose):将一个节点与树根之间的所以路径设为重边,存入一棵splay中。

void access(Node*x){Node*y = NIL;for (; x != NIL; x = x->pfa){splay(x);if (rch(x) != NIL)rch(x)->pfa = x, rch(x)->fa = NIL;x->ch[1] = y;y->fa = x; y->pfa = NIL;pushup(y = x);}}

makeroot:将一个点设为整棵树的树根,通常情况下题目要求维护路径信息,所以树根是谁并没有关系。一个操作由于只是把其中一条路径翻转一下,这条路径上面的点连的分支还是带在相同的点上,所以树里面所有的路径信息不会受到影响。但是如果是题目明确根的信息很重要(相当于有根树),操作之前还是要把本来的根给makeroot回来。

void makeroot(Node*x) {access(x), splay(x), uprev(x);}


link / cut:将两个点之间的边断开 / 将两颗子树以边<u,v>合并。
void link(int u, int v) {Node*x = nd+u, *y = nd+v;makeroot(x);x->pfa = y;}void cut(int u, int v) {Node*x = nd+u, *y = nd+v;makeroot(x);access(y), splay(y);y->ch[0] = x->fa = NIL;}

路径查询/修改:对于节点u,v之间的路径,将u作为根,然后access(v), splay(v),v的左子树为路径。注意由于不方便插入前驱和后继,需要单独考虑v节点。

int qmax(int u, int v) //例:查询路径最大值{Node*x = nd+u, *y = nd+v;makeroot(x);access(y), splay(y);return max(lch(y)->mx, y->w);}



五、主席树(可持久化线段树)

就是很多棵表示不同阶段/状态的线段树,共用了大量的节点。可以利用区间减法实现区间/树上路径第K大。

这个在做树上路径第K大的时候,每个点可以由它的父亲的线段树复制而来,这样查询不需要链剖,很巧妙。但是注意减的时候有个细节,应该是sum(u)+sum(v)-sum(lca(u,v))-sum(fa(lca(u,v))),这样减出来的才是完整的路径。

如果带修改,显然不能单纯修改某一棵线段树,因为有其他线段树共用了它的信息。需要外面套个树状数组,但是这样一来空间和时间开销都是mlog^2n的了,时间还好,内存有点吃不消。事实上不需要开满那么多,抵着内存上限开,然后减少一些insert,事实上消耗不了那么多空间。

struct Node {int cnt;Node*lch, *rch;};struct SegTree{Node nil, *NIL;Node nds[MAXN*19], *ncnt;Node *rt[MAXN];SegTree () {NIL = &nil;rt[0] = nil.lch = nil.rch = NIL;ncnt = nds;}Node*NewNode(int a, Node*b, Node*c){++ncnt;*ncnt = {a, b?b:NIL, c?c:NIL};return ncnt;}void ins(Node*&x, Node*p, int v, int l, int r){if (!p) p = NIL;x = NewNode(p->cnt+1, p->lch, p->rch);if (l == r) return;int mid = (l+r)>>1;if (v <= mid) ins(x->lch, p->lch, v, l, mid);else ins(x->rch, p->rch, v, mid+1, r);}int getkth(Node*p1, Node*p2, Node*q1, Node*q2, int k, int l, int r){if (l == r) return l;int mid = (l+r)>>1;int lx = (p2->lch->cnt) + (p1->lch->cnt);lx -= (q1->lch->cnt) + (q2->lch->cnt);if (k <= lx) return getkth(p1->lch, p2->lch, q1->lch, q2->lch, k, l, mid);else return getkth(p1->rch, p2->rch, q1->rch, q2->rch, k - lx, mid+1, r);}} sg;


0 0