树链剖分

dfs 序的应用

很显然的就不说了,分析加法对求的东西的贡献,因为区间操作基本只能对子树,子树的 dfs 序连续

把路径加路径查用差分转换为到根的路径,默认维护路径的都是维护到根的

  • 路径加,单点查询:考虑某个点只有在从根出发的路径的路径终点在子树内时才会被加,因此转化为 dfs 序序列上单点加,子树查询

  • 单点加,路径查询:此时用欧拉序,每个节点有 \(in,out\) 两个位置,加时把 \(in\)\(val\),把 \(out\)\(val\),查询则查询 \(1\sim in_x\) 的和

  • 子树加,路径查询:对 \(x\) 加,会对 \(x\) 子树内 \(y\) 到根的路径产生 \((dep_y-dep_x+1)\times val\) 的贡献,拆开,维护 \(\sum val,\sum dep_x\times val\),转化为子树加,单点查询

  • 子树加,子树查询:区间加区间查询

  • 路径加,子树查询:\(y\) 的贡献来自 \(y\) 子树内 \(x\) 被加过,产生 \((dep_x-dep_y+1)\times val\) 的贡献,同理拆开维护,转化为单点加,子树查询

但是,如果是路径修改,路径查询呢?


树链剖分

前置知识:dfs 序,线段树

这里定义:

重儿子:一个节点儿子中子树大小最大的儿子(叶子没有儿子,即无重儿子)

轻儿子:剩下的

重边:连接任意两个重儿子的边叫做重边

轻边:剩下的

重链:相邻重边连起来的连接一条重儿子的链叫重链

流程:

首先预处理各种内容,包括重儿子

inline void dfs1(ll x, ll fath)
{
	fa[x] = fath, siz[x] = 1;
	for(reg ll i = 0; i < (ll)edge[x].size(); ++i)
	{
		if(edge[x][i] == fath)	continue;
		dis[edge[x][i]] = dis[x] + 1;
		dfs1(edge[x][i], x);
		siz[x] += siz[edge[x][i]];
		if(siz[edge[x][i]] > siz[mson[x]])	mson[x] = edge[x][i];
	}
}

然后关键是处理出每个点所在链的开头,这里轻儿子有一条以它自己开头的链

注意这里我们给每个点用 dfs 序添加新编号,作为它在线段树上的位置编号

先遍历每个点的重儿子(后面说原因)

inline void dfs2(ll x, ll tp)
{
	dfn[x] = ++cnt, top[x] = tp; // tp 即为链的开头编号
	if(!mson[x])	return; // 叶子,返回
	dfs2(mson[x], tp); // 先遍历重儿子
	for(reg ll i = 0; i < (ll)edge[x].size(); ++i)
	    if(edge[x][i] != mson[x] && edge[x][i] != fa[x])	dfs2(edge[x][i], edge[x][i]);
}

P3384 【模板】轻重链剖分/树链剖分 为例

修改 \(x->y\) 的路径:

这里先遍历重儿子的好处凸显:每条重链的dfs序编号连续

就很方便在线段树上执行区间操作了

我们直接暴力跳重链,直到 \(x,y\) 处于一条重链上,然后直接区间修改

这里每次跳过的重链条数不超过 \(O(logn)\),单次复杂度不超过 \(O(log^2n)\)

inline void treeadd(ll x, ll y, ll val)
{
	while(top[x] != top[y]) // x和y不在一条重链上
	{
		if(dis[top[x]] < dis[top[y]])	swap(x, y); 
      // x更新为所在重链开头更深的节点,确保每次向上跳不会错过
		update(dfn[top[x]], dfn[x], val, 1, n, 1); // 线段树的修改
		x = fa[top[x]]; // x到所在重链的开头的父节点
	}
	if(dis[x] > dis[y])	swap(x, y); // x更新为深度小的
	update(dfn[x], dfn[y], val, 1, n, 1); // 一条重链上,区间修改
}

那查询就完全同理了~

