点分治杂题总结

前言

由于已经点明是点分治,所以我们在文章中约定,题解只叙述:求经过当前递归到的 x 对于答案的贡献,用以减少文章冗余程度。

如有错误,欢迎指出。

1.[BJOI2017] 树的难题

其实还是比较简单一题,做多了自然就会。

首先我们先 dfs ,在 x 的子树上处理一些与 x 有关的信息。(定义 cx 表示颜色 x 的权值,对应题目中输入)

  • by 表示 y 对应 x 的哪一颗子树。

  • dy 表示以 x 为起点,到 y 的路径所能产生的路径权值。

  • ey 表示以 y 为起点,到达 x 的路径中,最后一条的颜色。

  • depy 表示 yx 的距离。

这些应该可以通一次 dfs 得到。

然后我们按照点分治的常规思路,把 x 子树中所有的点储存在一个序列 a 中。

由于我们为了找到这样一条路径,需要合并两个 y1,y2x 的路径并计算其权值,我们发现,如果 ey1ey2,则合并后的权值为 dy1+dy2,否则,合并后的权值为 dy1+dy2cey1

我们发现,这个合并似乎与 e 数组是有关系的。

所以我们考虑把所有 e 相同的 a 中的元素放在一起。

首先计算不同的 e 之间可以贡献的最大路径权值。可以发现,他们与其他的元素合并路径时,只要满足 by1by2,depy1+depy2[l,r],那么就可以直接得到他们分别路径权值加起来的贡献。

显然,第一个限制,既然你 e 都不同了,那么 b 也显然不同。

由于还有后面那个限制,所以我们考虑每枚举一种 e,先暴力查询满足 [l,r]dep 中最大 d,然后把他们的 dep 当作下标插入这颗线段树,就解决了 e 不同的情况对答案的贡献。

然后看 e 相同的时候。

这个时候,我们考虑在内部按照 b 进行分组,相同的在一组,显然,只有组别不同的两个才能对答案产生贡献。然后我们就可以按照 e 不同如何求解的思路,在内部再开一颗线段树,算完一块 b 的答案,就全部插入,避免相同的 b 的干扰,合并时,记得做减法。

最后取个 max 即可。

单层内部操作,由于排序和权值线段树,为 O(nlogn)。故总时间复杂度 O(nlog2n),但是因为线段树的存在,常数巨大。

2.[COCI2018-2019] Transport

这个题目难点同样在合并上。

仍然,按照点分治基本的合并思路,我们需要对 x 子树上的 y 维护一些东西。

  • by 表示 y 对应 x 的哪一颗子树。

  • prey 表示从 x 开始走,走到 y 时,初始至少需要多少油才能走到。这个其实比较好维护,我们转化一下,就是去求你现在缺的油,和以前缺的油的最大值,维护一个前缀 max 即可。

  • lefty 表示从 y 开始走,走到 x,不管中间有没有把油耗完,到达之后所剩的油。(就不管不能为负数的条件,硬着头皮开过去还剩多少,当然可能是负数)

  • heldy 表示从 y 开始走,能否走到 x,能为 1,否则为 0。我们考虑这个怎么维护。其实可以在递归搜索的时候再传一个参数 z,表示一路下来的这条链中,每个点走回去所需要的油的最大值。假设从 y 开始向上走了一条路后所剩的油为 SS 可能为负数。),那么接着向下传就是 min{0,y}+S。(感觉这个虽然有点抽象,还是可以理解的。)然后,每次遍历到一个 y 时,就看 z 是否大于等于 0 来判断是否可以走回去。

那么有了这些东西之后,问题就简单很多了。

可以发现,如果要让一条路径合法,那就是让要合并的 y1,y2(假设是从 y1y2) 满足 heldy1=1,lefty1prey2,by1by2 即可。

那显然有一种思路是,我们维护两个数组 a1,a2,一个用于存放子树中所有的点,另一个存放 heldy=1 的点,显然起点肯定都在 a2 中有储存,然后我们看 a1 中有多少个能成为对应的终点即可。

可以用数据结构,但是我选择排序 + 双指针。

我们将 a1 按照 pre 从小到大排序,a2 按照 left 从小到大排序。我们从头到尾枚举 a2,并维护一个指针 l=0 和一个数组 numx 表示 [1,l]ba1y=x 的个数。然后枚举的时候,如果 leftiprel 那就把 l 向后移动,最后移动完毕之后,把答案加上 lnumbi 即可。

