【动态点分】绿老师 解题报告

本文详细介绍了动态点分治方法,通过2017北京八十中集训的考试题解析,在线求树上最远点对和ZJOI2007捉迷藏问题。动态点分治能在线带修,支持插入和查询操作,解决黑点间最长距离问题。文中还讨论了如何处理点分树上的距离计算,以及在维护最大值和次大值时确保来自不同子树的限制,同时提到了动态点分治在插入和反转操作中的应用。
摘要由CSDN通过智能技术生成

题目来源: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 ,b中的一维点分。
比如我们 a 点分,那么dis(ai,aj)就可以拆成 d[ai]+d[aj] (只要保证同一棵子树不会搞在一起)。这就相当于每个 bi 点带有 d[ai] 的点权,求带权最远点对。显然上面的做法处理端点点权简直无所畏惧。
需要注意的是点分的复杂度保证在于 k=O(NlogN) ,但是由于相近的 a 点对应的b点却是完全分散的,因此每次都有 n=N 。不能用 O(n) 的dfs做法,否则总复杂度就变成 O(N2) (跟暴力一样啊喂)。用动态点分的 O(klogn) 做,总复杂度就是 O(NlogNlogN)
对于一个点分中心,把它的前 i1 个子树中 a 所对应的b插入,然后拿第 i 棵子树去查,查完再把第i棵子树插入。这样就可避免查到同一个子树中的 a 。点分中心本身对应的b,要么在所有子树之前一起插入,要么在所有子树之后一起查询即可。

综上,这是个静态点分套动态点分。先把点分树建出来,然后再开一个静态点分,内套动态点分求答案。

Debug

第一次写出了两个bug

bug #1

最开始时,我记fa[]为点分树上的父亲,dis[]为到fa的距离。
WA掉样例后发现一个点到其点分树祖先的距离不一定等于点分树上链dis求和,这个距离应该在原树上跑dfs来求。
因此要开dis[200000][18]来分别记录它到所有( logn 个)祖先的距离。建点分树时,从每个点分中心出发,dfs它的管辖区域,来求得dis[][]数组。

bug #2

测了一次80分,发现WA了“ ai 都相同”的部分分。原来对于重合的 a 点没有处理好,究其原因是对于点分中心本身对应的b的处理方式没想清楚。
注意到点分中心对应的一堆 b 之间是可以互相查询的(这些a两两在点分树上的LCA确实是当前点分中心,需要在这里被统计)。因此应该查一个插一个,而不是一起操作(对,题解里是个flag,是我开始时的想法)。

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 的例子
如果只插入不删除,只需在每个点维护最大值和次大值,同时保证不来自同一子树
如果要插入+删除,可以在儿子处各开一个堆维护子树内的值,然后在自己身上开一个堆塞入所有儿子的堆顶,这样自己的堆中自然就是各方豪杰了。

插入 vs 反转

插入时,通常只要在每个点维护常数个最优解或部分最优解,因为最优解是只增不降的。
同时插入+删除时,

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值