inline ll treeask(ll x, ll y)
{
	ll res = 0;
	while(top[x] != top[y])
	{
		if(dis[top[x]] < dis[top[y]])	swap(x, y);
		res = res + query(dfn[top[x]], dfn[x], 1, n, 1);
		x = fa[top[x]];
	}
	if(dis[x] > dis[y])	swap(x, y);
	return res + query(dfn[x], dfn[y], 1, n, 1);
}

修改整个子树/查询子树和:

由于子树的dfs序也连续,直接用每个节点的 \(siz\) 找到所在dfs序的区间,直接修改


应用:

\(eg1\)P4211 [LNOI2014]LCA

\(solution\)

我们先只看单组询问,用一个小技巧

将每个 \(l\sim r\) 的数到根的路径上每条边权值 \(+1\),然后查询 \(x\) 到根的路径上的权值和,即可求出

但多组询问?

发现权值有可加性,可以差分

移动 \(r\),直接求出 \(1\sim r\) 的和


\(eg2\)P2486 [SDOI2011]染色

\(solution\)

很明显的树链剖分,难点在区间内颜色段数不好维护,因为可能合并时端点合成一段

那就在线段树上维护每个节点的两段颜色

修改:

  • 整段修改,颜色段数变为 \(1\)

  • 拆开修改,\(pushup\) 时注意两段颜色是否相同,若相同整体段数 \(-1\)

查询:同理,在跳链时也要先单点查询端点颜色

注意修改的 \(tag\) 先后

\(std\)

#include<bits/stdc++.h>
using namespace std;

