题目来源:2017北京八十中集训12.4考试题。出题人yyl
p.s. 这题有个dfs做法。但鉴于我从未写过动态点分,wxh佬建议我拿这道题练一下,作为我的第一道动态点分模板题。
Introduction
在线求树上最远点对
树有边权,给出 k 个黑点,求黑点间的最长距离。
如果是单次询问,当然可以dfs求直径
O(n)
。暂时不讲
或者静态点分单次
O(nlogn)
的naive做法
最好用动态点分,可以在线带修,
O(klogn)
对于一个点,把它插入到点分树上所有祖先中(包括它自己)。对于另一个点,查询点它分树上所有祖先的信息来更新答案。再保证一下查询到不在同一子树的解(即每个点分中心维护来自不同子树的最大值和次大值),就可以保证每两个点都恰好在点分树的LCA处被查到一次。
因此我们可以动态支持同时插入和查询。
BZOJ1095 [ZJOI2007] 捉迷藏
参考链接:https://www.cnblogs.com/zzqsblog/p/6393023.html
一棵树,每个点有一个颜色(黑或白),一开始全是黑色。修改是将某个点的颜色反转,询问黑点间的最长距离。
简要题解
这是公认的动态点分模板题。
询问时,仍然是每个点分中心找到不在同一子树中的最大值和次大值来更新答案。
这道题就是上面那道简单题加上删除操作。因为最大值可能被删掉,可以用可删除的堆来维护最大值。
在每个点开两个堆,第一个堆维护这个中心管辖的所有黑点到中心的距离,第二个堆维护所有分治儿子的堆顶。注意如果分治树上的重心是白点,那么不应该用单独一个来更新答案,如果只用一个黑点来更新答案是不合法的。为了统计答案,我们显然还要再开一个堆来维护答案。
修改就只要沿着点分治树往根走,维护一下经过的祖先节点的堆即可。
Solution
绿老师把这个最远点对的问题扩展到二维。
基本思路仍然是每次点分统计跨过中心的答案。
当然只能对
a
,
比如我们对
a
点分,那么
需要注意的是点分的复杂度保证在于
∑k=O(NlogN)
,但是由于相近的
a
点对应的
对于一个点分中心,把它的前
i−1
个子树中
a
所对应的
综上,这是个静态点分套动态点分。先把点分树建出来,然后再开一个静态点分,内套动态点分求答案。
Debug
第一次写出了两个bug
bug #1
最开始时,我记fa[]
为点分树上的父亲,dis[]
为到fa
的距离。
WA掉样例后发现一个点到其点分树祖先的距离不一定等于点分树上链dis
求和,这个距离应该在原树上跑dfs来求。
因此要开dis[200000][18]
来分别记录它到所有(
logn
个)祖先的距离。建点分树时,从每个点分中心出发,dfs它的管辖区域,来求得dis[][]
数组。
bug #2
测了一次80分,发现WA了“
ai
都相同”的部分分。原来对于重合的
a
点没有处理好,究其原因是对于点分中心本身对应的
注意到点分中心对应的一堆
b
之间是可以互相查询的(这些
Code
#include <cstdio>
#include <vector>
#define INF 9000000000000000LL
typedef long long ll;
typedef std::vector<int> vec;
inline void Max(ll &a, ll b) { if (a < b) a = b; }
int n, m;
vec b[100050];
int h[100050], nx[200050], to[200050], e = 1;
ll w[200050];
inline void adde(int u, int v, ll d) {
to[e] = v; w[e] = d;
nx[e] = h[u]; h[u] = e++;
}
bool vis[100050];
int sz[100050], mx[100050], hv[100050];
int dfssize(int x, int f) {
sz[x] = b[x].size() + 1; mx[x] = 0;
for (int i = h[x]; i; i = nx[i])
if (!vis[to[i]] && to[i] != f) {
register int ret = dfssize(to[i], x);
sz[x] += ret;
if (ret > mx[x]) {
mx[x] = ret;
hv[x] = i;
}
}
return sz[x];
}
int root;
int fa[100050][18], cnt[100050];
ll dis[100050][18];
vec son[100050];
void setroot(int u, int f, ll d) {
fa[u][cnt[u]] = root;
dis[u][cnt[u]++] = d;
for (int i = h[u]; i; i = nx[i])
if (!vis[to[i]] && to[i] ^ f)
setroot(to[i], u, d + w[i]);
}
void divide(int u, int f) {
int Size = dfssize(u, f);
while (mx[u] > Size>>1) u = to[hv[u]];
setroot(root = u, u, 0);
vis[u] = true;
son[f].push_back(u);
for (int i = h[u]; i; i = nx[i])
if (!vis[to[i]]) divide(to[i], u);
}
ll ans = 0;
ll d1[100050], d2[100050];
int g[100050];
void insert(int x, int f, ll d) {
for (int i = h[x]; i; i = nx[i])
if (vis[to[i]] && to[i] ^ f)
insert(to[i], x, d + w[i]);
for (int u : b[x])
for (int i = 0; i < cnt[u]; i++) {
register int p = fa[u][i], q = fa[u][i+1];
register ll v = d + dis[u][i];
if (v > d1[p]) {
if (q ^ g[p]) d2[p] = d1[p], g[p] = q;
d1[p] = v;
}
else if (v > d2[p] && q ^ g[p]) d2[p] = v;
}
}
void query(int x, int f, ll d) {
for (int i = h[x]; i; i = nx[i])
if (vis[to[i]] && to[i] ^ f)
query(to[i], x, d + w[i]);
for (int u : b[x])
for (int i = 0; i < cnt[u]; i++)
Max(ans, (fa[u][i+1] ^ g[fa[u][i]] ? d1 : d2)[fa[u][i]] + d + dis[u][i]);
}
void clear(int x, int f) {
for (int i = h[x]; i; i = nx[i])
if (vis[to[i]] && to[i] ^ f)
clear(to[i], x);
for (int u : b[x])
for (int i = cnt[u]-1, p; i >= 0 && ~g[p = fa[u][i]]; i--)
d1[p] = d2[p] = -INF, g[p] = -1;
}
void solve(int x) {
for (int i = h[x]; i; i = nx[i])
if (vis[to[i]]) {
query(to[i], x, w[i]);
insert(to[i], x, w[i]);
}
for (int u : b[x])
for (int i = 0; i < cnt[u]; i++) {
register int p = fa[u][i], q = fa[u][i+1];
register ll v = dis[u][i];
Max(ans, (q ^ g[p] ? d1 : d2)[p] + v);
if (v > d1[p]) {
if (q ^ g[p]) d2[p] = d1[p], g[p] = q;
d1[p] = v;
}
else if (v > d2[p] && q ^ g[p]) d2[p] = v;
}
clear(x,0);
vis[x] = false;
for (int nx : son[x]) solve(nx);
}
int main() {
freopen("forgive.in","r",stdin);
freopen("forgive.out","w",stdout);
scanf("%d%d",&n,&m);
int u, v; ll w;
for (int i = 1; i < n; i++) {
scanf("%d%d%lld",&u,&v,&w);
adde(u,v,w); adde(v,u,w);
}
while (m--) {
scanf("%d%d",&u,&v);
b[u].push_back(v);
}
divide(1,0);
clear(1,0);
solve(son[0][0]);
printf("%lld\n",ans);
return 0;
}
Conclusion
关于“树上最近/最远点对”问题的各种变形
最近 / 最远
最远点对需要在维护信息时保证不来自同一子树,而最近点对不需要,因为不合法不优。
保证来自不同子树
有两种思路来实现这个限制
【这部分不仅适用于最近/远点对,对所有动态点分都很重要】
(以下 父亲/儿子/子树 均指 点分树中的父亲/儿子/子树)
A. 把多出来的扣掉
通常需要在儿子处维护该子树内部对父亲的贡献,然后在父亲处扣掉。
这个数据结构通常和父亲维护答案的结构相似。如果维护的信息具有可减性,就完全用同样的方式维护,只是对答案造成负的贡献即可。(用 Plan A 的通常都是有可减性的信息)
B. 保证查不到非法解
这时需要针对题目构造一种维护方式,可以说是套路。作为不具可减性的代表,这里举两个关于
min/max
的例子
如果只插入不删除,只需在每个点维护最大值和次大值,同时保证不来自同一子树。
如果要插入+删除,可以在儿子处各开一个堆维护子树内的值,然后在自己身上开一个堆塞入所有儿子的堆顶,这样自己的堆中自然就是各方豪杰了。
点分实现的小细节
在使用 Plan A 时,会发现需要在setroot()
时就知道当前根root
和根的当前儿子near
,其中near
是子树的重心。所以必须先对子树进行divide()
求得重心后再setroot()
。
连锁反应:由于divide()
会设置vis
障碍,会导致setroot()
被挡住。其实只要在divide()
返回时把vis[root]
重置即可。
另外,fa[]
中是按深度由大到小还是由小到大也需认真考虑。
(这都是《Tree改题报告》中得到的人生经验。。。)
其它时候是否需要这样做还需具体分析,主要看维护信息是否需要同时知道root
和near
。(比如 Plan B 的两个例子就都不需要)
插入 vs 反转
只插入时,通常只要在每个点维护常数个最优解或部分最优解,因为最优解是只增不降的。
同时插入+删除时,一般用可删除堆来维护,因为每个值都不能随便扔掉。
上面的 Plan B 也体现了这一性质
多种颜色
如果不再是只有黑色一种,而是
O(n)
种颜色,支持修改颜色,查询同色最近/最远点对等。
按照多种颜色的套路,考虑如果对每种颜色维护一棵树怎么做。显然每次修改就变成从一边删除,再在另一边插入。
当然不可能维护
O(n)
棵树,事实上每个点只记其子树出现的颜色就对了。在每个点开一个map
记下颜色对应的堆的编号(或指针)。这样堆的个数和堆中元素总数都是
O(nlogn)
的,历史堆数是
O((n+q)logn)
,时间复杂度
O((n+q)lognlogn)