数据结构总结

一、树链刨分

按照重儿子分就行了,理论复杂度是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;


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值