点分治
点分治
点分治完全可以被称为一种优雅的暴力。
引入
统计一棵树上距离等于 \(k\) 的点对数量。
容易想到从上到下枚举每一个节点 \(u\),合并 \(u\) 子树内的答案与经过 \(u\) 的答案,在极限情况下(链),时间复杂度达到 \(O(n^2)\)。
为什么会超时,显然对于每一个深度,它枚举的 \(n\) 个点,而一共会枚举 \(n\) 个深度。
考虑对深度进行优化(事实上优化不了前点数),让每一层都很“均匀”,很容易想到重心。
依据重心分割子树求解,这就是点分治的本质。
时间复杂度
类比启发式合并,这样的”启发式分裂“,时间复杂度证明类似。
依据重心的定义,设母树大小 \(sz_u\) ,每一次分割出的子树大小最大为 \(\cfrac{sz_u}{2}\),即最多分割 \(log_2n\) 层。
时间复杂度为 \(O(nlogn)\)。
实现
点分治主函数
这是点分治的模板,几乎不会变。
void dfs(int u) { // 点分治主函数
vis[u] = 1,calc(u); // 统计以u为根的子树的答案
for (edge e : G[u]) {
int v = e.to;
if (vis[v]) continue;
get_sz(v,0); // 取得子树大小
sum = sz[v],rt = 0;
getCentroid(v,u); // 找重心
dfs(rt);
}
}
找重心
采用树形DP的方式解决(严格重心)。也可以从根向下沿着 \(sz\) 大的子树找(不一定是严格意义的重心,但时间复杂度也有保证)。
int sz[maxn + 5],mx[maxn + 5],sum = 0;
void getCentroid(int u,int fa) { // 从 u 出发的分割子树的重心
mx[u] = 0,sz[u] = 1;
for (edge e : G[u]) {
int v = e.to;
if (v == fa || vis[v]) continue;
getCentroid(v,u);
sz[u] += sz[v];
mx[u] = max(mx[u],sz[v]);
}
mx[u] = max(mx[u],sum - sz[u]);
if (mx[u] < mx[rt]) rt = u;
}
统计函数
这是所有点分治问题中最重要的一环(类比线段树中 push_up() 的地位),以下以统计等于 \(k\) 的路径条数为例。
int ans = 0; // 答案
int f[maxn + 5];
int depth[maxn + 5]; // 子树内每个节点到达u的路径长度(子树内深度)
int dis[maxn + 5],cnt = 0;
int cur[maxn + 5],tot = 0;
void getDis(int u,int fa) { // 计算到达 u 的路径->dis[v]
for (edge e : G[u]) {
int v = e.to,w = e.w;
if (vis[v] || v == fa) continue;
depth[v] = depth[u] + w;
dis[++ cnt] = depth[v];
getDis(v,u);
}
}
int tmp[maxd + 5]; // 在子树[1,i-1]中距离为j的子树有多少
void calc(int u) { // 统计经过重心u的合法路径
tot = 0; tmp[0] = 1; // 端点为u
for (edge e : G[u]) {
int v = e.to,w = e.w;
if (vis[v]) continue;
depth[v] = w,cnt = 0;
getDis(v,u);
for (int i = 1;i <= cnt;i ++) if (k >= dis[i]) ans += tmp[k - dis[i]]; // 匹配
for (int i = 1;i <= cnt;i ++) cur[++ tot] = dis[i],tmp[dis[i]] ++;
}
for (int i = 1;i <= tot;i ++) tmp[cur[i]] = 0;
}
这是不用容斥的写法,按顺序枚举子树,天生不重复。
注意到清空历史路径长度统计数组 tmp 时进行优化,只清除使用过的点 cur。

浙公网安备 33010602011771号