前言
由于已经点明是点分治,所以我们在文章中约定,题解只叙述:求经过当前递归到的 \(x\) 对于答案的贡献,用以减少文章冗余程度。
如有错误,欢迎指出。
\(\texttt{1.[BJOI2017] 树的难题}\)
其实还是比较简单一题,做多了自然就会。
首先我们先 \(\texttt{dfs}\) ,在 \(x\) 的子树上处理一些与 \(x\) 有关的信息。(定义 \(c_x\) 表示颜色 \(x\) 的权值,对应题目中输入)
-
\(b_y\) 表示 \(y\) 对应 \(x\) 的哪一颗子树。
-
\(d_y\) 表示以 \(x\) 为起点,到 \(y\) 的路径所能产生的路径权值。
-
\(e_y\) 表示以 \(y\) 为起点,到达 \(x\) 的路径中,最后一条的颜色。
-
\(dep_y\) 表示 \(y\) 到 \(x\) 的距离。
这些应该可以通一次 \(\texttt{dfs}\) 得到。
然后我们按照点分治的常规思路,把 \(x\) 子树中所有的点储存在一个序列 \(a\) 中。
由于我们为了找到这样一条路径,需要合并两个 \(y_1,y_2\) 到 \(x\) 的路径并计算其权值,我们发现,如果 \(e_{y_1}\neq e_{y_2}\),则合并后的权值为 \(d_{y_1}+d_{y_2}\),否则,合并后的权值为 \(d_{y_1}+d_{y_2}-c_{e_{y_1}}\)。
我们发现,这个合并似乎与 \(e\) 数组是有关系的。
所以我们考虑把所有 \(e\) 相同的 \(a\) 中的元素放在一起。
首先计算不同的 \(e\) 之间可以贡献的最大路径权值。可以发现,他们与其他的元素合并路径时,只要满足 \(b_{y_1}\neq b_{y_2},dep_{y_1}+dep_{y_2}\in[l,r]\),那么就可以直接得到他们分别路径权值加起来的贡献。
显然,第一个限制,既然你 \(e\) 都不同了,那么 \(b\) 也显然不同。
由于还有后面那个限制,所以我们考虑每枚举一种 \(e\),先暴力查询满足 \(\in[l,r]\) 的 \(dep\) 中最大 \(d\),然后把他们的 \(dep\) 当作下标插入这颗线段树,就解决了 \(e\) 不同的情况对答案的贡献。
然后看 \(e\) 相同的时候。
这个时候,我们考虑在内部按照 \(b\) 进行分组,相同的在一组,显然,只有组别不同的两个才能对答案产生贡献。然后我们就可以按照 \(e\) 不同如何求解的思路,在内部再开一颗线段树,算完一块 \(b\) 的答案,就全部插入,避免相同的 \(b\) 的干扰,合并时,记得做减法。
最后取个 \(\texttt{max}\) 即可。
单层内部操作,由于排序和权值线段树,为 \(\mathcal{O} (n\log n)\)。故总时间复杂度 \(\mathcal{O}(n\log^2n)\),但是因为线段树的存在,常数巨大。
\(\texttt{2.[COCI2018-2019] Transport}\)
这个题目难点同样在合并上。
仍然,按照点分治基本的合并思路,我们需要对 \(x\) 子树上的 \(y\) 维护一些东西。
-
\(b_y\) 表示 \(y\) 对应 \(x\) 的哪一颗子树。
-
\(pre_y\) 表示从 \(x\) 开始走,走到 \(y\) 时,初始至少需要多少油才能走到。这个其实比较好维护,我们转化一下,就是去求你现在缺的油,和以前缺的油的最大值,维护一个前缀 \(\texttt{max}\) 即可。
-
\(left_y\) 表示从 \(y\) 开始走,走到 \(x\),不管中间有没有把油耗完,到达之后所剩的油。(就不管不能为负数的条件,硬着头皮开过去还剩多少,当然可能是负数)
-
\(held_y\) 表示从 \(y\) 开始走,能否走到 \(x\),能为 \(1\),否则为 \(0\)。我们考虑这个怎么维护。其实可以在递归搜索的时候再传一个参数 \(z\),表示一路下来的这条链中,每个点走回去所需要的油的最大值。假设从 \(y\) 开始向上走了一条路后所剩的油为 \(S\) (\(S\) 可能为负数。),那么接着向下传就是 \(\min\{0,y\}+S\)。(感觉这个虽然有点抽象,还是可以理解的。)然后,每次遍历到一个 \(y\) 时,就看 \(z\) 是否大于等于 \(0\) 来判断是否可以走回去。
那么有了这些东西之后,问题就简单很多了。
可以发现,如果要让一条路径合法,那就是让要合并的 \(y_1,y_2\)(假设是从 \(y_1\) 到 \(y_2\)) 满足 \(held_{y_1}=1,left_{y_1}\ge pre_{y_2},b_{y_1}\neq b_{y_2}\) 即可。
那显然有一种思路是,我们维护两个数组 \(a_1,a_2\),一个用于存放子树中所有的点,另一个存放 \(held_{y}=1\) 的点,显然起点肯定都在 \(a_2\) 中有储存,然后我们看 \(a_1\) 中有多少个能成为对应的终点即可。
可以用数据结构,但是我选择排序 \(+\) 双指针。
我们将 \(a_1\) 按照 \(pre\) 从小到大排序,\(a_2\) 按照 \(left\) 从小到大排序。我们从头到尾枚举 \(a_2\),并维护一个指针 \(l=0\) 和一个数组 \(num_x\) 表示 \([1,l]\) 中 \(b_{a_{1_y}}=x\) 的个数。然后枚举的时候,如果 \(left_i \ge pre_l\) 那就把 \(l\) 向后移动,最后移动完毕之后,把答案加上 \(l-num_{b_i}\) 即可。
由于有子树内部的排序,复杂度为 \(\mathcal{O}(n\log^2 n)\)。
\(\texttt{3.Ridiculous Netizens}\)
科技题。
首先,我们仍然按照点分治的惯例,当然这里是求包含了 \(x\) 的合法的连通块的个数而已。
那么这里就变成了一个 \(x\) 的子树上的依赖背包问题,因为你为了选择 \(x\),那么你选了一个点 \(y\),那你就必须选择他的所有祖先。
这样应该可以打出一个 \(\mathcal{O}(nm^2\log n)\) 的一个树上背包,但是这样显然还不够,这个点分治的存在似乎是多造成了一个 \(\log\)。(之所以是平方,是因为你要合并)
所以我们考虑科技。
我们发现,对于一个数 \(Z\),对于所有的 \(Y\in[1,Z-1]\),\(\lfloor \dfrac{Z}{Y}\rfloor\) 的个数,似乎只有 \(\sqrt{Z}\) 以内个。
也就是说,我们的状态存在冗余。
所以我们考虑重新定义状态。
首先,对于子树内部乘积 \(\le \sqrt{m}\) 的 \(dp\) 状态,我们专门储存,然后对于乘积 \(>\sqrt{m}\) 的部分,我们储存 \(\lfloor\dfrac{m}{j}\rfloor\)。
然后,我们只需要对所有有效的状态进行转移,时间复杂度来到 \(\mathcal{O}(nm\log n)\)。
然后我们考虑如何把背包的平方去掉。
由于使用了点分治,所以我们不关心过每个子树树根的连通块,只关心过点分中心(重心)的连通块,这时修改背包的定义为:包含重心的连通块延伸到子树内的种类数。
感觉这一步有点抽象啊。
然后在代码实现上,我们发现,这样每次合并的时候,只需要 \(dp_{x,j}\) 加上 \(dp_{son_x,j}\),因为不需要考虑当前的贡献了。当然,还有一步,就是你需要把父节点的答案传到下面去,故有 \(dp_{x,j\times w_x}\) 加上 \(dp_{fa_x,j}\)。
感觉还是不太清楚啊,看一看部分代码。
void dfs(int x, int last)
{
siz[x] = 1;
for (int i = 1; i <= klen * 2; ++i) dp[x][i] = 0;
for (int i = 1; i <= klen; ++i) dp[x][get(i * c[x])] += dp[last][i], dp[x][get(i * c[x])] %= mod;
for (int i = c[x]; i <= klen; ++i) dp[x][klen + i / c[x]] += dp[last][klen + i], dp[x][klen + i / c[x]] %= mod;
for (int i = fst[x]; i; i = arr[i].nxt)
{
int j = arr[i].tar;
if(vis[j] || j == last) continue;
dfs(j, x);
siz[x] += siz[j];
for (int i = 1; i <= klen * 2; ++i) dp[x][i] += dp[j][i], dp[x][i] %= mod;
}
}
然后每次把 \(x\) 所有的答案加起来即可。
\(\texttt{4.[联合省选模拟] 感染}\)
还是神仙题,点分治优化建图是个什么玩意。。
不难有一个思路,就是我们把所有的 \(x\) 与所有 \(D(x,j) \le r_x\) 的 \(j\) 连一条边,然后跑一遍缩点,然后看入度为 \(0\) 的点有多少个。
但是显然,这么暴力建边根本就建不完。
所以接下来,我们考虑如何对这个建边进行优化。
首先,我们显然维护一个 \(d_y\) 表示当前点分治递归到 \(x\) 时,\(y\) 到 \(x\) 的距离。
然后,我们显然可以对 \(x\) 子树内所有的节点按照 \(d_y\) 从小到大排序。显然,\(y_1\) 能向 \(y_2\) 连边,必然满足 \(d_{y_1}+d_{y_2}\le r_{y_1}\)。
我们可以从头到尾枚举排完序后的子树,然后二分查找到最后一个满足条件的,然后与 \([1,pos]\) 全部连一条边。这个看似可以运用线段树优化建图解决,但是时空都不太可观。而且光是这个二分查找就已经没救了。
所以我们考虑换一种思路,并把这个二分查找去掉。
我们把子树中的所有节点再开一数组个存下来,并把这个数组按照 \(r_y-dep_y\) 从小到大排序,这样我们就可以维护一个指针,从 \(0\) 开始,指向最开始那个按照距离排序的数组。这样我们就不用二分查找了,\([1,l]\) 既是我们想要连的边。
可以发现,这是一个前缀。
我们考虑对于最开始按照距离排序的子树每个点开一个虚点 \(p_i\),表示 \([1,i]\) 这一段区间的点的总和。显然有 \(p_i\) 与 \(p_{i-1}\) 连边,\(p_i\) 与 \(i\) 在子树中对应的点连边。然后,每次在第二个数组中枚举时,将其对应的点与 \(p_l\) 连边,这样递归完毕就可以实现建图。
有点抽象,具体代码长这样:
stable_sort(a + 1, a + tot + 1, cmp);
stable_sort(A + 1, A + tot + 1, cmp2);
for (int i = 1; i <= tot; ++i) num[i] = ++gnow, adds(num[i], a[i]);
for (int i = 2; i <= tot; ++i) adds(num[i], num[i - 1]);
int l = 0, bj = 0;
for (int i = 1; i <= tot; ++i)
{
while(l < tot && dep[a[l + 1]] + dep[A[i]] <= c[A[i]]) ++l;
if(l) adds(A[i], num[l]);
}
然后,我们直接跑一遍缩点,并把所有含有 实点 的强连通分量当作一个黑点,否则当作一个白点。
那么问题也就转化成了,如何选择最少的黑点,使得能够通过这些黑点,到达其他所有的黑点。
其实非常简单,我们可以把所有入度为 \(0\) 的白点全部删掉,删完之后可能会产生新的,那就继续删。这一步可以通过拓扑排序实现。
然后,剩余的点中,入度为 \(0\) 的黑点的个数,即为答案。
点分治内部,存在排序,复杂度为 \(\mathcal{O}(n\log^2 n)\),对于缩点,由于边和点的级别都在 \(n\log n\) 级别,故复杂度为 \(\mathcal{O}(n\log n)\)。
故总复杂度为 \(\mathcal{O}(n\log ^2n)\)。
\(\texttt{5.[重庆省队互测] 路径中位数}\)
其实也蛮简单的,看到中位数最大,直接二分答案,将 \(a_x\ge mid\) 的看作 \(1\) 否则看作 \(-1\)。
于是转化为能否找到一条合法的路径,使得路径上的点权和 \(\ge 0\)。
这不需要教了吧,直接在点分治递归内部维护每个点到 \(x\) 的距离,然后按照这个从小到大排序,然后通过双指针找到每个点对应的另外一条合法的路径中,最大的路径点权和即可。
最后看这个时候是否 \(\ge 0\)。
时间复杂度,二分,排序,点分治, \(\mathcal{O} (n\log ^2 n \log V)\)。
离散化之后,可以做到 \(\mathcal{O} (n\log^3 n)\),看似不太可过,但是加上一些剪枝和点分不一定卡的满,也就毫无压力了。
\(\texttt{6. [USACO12NOV] Balanced Trees G}\)
做完之后,非常想骂人的一道题目。
首先先考虑如何合并两条路径使得括号匹配。(假定左括号对应点权为 \(1\),右括号对应点权为 \(-1\)。)
接着在想,哪些路径能从上到下,哪些能从上到下。
于是就有了我们要维护的一些东西:
-
\(sum_y\) 表示从 \(x\) 走到 \(y\) 这一路上的点权和。
-
\(up_y\) 表示从 \(y\) 走到 \(x\),一路上点权和的最小值。(就是每到一个点取一个最小值)
-
\(down_y\) 表示从 \(y\) 走到 \(x\),一路上点权和的最小值。(与上面类似)
这样其实就可以完全判断了。对于 \(y\),如果他能从下到上,至少要满足 \(up_y\ge 0\),如果他能从上到下,必须满足 \(down_y \ge 0\)。
然后对于分别满足从上到下,从下到上的两个 \(y_1,y_2\),只需要满足 \(sum_{y_1}+sum_{y_2}-a_x=0,b_{y_1}\neq b_{y_2}\),就可以构成一个合法的括号序列。(这里要减去 \(a_x\) 是因为两个 \(sum\) 都把他囊括了进去。)
然后我们考虑如何对两条路径计算那个什么最大嵌套。
转化一下,就是计算这条路径上,每个点前缀点权和的最大值。
于是我们继续定义两个东西:
-
\(lc_y\) 表示,从 \(y\) 走到 \(x\) 的前缀点权和的最大值。
-
\(rc_y\) 表示,从 \(x\) 走到 \(y\) 的前缀点权和的最大值。
于是,对于我们刚刚定义的两个 \(y_1,y_2\),其答案就是 \(\max \{lc_{y_1},sum_{y_1}+rc_{y_2}\}\)。
至于怎么求最大值,我们考虑直接按照 \(sum_y\) 从小到大排序,然后双指针处理即可。
上面那 \(5\) 个数组求法,应该与文章中的第二题大同小异,这里就不阐述了。
以及,记得讨论从 \(x\) 直接向下的一条链的这种情况。
维护的东西是真多。。
\(\texttt{7.[VK Cup 2015] Logistical Questions}\)
求导神仙题。
我们定义两个点的距离为 \(d(x,y)\)。(是题目中对应的那个距离,不是我们平常说的距离)
为了方便后文的叙述,\(x,y\) 不一定是一个确切的点,有可能是边上的任意一个点。
我们仔细观察这个 \(d\),发现对于树上的任意一点 \(x\)(这个点可能是边上的一个点),\(d(x,i)\)(\(i\) 是自变量)是一个下凸函数。
于是,我们对于一个点 \(x\) 的答案 \(\sum d(x,i)\) 也是一个下凸函数。
故,这个最小的 \(x\) 是唯一的。(因为有可能在边上)
我们考虑去缩小这个 \(x\) 的范围。
最开始肯定是 \(\in [1,n]\)。
然后,我们从根节点的儿子看去,假设根节点的答案为 \(S\)。由于是唯一的且下凸,那么显然最多只有一个儿子的答案是 \(\le S\) 的。
我们考虑一个变化量的问题。
如果从根节点 \(x\) 向他的儿子 \(y\) 移动了一个极小的距离 \(\Delta\),那么此时的点就在他们两个的那条边上,而此时的答案变化量为(我们始终只关心这个变化量的正负用以精准找到那个子树):
\[\sum_{i\notin tree_y}\left( (\text{dist}(i,x)+\Delta)^{1.5} - \text{dist}(i,x)^{1.5})\right) \times w_i+\sum_{i\in tree_y}\left( (\text{dist}(i,x)-\Delta)^{1.5} - \text{dist}(i,x)^{1.5})\right) \times w_i \]如果我们把 \(\text{dist}(i,x)\) 当作一个自变量 \(t\),定义 \(f_i(t)=t^{1.5}\times w_i\)。
那么我们发现,如果上面那个式子在下面除以一个 \(\Delta\),那就变成了一堆 \(f_i\) 的导数。
而我们显然知道,对于 \(f_i(t)\),其导数为 \(f_i'(t)=1.5\times \sqrt{t}\times w_i\)。
因为这个除下去的 \(\Delta\) 与正负号无关,故上面那个式子的正负就等于 \(\sum_{i\not \in tree_y}f_i'(\text{dist}(i,x))-\sum_{i\in tree_y}f_i'(\text{dist}(i,x))\)
而我们可以直接预处理出所有的 \(\sum_{i\in tree_y}f_i'(\text{dist}(i,x))\),来实现判断正负并精准找到要递归的子树。(也就是是负号的那个)
当然,我们每递归一次都要求一个答案,因为有可能他自己就是最大值。
但是这样的话,我们的复杂度就不对,假设递归层数为 \(T\),由于求一次答案复杂度为 \(\mathcal{O}(n)\),则总复杂度为 \(\mathcal{O}(Tn)\),显然不对。
所以我们根据点分治的思想,每次移动到一颗子树时,我们只知道答案在这颗树里面,所以我们并不关心是从那里开始的。所以就可以从这颗子树的重心开始讨论,由于判断正负的预处理只需要子树级别,就可以做到 \(\mathcal{O}(n\log n)\)。
后记
总的来说,淀粉质的题目其实分为三类。
-
通过在点分树上维护大量信息,来得到路径合并求权值,和算对最终答案贡献的目的。
-
通过点分树的,“必须包含 \(x\)” 的限制,借以完成对于时间复杂度的降低,例如树上依赖背包。
-
通过点分树深度为 \(\log n\) 的限制,每次精准的找到向下递归的点,从而保证时间复杂度。