int n, m, u, v, a[400010], dfn[400010], dep[400010], top[400010], fa[400010], cnt, siz[400010], son[400010], c, seg[400010][2];
int tree[400010], idx, sum[400010], lef[400010], rit[400010], tag[400010], pos[400010], id[400010], st[400010], ed[400010], num[400010];
char op[2];
vector<int> edge[300010];
void dfs1(int x, int fath)
{
	dep[x] = dep[fath] + 1, siz[x] = 1;
	for(int i = 0; i < edge[x].size(); i++)
	{
		if(edge[x][i] == fath)	continue;
		dfs1(edge[x][i], x);
		fa[edge[x][i]] = x, siz[x] += siz[edge[x][i]];
		if(siz[edge[x][i]] > siz[son[x]])	son[x] = edge[x][i];
	}
}
void dfs2(int x, int from)
{
	top[x] = from, dfn[++cnt] = x, pos[x] = cnt, ed[from] = cnt;
	if(!st[from])	st[from] = cnt;
	if(son[x])	dfs2(son[x], from);
	for(int i = 0; i < edge[x].size(); i++)
		if(!pos[edge[x][i]])	dfs2(edge[x][i], edge[x][i]);
} // 预处理
int lson(int x)	{return seg[x][0];}
int rson(int x)	{return seg[x][1];}
void pushup(int x)
{
	sum[x] = sum[lson(x)] + sum[rson(x)];
	if(rit[lson(x)] == lef[rson(x)])	sum[x]--;
	lef[x] = lef[lson(x)], rit[x] = rit[rson(x)];
}
void pushdown(int x)
{
	if(!tag[x])	return;
	tag[lson(x)] = tag[rson(x)] = tag[x]; 
   // 每次修改前都pushdown了,新标记在老标记后,颜色应该为新标记的颜色
	lef[lson(x)] = lef[rson(x)] = rit[lson(x)] = rit[rson(x)] = tag[x];
	sum[lson(x)] = sum[rson(x)] = sum[x] = 1;
 	tag[x] = 0;
}
void build(int l, int r, int p)
{
	if(l == r)
	{
		id[dfn[l]] = p;
		sum[p] = 1, lef[p] = rit[p] = a[dfn[l]];
		return;
	}
	int mid = (l + r) >> 1;
	seg[p][0] = ++idx, build(l, mid, idx), 
 	seg[p][1] = ++idx, build(mid + 1, r, idx);
	pushup(p);
}
void update(int l, int r, int nl, int nr, int p, int col)
{
	if(nl >= l && nr <= r)
	{
		sum[p] = 1, lef[p] = rit[p] = tag[p] = col;
		return;
	}
	int mid = (nl + nr) >> 1;
	pushdown(p);
	if(mid >= l)	update(l, r, nl, mid, lson(p), col);
	if(mid + 1 <= r)	update(l, r, mid + 1, nr, rson(p), col);
	pushup(p);
}
int query(int l, int r, int nl, int nr, int p)
{
	if(!l && !r)	return 0;
	if(nl >= l && nr <= r)	return sum[p];	
	int mid = (nl + nr) >> 1, ans = 0, flag1 = 0, flag2 = 0;
	pushdown(p);
	if(mid >= l)	ans += query(l, r, nl, mid, lson(p)), flag1 = 1;
	if(mid + 1 <= r)	ans += query(l, r, mid + 1, nr, rson(p)), flag2 = 1;
	if(flag1 && flag2 && rit[lson(p)] == lef[rson(p)])	ans--;
	return ans;
} // 线段树
void change(int x, int y, int col)
{
	while(top[x] != top[y])
	{
		if(dep[top[x]] < dep[top[y]])	swap(x, y);
		update(pos[top[x]], pos[x], st[top[x]], ed[top[x]], num[top[x]], col);
		x = fa[top[x]];
	}
	if(dep[x] > dep[y])	swap(x, y);
	update(pos[x], pos[y], st[top[x]], ed[top[x]], num[top[x]], col);
}
int checkc(int x, int nl, int nr, int p) // 查单点颜色
{
	if(nl == nr && nl == x)	return lef[p];
	int mid = (nl + nr) >> 1;
	pushdown(p);
	if(mid >= x)	return checkc(x, nl, mid, lson(p));
	else	return checkc(x, mid + 1, nr, rson(p));
}
int ask(int x, int y)
{
	int ans = 0;
	while(top[x] != top[y])
	{
		if(dep[top[x]] < dep[top[y]])	swap(x, y);
		ans += query(pos[top[x]], pos[x], st[top[x]], ed[top[x]], num[top[x]]);
		if(checkc(pos[top[x]], st[top[x]], ed[top[x]], num[top[x]]) == checkc(pos[fa[top[x]]], st[top[fa[top[x]]]], ed[top[fa[top[x]]]], num[top[fa[top[x]]]]))	ans--;
      // 跳链时若两端颜色相等,则段数--
		x = fa[top[x]];
	}
	if(dep[x] > dep[y])	swap(x, y);
	ans += query(pos[x], pos[y], st[top[x]], ed[top[x]], num[top[x]]);
	return ans;
}
int main()
{
	scanf("%d%d", &n, &m);
	for(int i = 1; i <= n; i++)	scanf("%d", &a[i]);
	for(int i = 1; i < n; i++)
	{
		scanf("%d%d", &u, &v);
		edge[u].push_back(v), edge[v].push_back(u); 
	}
	dfs1(1, 0), dfs2(1, 1);
	for(int i = 1; i <= n; i++)
		if(st[i] && ed[i])	 num[i] = ++idx, build(st[i], ed[i], idx);
	for(int i = 1; i <= m; i++)
	{
		scanf("%s%d%d", op, &u, &v);
		if(op[0] == 'C')
		{
			scanf("%d", &c);
			change(u, v, c);
		}
		else	printf("%d\n", ask(u, v));
	}
	return 0;
} 

3. 树上二分

树上怎么二分?

假设有这样的问题:

每条边上有一个信息,这些信息聚在一起会产生唯一一个满足条件的。一个点会走向周围的出边中唯一满足条件的一条边,且走过了就会删除这条双向边,现在路径是否满足条件可以看作是对整条路径上信息做某种运算,且每个点度数为常数大小,判断是否符合最开始的要求,且可以用线段树等数据结构维护,多次问从一个点出发一直走直到不能走,最终会到达哪里

对树重链剖分,则最多会在重链轻边上切换 \(\log n\)

路径一定可以看作从 \(x\) 先向上走到点 \(y\)(可以与 \(x\) 重合),然后走到 \(y\) 的子树中的某个叶子节点

