点分治学习笔记
点分治适合处理大规模的树上路径信息问题。
我们每次选定一个重心作为分治中心,然后统计经过分治中心的答案,再将树用分治中心分成若干个连通块进行分治。由于每次树的大小不超过原来的 \(1/2\),复杂度是 \(O(n \log n)\) 的。
「WC2010」重建计划
题意:给定一棵 \(n\) 个节点的树,每条边有一个价值 \(v_i\)。要求在树中找一条简单路径,路径的边数在 \([L,U]\) 之间,使得路径上边权的平均值最大,输出这个最大平均值(保留三位小数)。
这是一个 分数规划+点分治 的题,比较板。直接二分答案,对于每个 \(mid\),我们让边权减去 \(mid\),只需要检查是否又树上路径满足:长度在 \([L,U]\) 之间,边权和大于0。
考虑点分治,我们每次枚举到一个重心,用 \(bfs\) 来处理每个点的路径长度和权值和,因为bfs是按照点到重心的长度严格不降的,我们处理的同时吧这个序列记下来,记为 \(h\) 。这样降序枚举其中一个端点到重心的长度时,另一个端点到重心的长度有一个区间 \([tl, tr]\), \(tl,tr\) 是单调不降的,因此在 \(h\) 上形成一个滑动窗口,而我们需要的只有这个窗口里权值的最大值,容易用单调队列维护。
所以我们只需要点分治“二分次数”次,一个技巧是先预处理出点分治的重心序列,能够大大减少常数。复杂度时 \(O(n \log n \times 二分次数)\) 的。
#include<bits/stdc++.h>
#define int long long
#define rep(i, l, r) for(int i = (l); i <= (r); ++ i)
#define per(i, r, l) for(int i = (r); i >= (l); -- i)
#define ill __int128
#define fi first
#define se second
#define endl '\n'
#define pii pair<int, int>
#define pb push_back
using namespace std;
const int N = 2e5 + 10;
const double eps = 1e-15;
int tot, to[N], nxt[N], head[N];
double val[N];
int n, L, U;
int num[N], sz[N], h[N], q[N], last, top, mp[N], vis[N];
double dis[N], f[N];
int cnt, flag;
int len[N];
void add(int u, int v, double w) {
++ tot;
to[tot] = v;
val[tot] = w;
nxt[tot] = head[u];
head[u] = tot;
}
int mn, mx, now, rt;
void dfs(int u, int fa) {
int mx = 0;
sz[u] = 1;
for(int i = head[u]; i; i = nxt[i]) {
int v = to[i];
if(vis[v] || v == fa) continue;
dfs(v, u);
sz[u] += sz[v];
mx = max(mx, sz[v]);
}
mx = max(mx, now - sz[u]);
if(mx < mn) {
mn = mx;
rt = u;
}
}
int find_rt(int u) {
mn = 1e18;
dfs(u, 0);
return rt;
}
void init(int x) {
int u = find_rt(x);
num[++ num[0]] = u; vis[u] = 1;
for(int i = head[u]; i; i = nxt[i]) {
int v = to[i];
if(! vis[v]) {
now = sz[v];
init(v);
}
}
}
void bfs(int s) {
mp[h[++ top] = s] = 1;
for(int i = last + 1; i <= top; ++ i) {
int u = h[i];
for(int j = head[u]; j; j = nxt[j]) {
int v = to[j];
if(vis[v] || mp[v]) continue;
mp[v] = 1; h[++ top] = v;
dis[v] = dis[u] + val[j];
len[v] = len[u] + 1;
}
}
rep(i, last + 1, top) mp[h[i]] = 0;
}
void check() {
int l = 1, r = 0, pos = last + 1;
per(i, min(U, len[h[top]]), 0) {
int tl = (i >= L ? 0 : L - i), tr = U - i;
while(l <= r && len[q[l]] < tl) ++ l;
while(pos <= top && len[h[pos]] < tl) ++ pos;
while(pos <= top && len[h[pos]] <= tr) {
while(l <= r && dis[q[r]] + eps < dis[h[pos]]) -- r;
q[++ r] = h[pos];
++ pos;
}
if(l <= r && dis[q[l]] + f[i] >= -eps) {
flag = 1;
return ;
}
}
}
void solve(int x) {
int u = num[++ cnt];
vis[u] = 1;
h[0] = u;
f[0] = len[u] = dis[u] = 0;
top = 0;
for(int i = head[u]; i; i = nxt[i]) {
int v = to[i];
if(vis[v] == 0) {
dis[v] = val[i];
len[v] = 1;
last = top;
bfs(v);
check();
rep(j, last + 1, top) {
f[len[h[j]]] = max(f[len[h[j]]], dis[h[j]]);
}
}
}
rep(i, 0, top) f[len[h[i]]] = - 1e18;
for(int i = head[u]; i; i = nxt[i]) {
if(! vis[to[i]]) solve(to[i]);
}
}
void Jail() {
cin >> n >> L >> U;
rep(i, 1, n - 1) {
int u, v;
double w;
cin >> u >> v >> w;
add(u, v, w);
add(v, u, w);
}
now = n;
init(1);
double l = 0, r = 1e6;
while(r - l > 1e-5) {
double mid = (l + r) / 2;
rep(i, 1, tot) val[i] -= mid;
rep(i, 1, n) vis[i] = 0;
flag = 0;
cnt = 0;
solve(1);
rep(i, 1, tot) val[i] += mid;
if(flag) l = mid;
else r = mid;
}
printf("%.3lf\n", r);
}
signed main() {
freopen("rebuild.in", "r", stdin); freopen("rebuild.out", "w", stdout);
// ios :: sync_with_stdio(0); cin.tie(0); cout.tie(0);
int T = 1;
// cin >> T;
while(T --) Jail();
return 0;
}
「JOISC 2020 Day4」首都
给定一棵树,每个点有颜色,找出一个最小的颜色集合,使得所有这些颜色的点构成一个连通块,求最小的颜色集大小。
P6329 【模板】点分树 / 震波
给你一棵 n 个节点的树,每个节点有一个权值。需要在线处理m 个操作,操作有两种:
- 以节点
x为圆心,距离不超过k的所有节点,求它们的权值之和。 - 把节点
x的权值修改为y。
- \(n, m \le 10^5\)
对于单次询问,我们可以用点分治解决,但面对修改操作,每次修改不现实。点分治的本质是递归找中心我们将每一层的重心与上一层的重心连起来,形成的树称为点分树。他的性质是树高严格 \(O(n \log n)\), 树上任意两点的 \(lca\) 一定位于原树中两点的路径上(因为点分治过程中,\(u,v\) 首次被分到不同子树中,这个重心就是他们的 \(lca\),这个重心显然在 \(u\) 到 \(v\) 的路径上)。因此,
这个性质看似很鸡肋,但是在树高 \(log\) 级别的情况下很多不对劲的暴力复杂度都是正确的。
比如这题,我们一直 \(x\), 设另一个端点时 \(y\), 直接枚举 \(z = lca(x,y)\), 由于上面的性质,
显然 \(y\) 的点集就是 \(z\) 为根时,扣掉 \(x\) 方向上的那棵子树的, 且和 \(z\) 的距离小于等于一个定值的点。
容易用线段树维护。
#include<bits/stdc++.h>
#define int long long
using namespace std;
#define endl '\n'
#define rep(i, l, r) for(int i = (l); i <= (r); ++ i)
#define per(i, r, l) for(int i = (r); i >= (l); -- i)
#define se second
#define fi first
#define pb push_back
#define pii pair<int, int>
const int N = 2e5 + 10;
vector<int> p[N];
int n, m;
struct tree {
int pa[N], sz[N], top[N], dfn[N], son[N], d[N];
int tot = 0;
void dfs1(int u, int fa) {
pa[u] = fa, sz[u] = 1; d[u] = d[fa] + 1;
for(auto v : p[u]) {
if(v == fa) continue;
dfs1(v, u);
sz[u] += sz[v];
if(sz[son[u]] < sz[v]) son[u] = v;
}
}
void dfs2(int u, int tp) {
dfn[u] = ++ tot;
top[u] = tp;
if(son[u]) {
dfs2(son[u], tp);
}
for(auto v : p[u]) {
if(v == pa[u] || v == son[u]) continue;
dfs2(v, v);
}
}
int lca(int x, int y) {
while(top[x] != top[y]) {
if(d[top[x]] < d[top[y]]) swap(x, y);
x = pa[top[x]];
}
return (d[x] > d[y] ? y : x);
}
} tr;
int rt, sum, mn;
int pa[N], vis[N], sz[N], a[N];
int cdis(int x, int y) {
return tr.d[x] + tr.d[y] - tr.d[tr.lca(x, y)] * 2;
}
vector<int> c[2][N];
void upd(int id,int op, int x, int w) {
x = min(x, sz[id] - 1);
++ x;
for(int i = x;i <= sz[id]; i += i & (- i)) c[op][id][i] += w;
}
int qry(int id, int op, int x) {
x = min(x, sz[id] - 1);
++ x;
int res = 0;
for(int i = x; i >= 1; i -= i & -i) res += c[op][id][i];
return res;
}
void find_rt(int u, int fa) {
sz[u] = 1;
int res = 0;
for(auto v : p[u]) {
if(v == fa || vis[v]) continue;
find_rt(v, u);
sz[u] += sz[v];
res = max(res, sz[v]);
}
res = max(res, sum - sz[u]);
if(res < mn) mn = res, rt = u;
}
void get_size(int u, int fa) {
sz[u] = 1;
for(auto v : p[u]) {
if(v == fa || vis[v]) continue;
get_size(v, u);
sz[u] += sz[v];
}
}
void dfs(int u) {
vis[u] = 1;
sz[u] = sum + 1;
c[0][u].resize(sz[u] + 1), c[1][u].resize(sz[u] + 1);
for(auto v : p[u]) {
if(vis[v] == 0) {
get_size(v, u);
sum = sz[v]; rt = 0; mn = 1e15;
find_rt(v, u);
pa[rt] = u;
dfs(rt);
}
}
}
void modify(int u, int w) {
for(int i = u; i != 0; i = pa[i]) {
upd(i, 0, cdis(u, i), w);
}
for(int i = u; pa[i] != 0; i = pa[i]) upd(i, 1, cdis(u, pa[i]), w);
}
void Jail() {
cin >> n >> m;
rep(i, 1, n) cin >> a[i];
rep(i, 1, n - 1) {
int u, v;
cin >> u >> v;
p[u].pb(v);
p[v].pb(u);
}
tr.dfs1(1, 0); tr.dfs2(1, 1);
sum = n, mn = 1e15;
find_rt(1, 0);
dfs(rt);
rep(i, 1, n) modify(i, a[i]);
int ans = 0;
while(m --) {
int op, x, y;
cin >> op >> x >> y;
x ^= ans, y ^= ans;
if(! op) {
ans = 0;
ans += qry(x, 0, y);
for(int i = x; pa[i]; i = pa[i]) {
int dis = cdis(x, pa[i]);
if(y >= dis) {
ans += qry(pa[i], 0, y - dis) - qry(i, 1, y - dis);
}
}
cout << ans << endl;
} else {
modify(x, y - a[x]); a[x] = y;
}
}
}
signed main() {
// freopen("a.in", "r", stdin);
// freopen("a.out", "w", stdout);
ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
int T = 1;
// cin >> T;
while(T --) Jail();
return 0;
}
边分治
考虑找一条边来使得分开的连通块大小的最大值最小。但是菊花图会被卡,那么我们类似线段树的建树方式把树转为二叉树,即三度化,这样的话只增加 \(\le n\) 个点,每个点度数 \(\le 3\),每次分开的最大值最大是 \(2n/3\),时间复杂度 \(\mathcal{O}(n \log_{3/2}n)\),常数较大,但有时候比点分治要好用。
P12703
边分治板子题,题目给出的图是一颗树加上叶子结点连成的环,我们先把树三度化,当前作为分治中心的边把图分为两个连通块,且两个连通块之间只有 \(3\) 条边连接:两条环边和作为分治中心的树边,那么将这 \(3\) 条边上的 \(\le 6\) 个点分别作为源点跑一遍最短路,再更新所有询问即可,时间复杂度 \(\mathcal{O}(n \log^2 n + q \log n)\)。代码我没写。
「ZJOI2015」幻想乡战略游戏
给定一棵树,点有点权 \(d\) ,边有边权 \(w\)。\(Q\) 次修改一个点的点权 ,查询这棵树的带权重心。
\(n,Q \le 10^5\), 树上每个点的度数不超过 \(20\),保证 \(w > 0\) 。
这是是我认为点分树里十分难的题,代码里的 \(query\) 函数是我理解了一个上午题解之后写的。
首先有一个巧妙的性质,假设当前强制 \(u\) 为根, \(v\) 是 \(u\) 的一个子节点,\(sum_i\) 表示子树 \(i\) 的点权之和,\(w\) 为边 \((u,v)\) 的权值,那么当根从 \(u\) 转移到 \(v\) 是,代价会减小的充要条件为:
显然满足条件的 \(v\) 只有一个。
暴力就是从原树上一个点开始跳儿子节点,单次询问 \(O(n)\),我们考虑用点分树优化这个过程,为点分树树高是 \(O(log)\) 的。但有一个麻烦就是点分树上的父子关系在原树上不一定成立,我们在点分树上维护每个点:
1. \(d_u\) :每个点子树内的 \(d\) 值。
2. \(sum_u\) :子树 \(u\) 的所有 \(d\) 之和。
3. \(f_u\) :如果u为根,则该值等于树中所有点 \(v\) 的 \(dis(v,u)×d_v\) 之和,否则等于u的子树内所有点\(v\) 的 \(dis(v,fa_u)×d_v\) 之和。
修改比较简单,在点分树上往上跳。
难点是询问。假设现在到了 \(u\) 的分治子树,显然我们在点分树上跳上面的那个条件仍然是对的。如果点分树上下一层没有节点满足 \(sum\) 值大于 \(u\) 的一半,则 \(u\) 此时就是带权重心。否则,设 \(v\) 是那个唯一满足条件的子节点,\(w\) 是原树上 \(u\) 在 \(v\) 方向的子节点,我们的思路是把点分树上 \(v\) 子树外点的贡献从对 \(u\) 的转移到对 \(w\) 的,再把这些点的 \(d\) 值累加到 \(w\) 上,同时更新一下 \(w\) 到 \(u\) 以前所有祖先的 \(sum\) 和 \(f\) ,然后问题变成在 \(v\) 子树内带权重心加上之前算出来的那些对 \(w\) 的额外贡献,递归求解就可以了。询问不是很好理解,下面是一个伪代码和我的 \(AC\) 代码。
/* ---------- 查询辅助函数:计算从点分树节点 u 转移到临界点 x 时,外部点(非 v 子树)的代价 ---------- */
ll exc(int u, int w, int x) {
// u: 当前点分树节点,w: 要转移到的子节点(点分树上的子节点),x: 对应的原树临界点
ll ans = 0, cnt = d[u]; // cnt 累计外部点的总权值,初始为 u 自身
Edge2(u) { // 遍历 u 的所有点分树子节点
if ((v = go2[e]) != w) { // 排除目标子节点 w,剩下的都是外部点所在的子树
ans += f[v]; // 这些子树内的点到 u 的距离和(因为 f[v] 是到 fa[v]=u 的距离)
cnt += sumd[v]; // 累加权值
}
}
d[x] += cnt; // 临时将外部权值加到临界点 x 上,以便子问w题能感知
// 外部点从 u 移到 x 的代价 = 它们到 u 的代价和 + 外部权值 * dist(u, x)
return ans + cnt * dist(u, x);
}
/* ---------- 查询主函数:递归求解最小代价 ---------- */
ll query(ll ans, int u) {
// ans: 当前已经累计的、不在 u 管辖子树内的点的代价
// u: 当前考虑的点分树节点
Edge2(u) { // 遍历点分树子节点
if ((sumd[v = go2[e]] << 1) > sumd[u]) { // 若某子节点权值超过一半,则最优解在该子树内
ll wr = d[sc2[e]]; // 保存临界点原来的 d 值
ll delta = exc(u, v, sc2[e]); // 计算外部点转移到临界点的代价,并临时将外部权值加到临界点
ll tmp = d[sc2[e]] - wr; // 新增的外部权值
// 沿临界点到 u 的父链(不含 u)更新 sumd 和 f,使子问题能正确统计外部点
for (int w = sc2[e]; w != u; w = fa[w]) {
sumd[w] += tmp;
f[w] += tmp * dist(sc2[e], fa[w] ? fa[w] : w);
}
// 准备递归进入子节点 v:计算 v 子树内所有点到 v 的代价和作为新的初始 ans
ll nans = 0;
for (int z = adj2[v]; z; z = nxt2[z])
nans += f[go2[z]]; // 累加 v 的各子树的 f(即这些子树点到 v 的距离和)
// 递归求解子问题,总代价 = 外部代价 delta + 子问题最优额外代价
ans = delta + query(nans, v);
// 恢复临时修改
for (int w = sc2[e]; w != u; w = fa[w]) {
sumd[w] -= tmp;
f[w] -= tmp * dist(sc2[e], fa[w] ? fa[w] : w);
}
d[sc2[e]] = wr; // 恢复临界点的 d
return ans; // 返回最终总代价
}
}
// 若没有子节点满足条件,则当前 u 即为最优补给点,返回已累计的代价
return ans;
}
\(AC\) 代码:
#include<bits/stdc++.h>
#define int long long
#define rep(i, l, r) for(int i = (l); i <= (r); ++ i)
#define endl '\n'
using namespace std;
const int N = 1e5 + 10, M = N << 1;
int n, q;
int tot, to[M], val[M], nxt[M], head[N];
int tot2, to2[M], sc2[M], nxt2[M], head2[N];
int f[N], sum[N], pa[N], d[N];
int fr[N], dep[N], a[M], m, dis[N], vis[N], sz[N];
int lg[M], st[M][20];
int dist(int u, int v) {
int l = fr[u], r = fr[v];
if(l > r) swap(l, r);
int k = lg[r - l + 1];
int x = st[l][k], y = st[r - (1 << k) + 1][k];
int lca = (dep[x] < dep[y]) ? x : y;
return dis[u] + dis[v] - (dis[lca] << 1);
}
void add(int u, int v, int w) {
to[++tot] = v; val[tot] = w; nxt[tot] = head[u]; head[u] = tot;
}
void add2(int u, int v, int w) {
to2[++tot2] = v; sc2[tot2] = w; nxt2[tot2] = head2[u]; head2[u] = tot2;
}
void dfs(int u, int fa) {
dep[u] = dep[fa] + 1;
a[fr[u] = ++m] = u;
for(int i = head[u]; i; i = nxt[i]) {
int v = to[i], w = val[i];
if(v == fa) continue;
dis[v] = dis[u] + w;
dfs(v, u);
a[++m] = u;
}
}
int rt, rt0, now;
void dfs1(int u, int fa) {
sz[u] = 1;
for(int i = head[u]; i; i = nxt[i]) {
int v = to[i];
if(v == fa || vis[v]) continue;
dfs1(v, u);
sz[u] += sz[v];
}
}
void dfs2(int u, int fa, int all) {
int mx = all - sz[u];
for(int i = head[u]; i; i = nxt[i]) {
int v = to[i];
if(v == fa || vis[v]) continue;
dfs2(v, u, all);
mx = max(mx, sz[v]);
}
if(mx < now) now = mx, rt = u;
}
void find_rt(int u) {
dfs1(u, 0);
now = 1e9;
dfs2(u, 0, sz[u]);
}
int build(int u, int fa) {
find_rt(u);
int tmp = rt;
vis[rt] = 1;
pa[rt] = fa;
for(int i = head[rt]; i; i = nxt[i]) {
int v = to[i];
if(vis[v]) continue;
int x = build(v, tmp);
add2(tmp, x, v);
}
return tmp;
}
void change(int x, int w) {
int pre = x;
d[x] += w;
while(x) {
sum[x] += w;
f[x] += dist(pre, pa[x] ? pa[x] : x) * w;
x = pa[x];
}
}
int exc(int u, int w, int x) {
int res = 0, s = d[u];
for(int i = head2[u]; i; i = nxt2[i]) {
int v = to2[i];
if(v == w) continue;
s += sum[v];
res += f[v];
}
d[x] += s;
return res + dist(u, x) * s;
}
int query(int ans, int u) {
for(int i = head2[u]; i; i = nxt2[i]) {
int v = to2[i], w = sc2[i];
if(sum[v] * 2 > sum[u]) {
int pre = d[w];
int dt = exc(u, v, w), t = d[w] - pre;
for(int x = w; x != u; x = pa[x]) {
sum[x] += t;
f[x] += dist(w, pa[x] ? pa[x] : x) * t;
}
int nans = 0;
for(int j = head2[v]; j; j = nxt2[j]) {
int x = to2[j];
nans += f[x];
}
ans = dt + query(nans, v);
for(int x = w; x != u; x = pa[x]) {
sum[x] -= t;
f[x] -= dist(w, pa[x] ? pa[x] : x) * t;
}
d[w] = pre;
return ans;
}
}
return ans;
}
signed main() {
// freopen("a.in", "r", stdin); freopen("a.out", "w", stdout);
ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
cin >> n >> q;
rep(i, 1, n - 1) {
int u, v, w;
cin >> u >> v >> w;
add(u, v, w);
add(v, u, w);
}
dfs(1, 0);
lg[0] = -1;
rep(i, 1, m) lg[i] = lg[i >> 1] + 1;
rep(i, 1, m) st[i][0] = a[i];
rep(j, 1, 18)
rep(i, 1, m - (1 << j) + 1) {
int x = st[i][j - 1], y = st[i + (1 << (j - 1))][j - 1];
st[i][j] = (dep[x] < dep[y]) ? x : y;
}
rt0 = build(1, 0);
while(q--) {
int x, y;
cin >> x >> y;
change(x, y);
cout << query(f[rt0], rt0) << endl;
}
return 0;
}
P2305 [NOI2014] 购票
讲 李超树 遇到过,但有一个点分治+斜率优化的优雅做法。
- 给你一棵以 \(1\) 为根的树,共 \(n\) 个节点。从任意节点 ( v ) 出发,要到达根节点 1。
- 每次可以 购买一张票,从当前节点跳到它的 任意祖先\(u\), 满足 \(dep_v - dep_u \le l_v\) 。且费用为:\(p_v \times (dep[v] - dep[u]) + q_v\)
- 可以多次购票,直到到达根。
- 求每个节点到达根的最小总费用。
首先有一个显然的 \(n^2\) 转移
在序列上可以用全局凸包维护,但树上就不行。
后面不会了,贴个标以后学。
「WC2018」通道
给定三棵树,边带正权,求点对 \(u,v\) 使得三棵树上 \(u\) 到 \(v\) 的边权和最大,求这个最大值。
very hard。
边分治+虚树,我不会。但是有一个随机做法,大概是随机一个根,用爬山或者迭代最大值的手段更新一个最大值,糖丸了。
正解待补。

浙公网安备 33010602011771号