边分治
jiazhaopeng · · 个人记录
与点分治类似,边分治也是用来处理树上链问题的一种方法。只不过点分治是找到重心,考虑经过重心的路径;边分治是找到“重心边”(两边子树大小的最大值最小的那条边),考虑经过这条边的路径。通常情况下用边分治解题会更好想,复杂度也与点度数无关;而点分树常常需要担心点度数的问题。
三度化
直接找“重心边”的复杂度是错的,可以被菊花图卡成
有两种比较常用的方法。一种是造一条虚点链,链上每个点下面能挂个儿子(边为虚边);另一种是线段树式写法。我目前用过的是前者。
三度化的难点在于如何处理虚点虚边的权值。常常可以将那一串虚点和当前节点看作一个点,其间的边看作点内的边,依据题意设置权值。
模板
void dfs_rebuild(int cur, int faa) {
int lst = 0;
for (unsigned int i = 0; i < vec[cur].size(); ++i) {
int to = vec[cur][i]; if (to == faa) continue;
if (!lst) {
Addedge(cur, to, 1);
lst = cur;
} else {
int nw = ++tot; val[nw] = val[cur];
Addedge(lst, nw, 0); Addedge(nw, to, 1);
lst = nw;
}
dfs_rebuild(to, cur);
}
}
bool vis[N << 1];
int Siz, mxSiz, rte, siz[N];
void find_root(int cur, int faa) {
siz[cur] = 1;
for (int i = head[cur]; i; i = e[i].nxt) {
int to = e[i].to; if (to == faa || vis[i]) continue;
find_root(to, cur); siz[cur] += siz[to];
int v = max(siz[to], Siz - siz[to]);
if (v < mxSiz) mxSiz = v, rte = i;
}
}
ll ans;
void dfs_work(int nwe) {
vis[nwe] = vis[nwe ^ 1] = true;
int u = e[nwe].to, v = e[nwe ^ 1].to;
//do something
if (siz[u] > 1) {
Siz = siz[u], mxSiz = tot;
find_root(u, v);
dfs_work(rte);
}
if (siz[v] > 1) {
Siz = siz[v], mxSiz = tot;
find_root(v, u);
dfs_work(rte);
}
}
int main() {
//input
tot = n;
dfs_rebuild(1, 0);
Siz = tot, mxSiz = tot;//bug
find_root(1, 0);
dfs_work(rte);
//output
return 0;
}
例题
其实随便挑个点分治题几乎都能作为边分治的例题。
最长道路tree
给定一棵
n 个点的树,求树上一条链使得链的长度乘链上所有点中的最小权值所得的积最大。其中链长度定义为链上点的个数。
n \le 50000
(显然可以并查集+树的直径,或者点分治+树状数组)
边分治+双指针。
虚点点权设为当前点点权,虚边边权设为 0,实边边权设为 1.
通道
给定点数均为
n 的三棵树,边权非负,求:\max_{u,v} \{ dis_1(u,v) + dis_2(u,v) + dis_3(u,v)\} 1 \le n \le 10^5,0 \le w \le 10^{12}
考虑两棵树怎么做。可以在第一棵树上边分治,然后在第二棵树上建虚树跑点带权的直径。复杂度:
于是三棵树就是边分治建虚树然后虚树上边分治再虚树
注意到边权非负,于是直径是可以快速合并的,于是只有两棵树的时候可以直接在第一棵树上 DFS 枚举 LCA,然后根据
三棵树就可以在第一棵树上边分治,第二棵树上虚树 DFS,第三棵树上维护直径。注意由于边分治要求必须是黑点和白点之间的链,维护第三棵树的直径的时候要分别维护黑直径和白直径。可以证明最长的黑白直径一定是由黑直径端点和白直径端点组成的。
复杂度:sort 了)
科技:边分树合并
我们将每次的重心边记录下来成为树形关系就形成了边分树。边分树与点分树类似,只不过还包含了一些边,类似克鲁斯卡尔重构树那样,叶子节点全为点,其余节点全为边。此外,还有一些有用的性质:
- 二叉树
- 树高为
O(\log)
这两个性质使得边分树能够像动态开点线段树那样进行合并。如果能保证合并前边分树的大小之和为
例题:暴力写挂
给两棵树
T,T' ,求depth(x) + depth(y) - (depth(LCA(x , y)) + depth′ (LCA′ (x, y))) n \le 366666
枚举
复杂度:
关键代码
inline void build_chain(int cur) {
int lstp = cur, lstt = ++ttot;
for (unsigned int _ = vec[cur].size() - 1; ~_; --_) {
++ttot;
son[ttot][type_dir[lstp]] = lstt;
mxv[lstt] = vec[cur][_] + val[cur];//bug
lstp = bianfath[lstp]; lstt = ttot;
}
rt[cur] = ttot;
}
int merge(int x, int y) {
if (!x || !y) return x^ y;
if (son[x][0] && son[y][1]) MAX(ans, mxv[son[x][0]] + mxv[son[y][1]] - ansdel);
if (son[x][1] && son[y][0]) MAX(ans, mxv[son[x][1]] + mxv[son[y][0]] - ansdel);
son[x][0] = merge(son[x][0], son[y][0]);
son[x][1] = merge(son[x][1], son[y][1]);
MAX(mxv[x], mxv[y]);
return x;
}
void dfs_on_tree2(int cur, int faa, ll nwd) {
MAX(ans,(val[cur] - nwd) * 2);
build_chain(cur);
for (unsigned int _ = 0; _ < ve[cur].size(); ++_) {
int to = ve[cur][_], w = v[cur][_];
if (to == faa) continue;
dfs_on_tree2(to, cur, nwd + w);
ansdel = nwd << 1;
rt[cur] = merge(rt[cur], rt[to]);//bug
}
}
科技:可持久化边分树
既然边分树和线段树有那么多相似之处,线段树可以可持久化,边分树也应该可以可持久化。
可持久化边分树可以用来当作树上的“主席树”。
例题:Can Bash Save the Day?
- `1 l r x`:求 $\sum_{i=l}^r dis(p_i,x)
2 x:交换p_x,p_{x+1} 1 \le n,q \le 2 \cdot 10^5
显然点分树可以轻松解决这种问题,拿两种线段树随便维护即可。然而空间是
还有一种树剖线段树的方法,其原理为:
然而空间都太大了,无法通过此题。但是那个主席树的方法的那个
考虑用边分树去做这道题。维护每个点到其各个边分树祖先的距离,主席(边分)树维护每个边分树节点的左右子树中到这个节点的距离和。交换操作与链剖方法一样,查询就在主席树上从到
时空复杂度:
关键代码
void rebuild(int cur, int faa)
void find_root(int cur, int faa)
int dep[N];
ll state[N];
vector<ll> vec[N];
void dfs_dis(int cur, int faa, ll nwd, int t) {
state[cur] |= (t << dep[cur]); ++dep[cur]; vec[cur].push_back(nwd);//bug
//DFS
}
void dfs_tre(int nwe) {
vis[nwe] = vis[nwe ^ 1] = true;
int u = e[nwe].to, v = e[nwe ^ 1].to;
dfs_dis(u, v, 0, 0), dfs_dis(v, u, e[nwe].val, 1);
//divide and conquer
}
int son[NN][2], rt[N], ttot, jzpsiz[NN];
ll sum[NN];
void add(int p, int nwd, int s, int lstcur, int &cur) {
cur = ++ttot; son[cur][0] = son[lstcur][0], son[cur][1] = son[lstcur][1];//BUG
if (nwd == dep[p]) return ;
add(p, nwd + 1, s >> 1, son[lstcur][s & 1], son[cur][s & 1]);
int sn = son[cur][s & 1], lsn = son[lstcur][s & 1];
sum[sn] = sum[lsn] + vec[p][nwd];
jzpsiz[sn] = jzpsiz[lsn] + 1;
}
void modify(int p, int nwd, int s, int t, int lstcur, int &cur) {//Bug
cur = ++ttot; son[cur][0] = son[lstcur][0], son[cur][1] = son[lstcur][1];
if (nwd == dep[p]) return ;
modify(p, nwd + 1, s >> 1, t, son[lstcur][s & 1], son[cur][s & 1]);
int sn = son[cur][s & 1], lsn = son[lstcur][s & 1];
sum[sn] = sum[lsn] + t * vec[p][nwd];
jzpsiz[sn] = jzpsiz[lsn] + t;
}
ll query(int p, int nwd, int s, int addcur, int delcur) {
if (nwd == dep[p]) return 0;
int asn = son[addcur][s & 1], dsn = son[delcur][s & 1];
ll res = query(p, nwd + 1, s >> 1, asn, dsn);
int dedir = (s & 1) ^ 1;
asn = son[addcur][dedir], dsn = son[delcur][dedir];
res += (sum[asn] - sum[dsn]) + vec[p][nwd] * (jzpsiz[asn] - jzpsiz[dsn]);
return res;
}
int p[N];
int main() {
//input
rebuild(1, 0);
Siz = tot, mxSiz = tot;
find_root(1, 0);
dfs_tre(rte);
for (int i = 1; i <= n; ++i)
add(p[i], 0, state[p[i]], rt[i - 1], rt[i]);
ll ans = 0;
for (int i = 1; i <= q; ++i) {
//input
if (t == 1) {
//input
ans = query(x, 0, state[x], rt[r], rt[l - 1]);
//output
} else {
//input
modify(p[x], 0, state[p[x]], -1, rt[x], rt[x]);
swap(p[x], p[x + 1]);
modify(p[x], 0, state[p[x]], 1, rt[x], rt[x]);
}
}
return 0;
}