先向上走,看能否到达重链头,若到不了则二分能到达的最高点,切换为向下走,若到达了则判断是否能向上走轻边,到不了则向下,到的了则向上进入另一条重链,重复这个过程

切换到向下走和向下走时也是看能否到重链底端,到不了就二分出能到的最深位置,枚举那个点的每条边看走过哪条轻边,再到重链顶部,重复过程直到走到叶子为止

类似的应用:P5773 [JSOI2016] 轻重路径

考虑只有当 \(siz_x<\frac {siz_{root}}2\) 时,原本是重儿子的 \(x\) 才会变成轻儿子

设当前 \(nw\) 为删除的叶子节点

从根开始,二分找到最靠上层的 \(x\) 使 \(siz_x<\frac {siz_{root}}2\),判断 \(x\) 是否被换

因为 \(x\) 上方的 \(siz\) 已经比 \(\frac {siz_{root}}2\) 还大,不可能再成为它们父节点的轻儿子

然后把 \(root\) 设为 \(x\),继续,直到走到被删除的节点

注意特判节点被删除的细节

因为每二分一次 \(siz_x\) 减半,所以只会二分 \(\log n\)

利用 dfs 序完成路径加,单点查,复杂度 \(O(n\log^2n)\)

struct bit // dfs序 
{
	ll sum[N << 2];
	void add(ll x, ll y, ll val)
	{
		for(ll i = dfn[y]; i <= cnt; i += i & (-i))	sum[i] += val;
	}
	ll query(ll x)
	{
		ll res = 0;
		for(; x > 0; x -= x & (-x))	res += sum[x];
		return res;
	}
	ll ask(ll x)
	{
		return size[x] + query(mxdfn[x]) - query(dfn[x] - 1);
	}
}tree;
void dfs(ll x)
{
	size[x] = 1, dep[x] = dep[fa[x]] + 1, in[x] = ++idx; 
	if(son[x][0])	fa[son[x][0]] = x, dfs(son[x][0]), size[x] += size[son[x][0]];
	if(son[x][1])	fa[son[x][1]] = x, dfs(son[x][1]), size[x] += size[son[x][1]];
	mson[x] = !(!son[x][1] || size[son[x][0]] >= size[son[x][1]]);
	ans += son[x][mson[x]];
	out[x] = ++idx;
}
void dfs2(ll x, ll tp)
{
	top[x] = tp, dfn[x] = mxdfn[x] = ++cnt, lin[cnt] = x;	
	if(son[x][mson[x]])	dfs2(son[x][mson[x]], tp);
	if(son[x][!mson[x]])	dfs2(son[x][!mson[x]], son[x][!mson[x]]);
	mxdfn[x] = max({mxdfn[x], mxdfn[son[x][0]], mxdfn[son[x][1]]});
}
ll jump(ll x, ll step) // 从 x向上跳 step步 
{
	while(step >= dep[x] - dep[top[x]] + 1)	step -= dep[x] - dep[top[x]] + 1, x = fa[top[x]];
	return lin[dfn[x] - step];
}
ll wh(ll x)	{return x == son[fa[x]][1];}
int main()
{
	n = read();
	for(int i = 1; i <= n; ++i)	son[i][0] = read(), son[i][1] = read();
	dfs(1), dfs2(1, 1);
	print(ans), putchar('\n');
	q = read();
	for(int i = 1; i <= q; ++i)
	{
		nw = read(), root = 1;
		tree.add(1, nw, -1), book[nw] = 1;
		while(1)
		{
			ll l = 0, r = dep[nw] - dep[root], siz = tree.ask(root);
			while(l < r) // 二分 
			{
				ll mid = (l + r + 1) >> 1;
				if(tree.ask(jump(nw, mid)) <= (siz - 1) / 2)	l = mid;
				else	r = mid - 1;
			}
			ll nx = jump(nw, l), ny = son[fa[nx]][!wh(nx)];
			ll sizx = tree.ask(nx), sizy = tree.ask(ny);
			if(!book[ny] && (sizx < sizy || book[nx]) && wh(nx) == mson[fa[nx]])	
				ans += ny - nx, mson[fa[nx]] = wh(ny); // 换了重儿子,更新答案 
			else if(book[nx] && book[ny])	
			{
				ans -= son[fa[nx]][mson[fa[nx]]];
				break;
			}
			if(nx == root || book[nx])	break;	// 如果没有更新或为叶子节点,退出,把贡献减去 
			root = nx;
		}
		print(ans), putchar('\n');
	}
	return 0;
}
/*
当 siz_{nw} \le siz_{root}/2 时,nw才是 root轻儿子
二分找到路径上深度最浅的 nw
nw不一定是root的子节点,但没关系,nw到 root路径上的点 siz已经 >siz_{root}/2,不可能再成为它们父节点的轻儿子
把 root设为 nw,重复 
*/

