树链剖分
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. 边权转点权
比较平凡,把边权下放到深度更深的子节点正常维护
比较模板
难点在于线段树的标记
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. 配合换根
换根可以不用 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);
}