(动态)树分治

· · 个人记录

浅谈一下学了好久的树分治。

一、点分治

适合处理大规模树上路径信息问题。

P3806 【模板】点分治1

很基础的了,询问树上距离为 k 的点对是否存在。

大概就是每次找重心当作根,对于当前的根,统计每个子节点到它的距离,然后用双指针遍历,当且仅当两个儿子到当前根的距离之和为 k 且来自根的不同子树时,这两个子节点的距离就为 k,那么距离为 k 的点对存在。

#include<bits/stdc++.h>
using namespace std;

#define rep(i, a, b) for(int i = a; i <= b; ++i)
const int maxn = 1e4 + 5;
const int maxm = 105;
int n, m, q[maxm];
bool k[maxm], vis[maxn];
int cnt, hd[maxn];
struct node{
    int to, nxt, w;
}e[maxn << 1];
int a[maxn], tot, d[maxn], b[maxn], mxs[maxn], siz[maxn];
int rt;

inline void add(int u, int v, int w)
{
    e[++cnt].to = v;
    e[cnt].nxt = hd[u], hd[u] = cnt;
    e[cnt].w = w;
}

inline void getrt(int u, int fa, int sum)
{
    siz[u] = 1, mxs[u] = 0;
    for(int i = hd[u]; i; i = e[i].nxt)
    {
        int v = e[i].to;
        if(vis[v] or v == fa) continue;
        getrt(v, u, sum);
        mxs[u] = max(mxs[u], siz[v]);
        siz[u] += siz[v];
    }
    mxs[u] = max(mxs[u], sum - siz[u]);
    if(!rt or mxs[rt] > mxs[u]) rt = u;
}

inline void getdis(int u, int fa, int dis, int anc)
{
    a[++tot] = u, d[u] = dis, b[u] = anc;
    for(int i = hd[u]; i; i = e[i].nxt)
    {
        int v = e[i].to;
        if(v == fa or vis[v]) continue;
        getdis(v, u, dis + e[i].w, anc);
    }
}

inline bool cmp(int x, int y)
{
    return d[x] < d[y];
}

inline void clc(int u)
{
    tot = 0;
    /*
    记当前分治的根为 rootroot。
    a 数组记录从 root 能到的点;
    d 数组记录 ai 到 root 的距离;
    b 数组记录 ai属于 root 的哪一棵子树(即当 b[a[i]]=b[a[j] 时,说明 a[i]与 a[j]属于 rootroot 的同一棵子树)
    */
    a[++tot] = u, b[u] = u, d[u] = 0;
    for(int i = hd[u]; i; i = e[i].nxt)
    {
        int v = e[i].to;
        if(vis[v]) continue;
        getdis(v, u, e[i].w, v);
    }
    sort(a + 1, a + tot + 1, cmp);
    rep(i, 1, m)
    {
        if(k[i]) continue;
        int l = 1, r = tot;
        while(l < r)
        {
            int w = d[a[l]] + d[a[r]];
            if(w < q[i]) l += 1;
            else if(w > q[i]) r -= 1;
            else if(b[a[l]] == b[a[r]])
                if(d[a[r]] == d[a[r - 1]]) r -= 1;
                else l += 1;
            else
            {
                k[i] = 1;
                break;
            }
        }
    }
}

inline void slv(int u)
{
    vis[u] = 1;
    clc(u);
    for(int i = hd[u]; i; i = e[i].nxt)
    {
        int v = e[i].to;
        if(vis[v]) continue;
        rt = 0, getrt(v, 0, siz[v]);
        slv(rt);    
    }
}

int main()
{
    scanf("%d %d", &n, &m);
    rep(i, 1, n - 1)
    {
        int u, v, w;
        scanf("%d %d %d", &u, &v, &w);
        add(u, v, w), add(v, u, w);
    }
    rep(i, 1, m) 
    {
        scanf("%d", &q[i]);
        if(!q[i]) k[i] = 1;
    }
    mxs[0] = n;
    getrt(1, 0, n);
    slv(rt);
    rep(i, 1, m) 
        if(k[i]) printf("AYE\n");
        else printf("NAY\n");
    return 0;
}

