题解 P4178 【Tree】

Nickel_Angel

2019-10-06 13:58:32

Solution

### 这里要介绍一下不依靠容斥来避免点分治统计答案重复的方法 (下面简略介绍一下点分治算法的主要思想QwQ) 这个算法主要采用分治的思想:首先在树中选取一个节点作为树根,树根将这棵树分成了几个子树,我们统计这棵树树根对答案的贡献后,我们再用同样的方法递归处理树根的每一颗子树。为了使树分治出的子树尽量平均,尽可能最小化递归层数,我们每次选取当前树的重心作为根节点递归下去,保证递归层数在 $O(\log n)$ 级别。 本题要统计的是树上的路径,考虑树上路径分为经过根节点和不经过根节点两类。 故可以考虑在分治过程中,统计当前子树经过根节点的路径中,长度小于等于 $k$ 的路径即可。而对于那些不经过当前根节点的路径,我们将它们交给当前子树的子树处理。具体一点来说,可以考虑将经过根节点设其两段点为 $(x, y)$ 的路径拆分成两部分:$(root, x)$ 和 $(root,y)$,我们可以通过树形 dp 预处理出子树内每个点到根的距离 $dis[u]$,这样我们只需统计子树内点对 $x, y$ 满足 $dis[x] + dis[y] \leq k$ 的点对数量即可。 但这样有可能会统计到一些不符合要求的点对,即 $x, y$ 可能都在 $root$ 的同一棵子树中(如下图,$x, y$ 两点均在 $root$ 的黄色子树中),若此时也满足 $dis[x] + dis[y] \leq k$,则该点对也会被计入答案。而 $x,y$ 组成的路径显然没有经过根节点,故这条路径实际上是不合法的。我们需要考虑如何去掉这些不合法的路径。 ![](https://cdn.luogu.com.cn/upload/image_hosting/o6beyy07.png) 现在正题开始!qwq 看到大家一般都使用的是容斥法处理的以上情况,即每次在统计当前子树根节点的答案后,再减去根节点的各个子节点的子树中满足条件的点对。这样可能会导致常数较大, 故这里介绍另外一种常数较小但细节相对较多的方法:**染色法**。 考虑我们在一次分治一个子树过程中,我们已经找到了其根节点 $root$,每个节点到根节点的距离 $dis[u]$,每个节点在 $root$ 哪一个**子节点**的子树中 $color[u]$(特别的,$color[root] = root$) 。 有一个显然的性质是,一条路径经过 $root$,当且仅当路径的两端点 $u, v$ 满足 $color[u] \neq color[v]$ 。 我们先将树中的节点都取出放至数组中,按照它们到根节点的距离($dis[u]$)从小到大排序。排完序后,我们只需用两个指针 $i,j$,一个从前开始,一个从后开始扫描数组就可以计算出 $dis[x] + dis[y] \leq k$ 的点对 $(x,y)$ 的个数,其中 $x, y$ 分别是指针 $i,j$ 所指向的节点。 具体计算过程如下: 1、将左指针从左向右扫描,对每个点计算合法的路径条数。不难发现,在左指针扫描过程中,恰好使得 $(x, y)$ 满足 $dis[x] + dis[y] \leq k$ 的右指针位置是单调递减的。 2、对于如何去掉不合法的点对,我们可以维护一个数组 $f[i]$ 表示在左右两指针中间的点(包括左右两指针指向的点)中,满足 $color[u] = i$ 的点的个数。不难发现只有在左右指针移动时,$f$ 数组中的值才会发生变化,故 $f$ 数组可以在指针移动时直接维护。 3、设当前左指针指向的点为 $x$,则 $x$ 对答案的贡献为**左右指针中间的点的个数**减去**在这些点中与 $x$ 在 $root$ 相同子树中点的个数(即满足 $color[u] = color[x]$ 的点 $u$ 的个数)**,即 $j - i + 1 - f[color[x]]$ 。 显然上述排序时间复杂度为 $O(n \log n)$,扫描统计答案时间复杂度为 $O(n)$ 。 综上,结合 master 定理,本算法时间复杂度为 $O(n \log^2 n)$ 。 Code: ```cpp #include <cstdio> #include <cstring> #include <iostream> #include <algorithm> using namespace std; namespace Primary { const int maxn = 40010; int n, m, dp[maxn], size[maxn], dis[maxn], help[maxn]; int head[maxn], to[maxn << 1], next[maxn << 1], val[maxn << 1], tot = 1; int root, sum, cnt, ans = 0, color[maxn], f[maxn]; bool vis[maxn]; bool cmp(int x, int y) {return dis[x] < dis[y];} inline void add_edge(int u, int v, int w) { to[++tot] = v; val[tot] = w; next[tot] = head[u]; head[u] = tot; } void getroot(int u, int pre)//树形 dp 求树的重心 { size[u] = 1, dp[u] = 0; for (int c = head[u], v; c; c = next[c]) { v = to[c]; if (v == pre || vis[v]) continue; getroot(v, u); size[u] += size[v]; dp[u] = max(dp[u], size[v]); } dp[u] = max(dp[u], sum - size[u]); if (dp[u] < dp[root]) root = u; } void getdis(int u, int pre)//对于当前子树预处理 dis, color, f 等数组 { help[++cnt] = u;//将当前子树节点编号均放入 help 数组中 color[u] = color[pre], ++f[color[u]]; for (int c = head[u], v; c; c = next[c]) { v = to[c]; if (v == pre || vis[v]) continue; dis[v] = dis[u] + val[c]; getdis(v, u); } } inline int calc(int u) { dis[u] = 0, cnt = 0; help[++cnt] = u; color[u] = u, f[color[u]] = 1;//注意预处理细节 for (int c = head[u], v; c; c = next[c]) { v = to[c]; if (vis[v]) continue; color[v] = v, dis[v] = val[c]; f[color[v]] = 0;//注意清空 f 数组 getdis(v, v); } sort(help + 1, help + cnt + 1, cmp);//将节点按 dis 从小到大排序 int i = 1, j = cnt, res = 0; while (i < j) { while (i < j && dis[help[i]] + dis[help[j]] <= m) { res += j - i + 1 - f[color[help[i]]]; --f[color[help[i]]];//指针移动时直接维护 f 数组,有些类似于莫队 ++i; } --f[color[help[j]]]; --j; } return res; } void solve(int u)//主要分治函数 { vis[u] = true; ans += calc(u); for (int c = head[u], v; c; c = next[c]) { v = to[c]; if (vis[v]) continue; sum = size[v] > size[u] ? n - size[u] : size[v];//计算下层递归子树大小 root = 0, dp[0] = n; getroot(v, u);//每次求出重心后继续分治 solve(root); } } void main() { scanf("%d", &n); for (int i = 1, u, v, w; i < n; ++i) { scanf("%d%d%d", &u, &v, &w); add_edge(u, v, w), add_edge(v, u, w); } scanf("%d", &m); dp[0] = sum = n; getroot(1, 0); solve(root); printf("%d\n", ans); } } int main() { Primary::main(); return 0; } ```