由于有子树内部的排序,复杂度为 O(nlog2n)

3.Ridiculous Netizens

科技题。

首先,我们仍然按照点分治的惯例,当然这里是求包含了 x 的合法的连通块的个数而已。

那么这里就变成了一个 x 的子树上的依赖背包问题,因为你为了选择 x,那么你选了一个点 y,那你就必须选择他的所有祖先。

这样应该可以打出一个 O(nm2logn) 的一个树上背包,但是这样显然还不够,这个点分治的存在似乎是多造成了一个 log。(之所以是平方,是因为你要合并)

所以我们考虑科技。

我们发现,对于一个数 Z,对于所有的 Y[1,Z1]ZY 的个数,似乎只有 Z 以内个。

也就是说,我们的状态存在冗余。

所以我们考虑重新定义状态。

首先,对于子树内部乘积 mdp 状态,我们专门储存,然后对于乘积 >m 的部分,我们储存 mj

然后,我们只需要对所有有效的状态进行转移,时间复杂度来到 O(nmlogn)

然后我们考虑如何把背包的平方去掉。

由于使用了点分治,所以我们不关心过每个子树树根的连通块,只关心过点分中心(重心)的连通块,这时修改背包的定义为:包含重心的连通块延伸到子树内的种类数。

感觉这一步有点抽象啊。

然后在代码实现上,我们发现,这样每次合并的时候,只需要 dpx,j 加上 dpsonx,j,因为不需要考虑当前的贡献了。当然,还有一步,就是你需要把父节点的答案传到下面去,故有 dpx,j×wx 加上 dpfax,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 所有的答案加起来即可。

4.[联合省选模拟] 感染

还是神仙题,点分治优化建图是个什么玩意。。

不难有一个思路,就是我们把所有的 x 与所有 D(x,j)rxj 连一条边,然后跑一遍缩点,然后看入度为 0 的点有多少个。

但是显然,这么暴力建边根本就建不完。

所以接下来,我们考虑如何对这个建边进行优化。

首先,我们显然维护一个 dy 表示当前点分治递归到 x 时,yx 的距离。

然后,我们显然可以对 x 子树内所有的节点按照 dy 从小到大排序。显然,y1 能向 y2 连边,必然满足 dy1+dy2ry1

我们可以从头到尾枚举排完序后的子树,然后二分查找到最后一个满足条件的,然后与 [1,pos] 全部连一条边。这个看似可以运用线段树优化建图解决,但是时空都不太可观。而且光是这个二分查找就已经没救了。

所以我们考虑换一种思路,并把这个二分查找去掉。

我们把子树中的所有节点再开一数组个存下来,并把这个数组按照 rydepy 从小到大排序,这样我们就可以维护一个指针,从 0 开始,指向最开始那个按照距离排序的数组。这样我们就不用二分查找了,[1,l] 既是我们想要连的边。

可以发现,这是一个前缀。

我们考虑对于最开始按照距离排序的子树每个点开一个虚点 pi,表示 [1,i] 这一段区间的点的总和。显然有 pipi1 连边,pii 在子树中对应的点连边。然后,每次在第二个数组中枚举时,将其对应的点与 pl 连边,这样递归完毕就可以实现建图。

有点抽象,具体代码长这样:

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 的黑点的个数,即为答案。

点分治内部,存在排序,复杂度为 O(nlog2n),对于缩点,由于边和点的级别都在 nlogn 级别,故复杂度为 O(nlogn)

故总复杂度为 O(nlog2n)

5.[重庆省队互测] 路径中位数

其实也蛮简单的,看到中位数最大,直接二分答案,将 axmid 的看作 1 否则看作 1

于是转化为能否找到一条合法的路径,使得路径上的点权和 0

这不需要教了吧,直接在点分治递归内部维护每个点到 x 的距离,然后按照这个从小到大排序,然后通过双指针找到每个点对应的另外一条合法的路径中,最大的路径点权和即可。

最后看这个时候是否 0

时间复杂度,二分,排序,点分治, O(nlog2nlogV)

离散化之后,可以做到 O(nlog3n),看似不太可过,但是加上一些剪枝和点分不一定卡的满,也就毫无压力了。

6. [USACO12NOV] Balanced Trees G

做完之后,非常想骂人的一道题目。

首先先考虑如何合并两条路径使得括号匹配。(假定左括号对应点权为 1,右括号对应点权为 1。)

接着在想,哪些路径能从上到下,哪些能从上到下。