另一道类似的升级题:P4178 Tree。

题意变为求出树上两点距离小于等于 k 的点对数量。思路差不多,不过是在遍历统计答案的时候使用双指针统计答案,即合法点对个数。但是要注意,如果只是单单这样,显然双指针会把这种情况统计进去:

(图自此博客。)

所以再套个容斥就好了。

code

二、动态点分治/点分树

感觉这个东西还是比较灵活的。

P6329 【模板】点分树 | 震波

所有与 x 距离不超过 k 的城市都将受到影响,求该次地震造成的经济损失,即所有受影响城市的价值和,待修改(修改某节点价值)。强制在线。

首先提出一个关于点分树的概念——重构树。即把当前树的重心和上一层的树重心连边,后者是前者父亲。这样得到的重构树形态优秀,有利于解决和树的形态无关的问题

剩下具体怎么在重构树上查询距离、记录距离等等见此题解。

其中还用了一个 \mathcal{\text{O}}(1) 求 LCA 的小技巧。

特别要注意的一点是:我们再构造重构树之前就已经记录下原树上两个节点的距离了,后续题目不会再对树的形态进行改变了,所以方法可行。

#include<bits/stdc++.h>
using namespace std;

#define rep(i, a, b) for(register int i = a; i <= b; ++i)
#define go(u) for(int i = hd[u], v = e[i].to; i; i = e[i].nxt, v = e[i].to)
const int maxn = 2e5 + 5;
const int inf = 1e9 +7;
int n, m;
int a[maxn], cnt, hd[maxn];
struct node{
    int to, nxt;
}e[maxn << 1];
int pos[maxn], id[maxn], tot;
int st[maxn << 1][21], dep[maxn], rt, sum;
int siz[maxn], fa[maxn], lg[maxn << 1];
bool vis[maxn];
int ans, mxx;
vector <int> c[2][maxn];

inline void add(int u, int v){
    e[++cnt] = (node){v, hd[u]};
    hd[u] = cnt;
}

inline void dfs1(int u, int f){
    st[++tot][0] = u, pos[u] = tot;
    go(u) if(v != f){
        dep[v] = dep[u] + 1, dfs1(v, u);
        st[++tot][0] = u;
    }
}

inline int minn(int a, int b){
    return dep[a] < dep[b] ? a : b;
}

inline void pre(){
    lg[0] = -1;
    rep(i, 1, tot) lg[i] = lg[i / 2] + 1;
    for(int k = 1; (1 << k) <= cnt; ++k)
        for(int i = 1; i + (1 << k) <= cnt; ++i)
            st[i][k] = minn(st[i][k - 1], st[i + (1 << (k - 1))][k - 1]);
}

inline void findrt(int u, int f){
    siz[u] = 1;
    int mxs = 0;
    go(u) if(v != f and !vis[v]){
        findrt(v, u), siz[u] += siz[v];
        mxs = max(mxs, siz[v]);
    }
    mxs = max(mxs, sum - mxs);
    if(!rt or mxs < mxx) rt = u, mxx = mxs;
}

inline void build(int u){
    vis[u] = 1, siz[u] = sum + 1;
    c[0][u].resize(siz[u] + 1), c[1][u].resize(siz[u] + 1);
    go(u) if(!vis[v]){
        sum = siz[v], rt = 0, mxx = -inf, findrt(v, u);
        fa[rt] = u, build(rt);
    }
}

inline int gdis(int u, int v){
    if(pos[u] > pos[v]) swap(u, v);
    int x = pos[u], y = pos[v], k = y - x + 1;
    int lca = minn(st[x][lg[k]], st[y - (1 << lg[k]) + 1][lg[k]]);
    return dep[u] + dep[v] - 2 * dep[lca];
}

inline int lb(int x){
    return x & (-x);    
}

inline void update(int u, int opt, int x, int w){
    x += 1;
    for(int i = x; i <= siz[u]; i += lb(i)) 
        c[opt][u][i] += w;
}