4. 边权转点权

比较平凡,把边权下放到深度更深的子节点正常维护

P4315 月下“毛景树”

比较模板

P1505 [国家集训队] 旅游

难点在于线段树的标记

int lson(int x)	{return x << 1;}
int rson(int x)	{return x << 1 | 1;}
void pushup(int p)	
{
	sum[p] = sum[lson(p)] + sum[rson(p)];
	mn[p] = min(mn[lson(p)], mn[rson(p)]), mx[p] = max(mx[lson(p)], mx[rson(p)]);
}
void pushdown(int x)
{
	if(tag[x] == 1)	return;
	tag[lson(x)] *= tag[x], tag[rson(x)] *= tag[x];
	sum[lson(x)] *= tag[x], sum[rson(x)] *= tag[x];
	swap(mn[lson(x)], mx[lson(x)]), mn[lson(x)] *= -1, mx[lson(x)] *= -1;
	swap(mn[rson(x)], mx[rson(x)]), mn[rson(x)] *= -1, mx[rson(x)] *= -1;
	tag[x] = 1;
}
void build(int l, int r, int p)
{
	tag[p] = 1;
	if(l == r)	{sum[p] = mx[p] = mn[p] = a[lin[l]];	return;}
	int mid = (l + r) >> 1;
	build(l, mid, lson(p)), build(mid + 1, r, rson(p));
	pushup(p);
}
void modify1(int id, int val, int l, int r, int p)
{
	if(l == r)	{sum[p] = mx[p] = mn[p] = val;	return;}
	int mid = (l + r) >> 1;
	pushdown(p);
	if(mid >= id)	modify1(id, val, l, mid, lson(p));
	else	modify1(id, val, mid + 1, r, rson(p));
	pushup(p);
}
void modify2(int l, int r, int nl, int nr, int p)
{
	if(l <= nl && nr <= r)
	{
		tag[p] *= -1, sum[p] *= -1;
		int lsh = mx[p];	mx[p] = -mn[p], mn[p] = -lsh;
		return;
	}
	int mid = (nl + nr) >> 1;
	pushdown(p);
	if(mid >= l)	modify2(l, r, nl, mid, lson(p));
	if(mid < r)	modify2(l, r, mid + 1, nr, rson(p));
	pushup(p);	
}
int querysum(int l, int r, int nl, int nr, int p)
{
	if(l <= nl && nr <= r)	return sum[p];
	int mid = (nl + nr) >> 1, res = 0;
	pushdown(p);
	if(mid >= l)	res += querysum(l, r, nl, mid, lson(p));
	if(mid < r)	res += querysum(l, r, mid + 1, nr, rson(p));
	return res;
}
int querymax(int l, int r, int nl, int nr, int p)
{
	if(l <= nl && nr <= r)	return mx[p];
	int mid = (nl + nr) >> 1, res = -inf;
	pushdown(p);
	if(mid >= l)	res = max(res, querymax(l, r, nl, mid, lson(p)));
	if(mid < r)	res = max(res, querymax(l, r, mid + 1, nr, rson(p)));
	return res;
}
int querymin(int l, int r, int nl, int nr, int p)
{
	if(l <= nl && nr <= r)	return mn[p];
	int mid = (nl + nr) >> 1, res = inf;
	pushdown(p);
	if(mid >= l)	res = min(res, querymin(l, r, nl, mid, lson(p)));
	if(mid < r)	res = min(res, querymin(l, r, mid + 1, nr, rson(p)));
	return res;
}
void update(int x, int y)
{
	while(top[x] != top[y])
	{
		if(dep[top[x]] < dep[top[y]])	swap(x, y);
		modify2(dfn[top[x]], dfn[x], 1, n, 1);
		x = fa[top[x]];
	}
	if(dfn[x] > dfn[y])	swap(x, y);
	if(dfn[x] < dfn[y])	modify2(dfn[x] + 1, dfn[y], 1, n, 1);
}
int asksum(int x, int y)
{
	int res = 0;
	while(top[x] != top[y])
	{
		if(dep[top[x]] < dep[top[y]])	swap(x, y);
		res += querysum(dfn[top[x]], dfn[x], 1, n, 1);
		x = fa[top[x]];
	}
	if(dfn[x] > dfn[y])	swap(x, y);
	if(dfn[x] < dfn[y])	res += querysum(dfn[x] + 1, dfn[y], 1, n, 1);
	return res;
}
int askmin(int x, int y)
{
	int res = inf;
	while(top[x] != top[y])
	{
		if(dep[top[x]] < dep[top[y]])	swap(x, y);
		res = min(res, querymin(dfn[top[x]], dfn[x], 1, n, 1));
		x = fa[top[x]];
	}
	if(dfn[x] > dfn[y])	swap(x, y);
	if(dfn[x] < dfn[y])	res = min(res, querymin(dfn[x] + 1, dfn[y], 1, n, 1));
	return res;
}
int askmax(int x, int y)
{
	int res = -inf;
	while(top[x] != top[y])
	{
		if(dep[top[x]] < dep[top[y]])	swap(x, y);
		res = max(res, querymax(dfn[top[x]], dfn[x], 1, n, 1));
		x = fa[top[x]];
	}
	if(dfn[x] > dfn[y])	swap(x, y);
	if(dfn[x] < dfn[y])	res = max(res, querymax(dfn[x] + 1, dfn[y], 1, n, 1));
	return res;
}