于是就有了我们要维护的一些东西:

  • sumy 表示从 x 走到 y 这一路上的点权和。

  • upy 表示从 y 走到 x,一路上点权和的最小值。(就是每到一个点取一个最小值)

  • downy 表示从 y 走到 x,一路上点权和的最小值。(与上面类似)

这样其实就可以完全判断了。对于 y,如果他能从下到上,至少要满足 upy0,如果他能从上到下,必须满足 downy0

然后对于分别满足从上到下,从下到上的两个 y1,y2,只需要满足 sumy1+sumy2ax=0,by1by2,就可以构成一个合法的括号序列。(这里要减去 ax 是因为两个 sum 都把他囊括了进去。)

然后我们考虑如何对两条路径计算那个什么最大嵌套。

转化一下,就是计算这条路径上,每个点前缀点权和的最大值。

于是我们继续定义两个东西:

  • lcy 表示,从 y 走到 x 的前缀点权和的最大值。

  • rcy 表示,从 x 走到 y 的前缀点权和的最大值。

于是,对于我们刚刚定义的两个 y1,y2,其答案就是 max{lcy1,sumy1+rcy2}

至于怎么求最大值,我们考虑直接按照 sumy 从小到大排序,然后双指针处理即可。

上面那 5 个数组求法,应该与文章中的第二题大同小异,这里就不阐述了。

以及,记得讨论从 x 直接向下的一条链的这种情况。

维护的东西是真多。。

7.[VK Cup 2015] Logistical Questions

求导神仙题。

我们定义两个点的距离为 d(x,y)。(是题目中对应的那个距离,不是我们平常说的距离)

为了方便后文的叙述,x,y 不一定是一个确切的点,有可能是边上的任意一个点。

我们仔细观察这个 d,发现对于树上的任意一点 x(这个点可能是边上的一个点),d(x,i)i 是自变量)是一个下凸函数。

于是,我们对于一个点 x 的答案 d(x,i) 也是一个下凸函数。

故,这个最小的 x 是唯一的。(因为有可能在边上)

我们考虑去缩小这个 x 的范围。

最开始肯定是 [1,n]

然后,我们从根节点的儿子看去,假设根节点的答案为 S。由于是唯一的且下凸,那么显然最多只有一个儿子的答案是 S 的。

我们考虑一个变化量的问题。

如果从根节点 x 向他的儿子 y 移动了一个极小的距离 Δ,那么此时的点就在他们两个的那条边上,而此时的答案变化量为(我们始终只关心这个变化量的正负用以精准找到那个子树):

itreey((dist(i,x)+Δ)1.5dist(i,x)1.5))×wi+itreey((dist(i,x)Δ)1.5dist(i,x)1.5))×wi

如果我们把 dist(i,x) 当作一个自变量 t,定义 fi(t)=t1.5×wi

那么我们发现,如果上面那个式子在下面除以一个 Δ,那就变成了一堆 fi 的导数。

而我们显然知道,对于 fi(t),其导数为 fi(t)=1.5×t×wi

因为这个除下去的 Δ 与正负号无关,故上面那个式子的正负就等于 itreeyfi(dist(i,x))itreeyfi(dist(i,x))

而我们可以直接预处理出所有的 itreeyfi(dist(i,x)),来实现判断正负并精准找到要递归的子树。(也就是是负号的那个)

当然,我们每递归一次都要求一个答案,因为有可能他自己就是最大值。

但是这样的话,我们的复杂度就不对,假设递归层数为 T,由于求一次答案复杂度为 O(n),则总复杂度为 O(Tn),显然不对。

所以我们根据点分治的思想,每次移动到一颗子树时,我们只知道答案在这颗树里面,所以我们并不关心是从那里开始的。所以就可以从这颗子树的重心开始讨论,由于判断正负的预处理只需要子树级别,就可以做到 O(nlogn)

后记

总的来说,淀粉质的题目其实分为三类。

  • 通过在点分树上维护大量信息,来得到路径合并求权值,和算对最终答案贡献的目的。

  • 通过点分树的,“必须包含 x” 的限制,借以完成对于时间复杂度的降低,例如树上依赖背包。

  • 通过点分树深度为 logn 的限制,每次精准的找到向下递归的点,从而保证时间复杂度。

posted @   Saltyfish6  阅读(20)  评论(1编辑  收藏  举报
相关博文:
阅读排行:
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探
Document
点击右上角即可分享
微信分享提示