inline int query(int x, int opt, int y){
    y += 1;
    int res = 0;
    for(int i = min(y, siz[x]); i; i -= lb(i))
        res += c[opt][x][i];
    return res;
}

inline void update_path(int x, int w){
    for(int i = x; i; i = fa[i]) update(i, 0, gdis(x, i), w);
    for(int i = x; fa[i]; i = fa[i]) update(i, 1, gdis(x, fa[i]), w);
}

int main(){
    scanf("%d%d", &n, &m);
    rep(i, 1, n) scanf("%d", &a[i]);
    rep(i, 2, n){
        int u, v;
        scanf("%d%d", &u, &v);
        add(u, v), add(v, u);
    }
    dep[1] = 1, dfs1(1, 0), pre();
    sum = n, mxx = -inf, findrt(1, 0), build(rt);
    rep(i, 1, n) update_path(i, a[i]);
    rep(i, 1, m){
        int opt, x, y;
        scanf("%d%d%d", &opt, &x, &y);
        x ^= ans, y ^= ans;
        if(!opt){
            ans = query(x, 0, y);
            for(int i = x; fa[i]; i = fa[i]){
                int dis = gdis(fa[i], x);
                if(y >= dis)
                    ans += query(fa[i], 0, y - dis) - query(i, 1, y - dis);
            }
            printf("%d\n", ans);
        }
        else update_path(x, y - a[x]), a[x] = y;
    }
    return 0;
}   

重构树的具体应用可以见下述的“动态树分治”。

三、动态树分治

模板:P4719 【模板】"动态 DP"&动态树分治

动态树分治,也叫动态 DP 问题,是猫锟在 WC2018 讲的黑科技,一般用来解决树上的带有点权(边权)修改操作的 DP 问题。

