[SNOI2019] 网络 题解
[SNOI2019] 网络 题解
最喜欢这道题。
简要题意
给一颗 \(n\) 个节点的树和一个参数 \(d\),定义两个节点 \(x,y\) 之间的距离为 \(x\) 到 \(y\) 的简单路径上的边数。
定义一个树上连通块的权值为连通块中任意两点的距离之和。定义一个树上连通块的直径为连通块中任意两点距离的最大值。
给出 \(q\) 次询问,第 \(i\) 次询问给出一个点 \(u_i\),请求出所有包含 \(u_i\) 的直径不超过 \(d\) 的树上连通块中权值的最大值。
\(n\le 5\times 10^5,q\le 10\)
前置知识
DP,长链剖分。
题目分析
由于要求的是权值的最大值,所以说我们最终选择的连通块一定是一个直径不超过 \(d\) 的极大连通块,所以说可以考虑对于每个点和边,求出以它为中心的极大连通块的权值,然后每次询问遍历所有合法的中心取答案最大值即可。
具体地:
当 \(d\) 是偶数的时候,以点 \(x\) 为中心的极大合法连通块是所有与 \(x\) 距离不超过 \(\dfrac{d}{2}\) 的点的集合。
当 \(d\) 是奇数的时候,以 边 \((x,y)\) 为中心的极大合法连通块是所有与 \(x\) 和 \(y\) 距离都不超过 \(\dfrac{d+1}{2}\) 的点的集合。
以点或边为中心的极大合法连通块的权值就是集合内的点两两距离之和,下面考虑如何对于每个点和边都求出以它为中心的权值。
这里有一个小技巧就是把原树上每条边中间建一个虚拟点,把每条边拆成两段,即长度乘二,这样相当于连通块直径最大长度为 \(2d\),为偶数,就都可以转化为以点为中心,这样合法的中心就是与 \(u\) 距离不超过 \(d\) 的点。
\(n\le 5000\)
这部分可以暴力枚举中心,把每个中心的答案预处理出来计算即可,复杂度 \(\mathcal{O}(n^2+qn)\),具体看 代码(代码使用了上面说的拆边技巧)。
\(d=n-1\)
因为整棵树的直径不超过 \(n-1\),所以相当于没有直径的限制,每次都取整棵树就行,蒟蒻没有写这部分的代码,相信大家都会。
下文中为了表述方便,记原题中的 \(d\) 为 \(D\),记原题中的 \(\lfloor\dfrac{d}{2}\rfloor\) 为 \(d\),记原题中的 \(\lfloor\dfrac{d+1}{2}\rfloor\) 为 \(d'\)。
\(n\le 10^5\)
下文中不再使用拆边的技巧,因为蒟蒻拆边被卡空间了。
到了这个数量级显然不能直接枚举中心来计算了,首先我们先考虑如果不暴力枚举中心,而采取 DP 如何做(下面钦定 \(1\) 为根,转化成有根树):
首先,对于一个点 \(x\),如果只考虑在 \(x\) 子树内的点那么如何 DP。可以考虑设计如下状态:
\(f_{x,i}\) 表示 \(x\) 子树内与 \(x\) 的距离不超过 \(i\) 的点的个数。
\(g_{x,i}\) 表示 \(x\) 子树内与 \(x\) 的距离不超过 \(i\) 的点与 \(x\) 的距离之和。
\(h_{x,i}\) 表示 \(x\) 子树内与 \(x\) 的距离不超过 \(i\) 的所有点中任意两点距离之和。
转移如下:
对于点 \(x\),考虑依次加入每个子树的贡献,记 \(f',g',h'\) 为加入子树 \(y\) 之前的 DP 值(\(y\in SON_x\),\(SON_x\) 表示 \(x\) 的儿子集合)。
在加入所有子树之前有初始值 \(\forall i\ge 0,f_{x,i}=1,g_{x,i}=0,h_{x,i}=0\)。
当加入子树 \(y\) 时有转移:
接着考虑在 \(x\) 子树外的点(子树外的点中不包括点 \(x\),换句话说就是点 \(x\) 包含在子树 \(x\) 内),类似上面的可以设计如下状态:
\(F_{x,i}\) 表示 \(x\) 子树外与 \(x\) 的距离不超过 \(i\) 的点的个数。
\(G_{x,i}\) 表示 \(x\) 子树外与 \(x\) 的距离不超过 \(i\) 的点与 \(fa_x\) 的距离之和。
\(H_{x,i}\) 表示 \(x\) 子树外与 \(x\) 的距离不超过 \(i\) 的所有点中任意两点距离之和。
其中 \(fa_x\) 表示树上 \(x\) 的父亲,这样设计状态的目的是为了转移式子更优美。跟上面不同,这些 DP 状态要从上往下转移,转移如下:
依旧是一次考虑加入以 \(fa_x\) 的儿子为根的子树(除了子树 \(x\))的贡献,\(F',G',H'\) 为加入子树 \(y\) 之前的 DP 值(\(y\in SON_{fa_x},y\not=x\))。
在加入所有子树之前有初始值(\(i\ge 1\)):
当加入子树 \(y\) 时有一下转移:
转移完之后,若 \(D\) 为偶数,则以点 \(x\) 为中心的极大合法连通块的权值为:
若 \(D\) 为奇数,则以边 \((x,fa_x)\) 为中心的极大合法连通块的权值为:
(请读者注意下标中 \(d\) 和 \(d'\) 的区别)。
这样对于询问 \(u\),合法的中心点就是与 \(u\) 距离不超过 \(d\) 的点,中心边 \((x,y)\) 合法需要满足 \(x\) 或 \(y\) 与 \(u\) 的距离不超过 \(d\)。
所以到此为止就做到了 \(\mathcal{O}(nD)\) 的复杂度。虽然说用这么麻烦的方法依旧时 \(\mathcal{O}(nD)\) 的复杂度,得分没有变化,但是对于正解有较大的启发意义。
因为这是一个有关深度的 DP,所以说可以尝试一下使用长链剖分进行优化。下文中记 \(son_x\) 表示 \(x\) 的长儿子,\(top_x\) 表示 \(x\) 所在长链的顶端的点的标号(下文中的 \(x\) 的 DP 值都存到 \(top_x\) 的 DP 数组中),\(ht_x\) 表示 \(x\) 的高度,其中叶子结点的高度为 \(1\),\(id_x\) 表示 \(x\) 与 \(top_x\) 的距离(这个是方便转换下标用的),\(len_x=\max_{y\in SON_x,y\not=son_x}ht_y\)。
首先对于 \(f,g,h\) 这三个 DP 数组的转移的长链剖分优化是比较显然的。\(x\) 继承 \(son_x\) 的数组,然后暴力合并其他短儿子的数组,然后长链剖分优化之前的 \(f_{x,i},g_{x,i},h_{x,i}\) 分别存到长链剖分优化之后的 \(f_{top_x,id_x+i},g_{top_x,id_x+i},h_{top_x,id_x+i}\) 中,这样就可以很好的继承长儿子。
但是转移不止这么简单,还有一个新的问题就是我们的 DP 数组存的是距离 不超过 \(i\) 的……,也就是存的是一个前缀和,即 DP 数组的下标 \([0,+\infty]\) 都是有值的,只不过从下标 \(ht_{top_x}\) 开始往后的 DP 值都等于下标 \(ht_{top_x}-1\) 处的 DP 值,我们就没有存,而如果我们在把短儿子暴力合并到当前 DP 数组时如果直接遍历到下标 \(ht_{top_x}-1\) 去更新 DP 值,那复杂度就炸了(可以造一个长链,然后在长链的顶端挂一个菊花),所以说我们不能直接遍历,那么如何做?
由于我们无法暴力遍历的是一个后缀,并且这个后缀用来需要更新的 \(f_y,g_y,h_y\) 的值是相同的,所以说我们可以考虑在每个长链的 DP 数组上建一个线段树,看能否设计一个 \(lazy\_tag\),实现区间修改。
首先我们列出有哪些区间修改操作:
对于没有加入任何子树 \(y\) 之前,我们继承 \(son_x\) 之后,将点 \(x\) 加入 DP 数组造成的修改为对于 \(i\ge 0\) 依次进行如下操作(记为操作 \(1\)):
而对于加入子树 \(y\) 对 DP 数组造成的修改为对于 \(i\ge 1\) 依次进行如下操作(这里因为 \(y\) 是短儿子,所以 \(y=top_y\))(记为操作 \(2\)):
综上来看,\(h\) 数组最先变化,\(g\) 数组第二个变化,\(f\) 数组第三个变化。
对于 \(h\) 数组,它的变化有三部分:加某个常数、加某个常数与当前 \(g\) 的乘积、加某个常数与当前 \(f\) 的乘积;
对于 \(g\) 数组,它的变化有两部分:加某个常数、加某个常数与当前 \(f\) 的乘积;
对于 \(f\) 数组,他的变化有一部分:加某个常数。
因此我们可以设计一个六元 \(lazy\_tag=(kf,kg,tag\_h,fg,tag\_g,tag\_f)\)。该六元 \(lazy\_tag\) 的含义是对 \(f,g,h\) 数组依次进行以下操作(为了写着方便,下面这个公式中将 \(f_{x,i}\) 简记作 \(f_i\),其他类似):
那么如果当前的 \(lazy\_tag=(kf,kg,tag\_h,fg,tag\_g,tag\_f)\),此时有进行一次区间修改,修改的新的 \(lazy\_tag'=(kf',kg',tag\_h',fg',tag\_g',tag\_f')\),合并方法如下(读者可以自己推一下,方法就是用 \(lazy\_tag\) 的数来表示数组中的真实值,然后再用 \(lazy\_tag'\) 修改,对比系数即可),合并运算符号记为 \(*\),其中 \(A*B\not= B*A\):
那么对于操作 \(1\) 相当于下标区间 \([id_x,ht_x-1]\) 更新一个 \((1,1,0,1,0,1)\) 的标记;
对于操作 \(2\) 相当于在下标区间 \([ht_y,ht_x-1]\) 更新一个 \((g_{y,ht_y-1}+f_{y,ht_y-1},f_{y,ht_y-1},h_{y,ht_y-1},g_{y,ht_y-1},f_{y,ht_y-1})\) 的标记 。
到此为止,关于 \(f,g,h\) 的长链剖分优化先暂时告一段落,到这里为止 \(f,g,h\) 求解的复杂度达到 \(\mathcal{O}(n\log n)\)。
对于 \(F,G,H\) 数组依旧是可以进行长链剖分优化的。因为我们最终算出以每个点为中心的极大连通块时我们只需要用到 \(F,G,H\) 中距离 \(x\) 不超过 \(d'\) 的 DP 值,而当我们从 \(fa_x\) 转移到 \(x\) 时是让 \(F_{x,i},G_{x,i},H_{x,i}\) 从 \(F_{fa_x,i-1},G_{fa_x,i-1},H_{fa_x,i-1}\) 转移过来,也就是说如果 \(F_{fa_x},G_{fa_x},H_{fa_x}\) 中当 \(i\ge p\) 的时候数组中的 DP 值才有效,每往下转移一层的本质相当于令 \(p\gets p+1\),即损失掉一个位置的值,那么也就是说在 \(x\) 这个位置,我们只需要知道:\(F_{x,i},G_{x,i},H_{x,i}\),其中 \(i\in[d'-ht_x+1,d']\) 处的值就行了,可以发现 \(F,G,H\) 数组中有用的长度依旧是 \(ht_x\),所以可以进行长剖优化。
具体地:
首先说明有关下标的问题:先令原来 \(F_{x,i},G_{x,i},H_{x,i}\) 中的值储存在 \(F_{x,d'-i},G_{x,d'-i},H_{x,d'-i}\),这样子下标的值域就变成了 \([0,ht_x-1]\),然后因为 \(son_x\) 要继承 \(x\) 的 DP 值,所以说为了方便继承,令刚才说明过的数组值 \(F_{x,i},G_{x,i},H_{x,i}\) 储存在 \(F_{top_x,id_x+i},G_{top_x,id_x+i},H_{top_x,id_x+i}\)。
总的来说,就是继承到 \(x\) 号节点时,\(F_{top_x,id_d+d'-i},G_{top_x,id_d+d'-i},H_{top_x,id_d+d'-i}\) 存的是到点 \(x\) 距离不超过 \(i\) 的点的 DP 值。
接着说明转移的问题,与 \(f,g,h\) 部分类似,还是分为两部分:
第一部分是继承之后更新 \(fa_x\) 对于 DP 数组的贡献,即对于 \(i< d'\),更新的实质是依次进行以下操作(操作 \(3\)):
第二部分是加入子树 \(y\) 的贡献,即对于 \(i\le d'-2\) 更新的实质是依次进行以下操作(操作 \(4\)):
标记跟上面的构造方法相同,发现最后构造出来是一样的,即:
然后就区间修改就行了。注意区间不要搞错。操作 \(3\) 需要更新的区间是 \([0,d'-1]\),操作 \(4\) 需要更新的区间是 \([0,d'-ht_y-2]\)。
但是显然不能对于每一个 \(fa_x\) 的儿子 \(y\) 都重新枚举一遍 \(fa_x\) 的不为 \(y\) 的儿子 \(z\) 去更新 \(F_{top_y},G_{top_y},H_{top_y}\),但是观察可以发现,我们可以先把所有儿子都统一更新一遍,计算按照转移式子把所有子树的贡献都加入之后的 DP 数组,储存在 \(F_{0,i},G_{0,i},H_{0,i}\) 中,然后当计算 \(F_{top_y},G_{top_y},H_{top_y}\) 的时候把 \(y\) 子树的贡献扣除就行了,扣除的方法就是加入的转移倒过来,比较简单就不推了。不过如果都加的话会导致加入子树 \(x\) 时复杂度又会错,所以要先特殊更新一下 \(F_{top_x},G_{top_x},H_{top_x}\),之后计算 \(F_{0,i},G_{0,i},H_{0,i}\) 只需要存下来 \(i\in [0,len_{fa_x}-1]\) 的值就行,因为只有这部分的值有用,然后复杂度就正确了。
最后还有一个小问题,就是 \(f,g,h\) 是从下到上 DP 的,而 \(F,G,H\) 是从上到下 DP 的,并且 \(F,G,H\) 的转移需要用到 \(f,g,h\) 的值,但转移的时候长儿子的 \(f,g,h\) 的值会被覆盖,所以需要找到一种方法能够从 \(x\) 处的 \(f,g,h\) 值还原到 \(son_x\) 处 \(f,g,h\) 处的值(可撤销 DP 数组),方法就是把修改的线段树节点信息记录下来。然后遍历顺序就是先遍历长儿子再遍历短儿子,在遍历长儿子之前还原 \(f,g,h\) 数组,然后当 \(x=son_{fa_x}\) 时更新 \(SON_{fa_x}\) 中的点的 \(F,G,H\) 值,伪代码如下:
//f,g,h 已经求完
dfs2(int x, int fa, int Fa) {//fa 是 x 的父亲,Fa 是 fa 的父亲,该 dfs2 用来求 F,G,H
if (fa && x == son[fa]) {
work(x);//求解 SON_fa 中的点的 F,G,H
}
back(x);//撤销一次 DP 数组
if (son[x]) dfs2(son[x], x, fa);
for (auto y : to[x])
if (y != son[x] && y != fa) dfs2(y, x, fa);
}
到这里整道题的复杂度变为 \(\mathcal{O}(n\log n+qn)\),当然最后那个 \(\mathcal{O}(qn)\) 可以变成 \(\mathcal{O}(q\log n+nlog^2n)\),具体就是采用淀粉质或者淀粉树。
由于蒟蒻比较懒所以没写这一部分,就不放代码了,相信大家都能轻松写出来。具体的写法可以略微参考正解写法(。
\(n\le 5\times 10^5\)
其实如果常数小的话感觉 \(\mathcal{O}(n\log n)\) 或许能过,但我没有尝试,因为空间爆炸!!!
所以说要把线段树优化掉(。
首先以 \(f,g,h\) 的转移为例进行说明:
我们之所以要使用 \(lazy\_tag\) 是因为 \(\sum_x\sum_{y\in SON_x,y\not= son_x} ht_{x}-ht_{y}\) 最大能到 \(\mathcal{O}(n^2)\) 级别,但是长剖的性质决定了:
所以如果能把后缀区间修改变成前缀修改就不需要线段树了,因此我们考虑维护一个全局标记 \(lazy\_tag\),表示对整个数组进行标记表示的变化。具体地,操作 \(1\) 就是全局更新标记 \((1,1,0,1,0,1)\),操作 \(2\) 就是全局更新标记 \((g_{y,ht_y-1}+f_{y,ht_y-1},f_{y,ht_y-1},h_{y,ht_y-1},g_{y,ht_y-1},f_{y,ht_y-1})\),然后要对于不需要更新的地方进行还原:
具体方法是,对于不需要全局更新的下标位置,假如操作之前 DP 下放全局标记之后的真实值为 \(f,g,h\),操作之后的全局标记是 \(lazy\_tag=(kf,kg,tag\_h,fg,tag\_g,tag\_f)\),那么操作之后数组中应当储存的值 \(f',g',h'\) 应该是:
那么 \(f,g,h\) 中有几处需要被还原:
- \(x\) 继承 \(son_x\) 之后并且还没有更新 \(x\) 这个点对 \(f,g,h\) 数组的贡献时,\(f_{top_x,id_x},g_{top_x,id_x},h_{top_x,id_x}\) 要还原成 \(0\)。
- 加入 \(y\) 子树的贡献之后,要把下标在 \([0,ht_y-1]\) 的值还原。
那么对于 \(F,G,H\) 也是类似的,大致就是还原以下内容:
- 插入子树 \(y\) 之后还原下标区间 \([\max(0,d'-ht_y-1),d'-1]\)。
- 如果 \(id_x+d'<ht_{top_x}\),那么最终要把 \(F_{top_x,id_x+D},G_{top_x,id_x+D},H_{top_x,id_x+D}\) 的值还原成 \(0\)。
本题题解到这里就结束了,代码在 这里,大家可以参考一下。
如果做完这题还意犹未尽的话可以看一看这道 [十二省联考 2019] 希望。