5. 配合换根

P3979 遥远的国度

换根可以不用 lct 换,对路径加没有影响

分析子树的情况

  • \(x\)\(root\) 时,为整棵树

  • \(root\) 不在 \(x\) 子树内,就是原来子树

  • \(root\)\(x\) 子树内时,为整棵树去掉 \(root\) 所在的 \(x\) 的子节点对应的子树,可以将 \(root\) 往上跳重链,直到遇到 \(x\) 轻子节点,否则就对应重儿子

void update(int x, int y, int val)
{
	while(top[x] != top[y])
	{
		if(dep[top[x]] < dep[top[y]])	swap(x, y);
		modify(dfn[top[x]], dfn[x], val, 1, n, 1);
		x = fa[top[x]];
	}
	if(dfn[x] > dfn[y])	swap(x, y);
	modify(dfn[x], dfn[y], val, 1, n, 1);
}
int merge(int x, int val)
{
	int y = root;
	while(top[x] != top[y])
	{
		if(fa[top[y]] == x)	return top[y];
		y = fa[top[y]];
	}
	return mson[x];
}
int ask(int x)
{
	if(x == root)	return query(1, n, 1, n, 1);
	if(dfn[x] < dfn[root] && dfn[root] <= dfn[x] + siz[x] - 1)
	{
		int y = merge(x, dfn[root]), mn = inf;
		if(dfn[y] > 1)	mn = min(mn, query(1, dfn[y] - 1, 1, n, 1));
		if(dfn[y] + siz[y] <= n)	mn = min(mn, query(dfn[y] + siz[y], n, 1, n, 1));
		return mn;
	}
	return query(dfn[x], dfn[x] + siz[x] - 1, 1, n, 1);
}
posted @ 2024-02-15 10:30  KellyWLJ  阅读(5)  评论(0编辑  收藏  举报