首先我们把不带修改的 dp 问题进行求解,求出它的转移方程。 然后带上修改。不难发现,如果更改了某一节点的权值,那么它到该树根节点路径上所有的节点的 dp 值都会受到影响。所以为了保证 $\mathcal{\text{O}}(logn)$ 的复杂度,我们使用重链剖分。 然后再去优化转移方程(即是把方程里面的 $\sum$ 去掉),这样以来,我们发现对于区间(即一条重链)内的区间加并不好处理,所以用矩阵乘法把它优化成区间“乘积”。 但是这样有又了新问题:优化后的转移方程和平时常用的矩阵乘法运算不一样(转移方程内有一个取 $max$ 的步骤,但矩阵乘法中没有)。所以我们重新定义矩阵乘法。 然后再根据转移方程去构造转移矩阵。 这样,我们就把修改操作转换为区间乘了,对于每条重链我们用线段树维护即可。 其余转移方程、矩阵乘法重定义、转移矩阵等具体内容见[此博客(题解)](https://www.luogu.com.cn/blog/Tweetuzki/solution-p4179)。 代码: ```cpp #include<bits/stdc++.h> using namespace std; #define rep(i, a, b) for(register int i = a; i <= b; ++i) #define go(u) for(int i = hd[u], v = e[i].to; i; i = e[i].nxt, v = e[i].to) const int maxn = 1e5 + 5; const int maxm = 4e5 + 5; int n, m; int a[maxn], cnt, hd[maxn]; int tot, id[maxn], dfn[maxn], siz[maxn]; int mxs[maxn], f[maxn][2], fa[maxn]; int top[maxn], ed[maxn]; struct node{ int to, nxt; }e[maxn << 1]; struct matrix{ int mt[2][2]; matrix(){ memset(mt, -0x3F, sizeof mt); } inline matrix operator * (matrix b){ matrix c; rep(i, 0, 1) rep(j, 0, 1) rep(k, 0, 1) c.mt[i][j] = max(c.mt[i][j], mt[i][k] + b.mt[k][j]); return c; } }val[maxn], tr[maxm]; int L[maxm], R[maxm]; inline void add(int u, int v){ e[++cnt] = (node){v, hd[u]}; hd[u] = cnt; } inline void dfs1(int u){ siz[u] = 1; go(u) if(v != fa[u]){ fa[v] = u, dfs1(v); siz[u] += siz[v]; if(siz[v] > siz[mxs[u]]) mxs[u] = v; } } inline void dfs2(int u, int anc){ id[u] = ++tot, dfn[tot] = u; top[u] = anc, ed[anc] = max(ed[anc], id[u]); f[u][0] = 0, f[u][1] = a[u]; val[u].mt[0][0] = val[u].mt[0][1] = 0; val[u].mt[1][0] = a[u]; if(mxs[u]){ dfs2(mxs[u], anc); f[u][0] += max(f[mxs[u]][0], f[mxs[u]][1]), f[u][1] += f[mxs[u]][0]; } go(u) if(v != mxs[u] and v != fa[u]){ dfs2(v, v); f[u][0] += max(f[v][0], f[v][1]); f[u][1] += f[v][0]; val[u].mt[0][0] += max(f[v][0], f[v][1]); val[u].mt[0][1] = val[u].mt[0][0]; val[u].mt[1][0] += f[v][0]; } } inline void up(int x){ tr[x] = tr[x << 1] * tr[x << 1 | 1]; } inline void build(int i, int l, int r){ L[i] = l, R[i] = r; if(l == r){ tr[i] = val[dfn[l]]; return; } int mid = l + r >> 1; build(i << 1, l, mid), build(i << 1 | 1, mid + 1, r); up(i); } inline void update(int i, int pos){ if(L[i] == R[i]){ tr[i] = val[dfn[pos]]; return; } int mid = L[i] + R[i] >> 1; if(pos <= mid) update(i << 1, pos); else update(i << 1 | 1, pos); up(i); } inline matrix query(int i, int l, int r){ if(L[i] >= l and R[i] <= r) return tr[i]; int mid = L[i] + R[i] >> 1; if(r <= mid) return query(i << 1, l, r); else if(l > mid) return query(i << 1 | 1, l, r); else return query(i << 1, l, r) * query(i << 1 | 1, l, r); } inline void update_path(int u, int k){ val[u].mt[1][0] += k - a[u], a[u] = k; matrix bef, aft; while(u > 0){ bef = query(1, id[top[u]], ed[top[u]]); update(1, id[u]); aft = query(1, id[top[u]], ed[top[u]]); u = fa[top[u]]; val[u].mt[0][0] += max(aft.mt[0][0], aft.mt[1][0]) - max(bef.mt[0][0], bef.mt[1][0]); val[u].mt[0][1] = val[u].mt[0][0]; val[u].mt[1][0] += aft.mt[0][0] - bef.mt[0][0]; } } int main(){ scanf("%d%d", &n, &m); rep(i, 1, n) scanf("%d", &a[i]); rep(i, 2, n){ int u, v; scanf("%d%d", &u, &v), add(u, v), add(v, u); } dfs1(1), dfs2(1, 1); build(1, 1, n);//线段树叶子节点 i 代表 dfs序第 i 个的 val 值 rep(i, 1, m){ int u, k; scanf("%d%d", &u, &k); update_path(u, k); matrix fin = query(1, 1, ed[1]);//need to test printf("%d\n", max(fin.mt[0][0], fin.mt[1][0])); } return 0; } ``` 例题:[P3920 [WC2014]紫荆花之恋](https://www.luogu.com.cn/problem/P3920) 大视野上把它归在动态树分治里,但是 $\text{solution}$ 里可谓是八仙过海。 我贺的是林教练的做法:分情况讨论 + 替罪羊树思想 + 平衡树。所以也不算是动态树分治了。 具体地:[【LG-P3920/WC2014】 紫荆花之恋-题解(by 531)](https://www.luogu.com.cn/blog/469672/solution-p3920)。 例题:[P2056 [ZJOI2007] 捉迷藏](https://www.luogu.com.cn/problem/P2056) (其实也算是动态点分治的例题... 不带修改,就是经典的树上两点距离最大值的求解。 所以我们要动态维护每个节点子树内最大值和次大值,此处对于每个节点采用两个堆维护。 在重构树的基础上进行查询修改操作即可。[code.](https://www.luogu.com.cn/record/77452456) ------------ ——$End$——