Live2D

Solution -「GLR-R3」谷雨(出题人题解)

\(\mathscr{Description}\)

  Link.

  给定一棵含有 \(n\) 个结点的树,点有点权。现进行 \(q\) 次操作,每次操作形如:

  1. 修改操作 给定 \(u,v,k\),对于所有在路径 \((u,v)\) 上,或者存在一个邻接点在路径 \((u,v)\) 上的结点 \(w\),将其点权变为 \(k\)
  2. 询问操作 给定 \(u,v\),从 \(u\) 出发沿树边走向 \(v\),走到结点 \(w\) 时:\(a)\)\(w\) 的点权加入队列 \(S\)\(b)\) 按标号从小到大枚举 \(w\) 的邻接点 \(x\),若 \(x\) 不在路径上,将 \(x\) 的点权加入队列 \(S\)\(c)\) 最后,走向路径上的下一个点。求出 \(S\) 的最大可空子段和。

  \(n,q\le10^5\)

\(\mathscr{Solution}\)

\(\mathscr{Subtasks}\)

  Subtask 1 按题意模拟,注意开 long long,大家应该都有这档√

  Subtask 2 线段树板题。反正正解也要打线段树,所以这是非常划算的部分分,应该也不难拿叭。

  Subtask 3 在每个结点处拿一些东西(例如线段树)维护儿子,应该对正解有些许启发;当然,也能直接离线处理。

  Subtask 4 & Subtask 5 献给差不多想到解法但觉得修改 / 询问操作特别毒瘤的选手,当然如果确实有特殊解法也是好事 awa。由于这两档已经向正解靠拢了,所以分值给高一些 w。

  Subtask 6 相当于赋值与求和。献给大概会维护但是不会处理结点顺序或者懒得写分讨的选手,有特殊解法……也是好事。(

  Subtask 7 献给高高兴兴写完发现事情并不简单(?)的选手(比如我),从某种意义上也算正解卡常的保底档。拿到这档几乎就会正解啦。

  Subtask 8 献给正解。恭喜拿到这档的选手!因为自己的代码很冗长,也想学习一下各位的实现呢 www。

\(\mathscr{Intro}\)

  本题 idea 极其昂贵,它出自 「NOI 2021」轻重边。当然这道题的诞生意味这我在 NOI 2021 成为了尸体。(

  先从「NOI 2021」轻重边 这道题入手讨论。我们可以把题目抽象为,给定一棵树,支持两种操作:

  1. 毛毛虫赋值;
  2. 路径和查询。

其中毛毛虫正如本题修改和查询中涉及的树结构。这里给出形式化定义:

  毛毛虫是一个树上点集,由一条树上路径 \(P\) 描述。毛毛虫 \(C\) 表示路径 \(P\) 中的以及与 \(P\) 邻接的所有结点构成的点集。此时,称 \(P\)\(C\)虫身\(C\setminus P\)\(C\)虫足

  自然地,毛毛虫和路径有诸多相似之处,那么可以想到用树上路径维护的一贯做法——树链剖分维护毛毛虫。可惜,直接树剖全然无从下手。所以,有一个蠢货(特指我)在赛场上磨蹭半天 DIY 出了一个“毛毛虫剖分”。对于想要进行毛毛虫维护的树,毛毛虫剖分用如下方式对其重标号:

  • 首先重链剖分,求出重链相关信息。

  • 从树根开始递归标号。若现在递归到 \(u\)

    • \(u\) 未被标号,则按顺序为其标号;
    • \(u\) 是重链头,遍历这条重链,按顺序为不在重链上但邻接这条重链的结点标号;
    • \(u\) 有重儿子,先递归重儿子;最后递归 \(u\) 的其他儿子。

  考察这个妙妙标号方法的性质:

  1. 对于重链,除链头外,结点标号连续。

  2. 对于任意结点,它的轻儿子标号连续。

  3. 对于重链,不在这条重链但与其邻接的结点标号连续。

  可见,利用这三个性质,毛毛虫剖分能够同时支持:

  • 树上路径修改 / 查询;
  • 树上毛毛虫修改 / 查询;
  • 子树修改 / 查询。

  时间复杂度基于标号后选用的数据维护方法,总的来说,该方法不弱于传统的重链剖分。当然,标号复杂的代价是常数因子,不过这种算法确实是能够通过 NOI 那题的。

\(\mathscr{Body}\)

  什么鬼标题啊这是,不能用分割线吗。

  用上文的毛毛虫剖分,我们似乎能够轻松解决这道题啦?

  可惜,这种剖分方法将毛毛虫的结构乱序化,我们就算可以分别得到虫身和虫足的点集,却无法用同样优秀的时间去维护或修改顺序访问毛毛虫得到的序列

  受上文“毛毛虫剖分 1.0”的启发,我们立马弄一个“毛毛虫剖分 2.0”的重标号方法:

  • 首先重链剖分,求出重链相关信息。
  • 先为树根标号,然后从树根开始递归标号。若现在递归到结点 \(u\)
    • \(u\) 是重链头,从 \(u\) 出发遍历这条重链,若现在走到结点 \(v\)
      • \(v\) 不是重链头,为它标号;
      • 顺序遍历 \(v\)轻儿子,为它们标号;
      • \(v\) 还有重儿子,则继续向重儿子遍历。
    • 忽略这条重链,直接以任意顺序递归所有邻接于重链的结点。

  可见,毛毛虫剖分 2.0 无法支持路径修改 / 访问,但却支持顺序修改 / 访问毛毛虫。我们可以用线段树维护重标号后序列的最大子段和,然后爬重链进行修改和询问。


  想一想对于一条垂直的链(端点存在祖先-后代关系)在这种标号方法下得到的序列信息是:从上到下遍历链,先加入链上结点,然后顺序加入链外儿子。这对应了 subtask 7\(20\) 分。

  但是!事情确实并不简单,正常情况下,我们做树剖询问需要两个路径端点各自爬到 LCA,然后将其中一个结点爬到的答案反序,最后将两个答案拼在一起。而这种编号方法反序的结果是:从下到上遍历链,先逆序加入链外儿子,后加入链上结点,这不符合题目要求的顺序!

  因此,我们还需要设计一个与“毛毛虫剖分 2.0”对应的“毛毛虫剖分 2.1”:

  • 首先重链剖分,求出重链相关信息。
  • 先为树根标号,然后从树根开始递归标号。若现在递归到结点 \(u\)
    • \(u\) 是重链头,从 \(u\) 出发遍历这条重链,若现在走到结点 \(v\)
      • 逆序遍历 \(v\)轻儿子,为它们标号;
      • \(v\) 不是重链头,为它标号;
      • \(v\) 还有重儿子,则继续向重儿子遍历。
    • 忽略这条重链,直接以任意顺序递归所有邻接于重链的结点。

  在“毛毛虫剖分 2.1”下,询问垂直树链得到的序列信息是:从上到下遍历链,先逆序加入链外儿子,后加入链上结点。反序得到:从下到上遍历链,先加入链上结点,后顺序加入链外儿子。这样,我们才能用这一信息与另一条链合并。

  概括地讲,标算为:用两种标号方法对树重标号,用线段树分别维护序列最大子段和。复杂度为 \(\mathcal O(q\log^2 n)\)。还有一大难点是细节,例如重链头与重链头的儿子们的先后顺序;LCA 处删去两个轻儿子贡献,加入 LCA 祖先和 LCA 重儿子贡献的顺序……难以一一罗列,具体可见代码中的处理方法。


  本来想加上但是害怕被群殴,所以写在题解里的 hard version:追加两种操作:

  1. 修改操作 ……

  2. 询问操作 ……

  3. 链询问操作 给定 \(u,v\),询问路径 \((u,v)\) 上点权序列的最大子段和。(需额外维护“毛毛虫剖分 1.0”。)

  4. 优先级修改操作 每个点 \(u\) 初始有优先级 \(r_u=u\),询问操作中遍历儿子时,以优先级升序遍历。给定 \(u,v\),交换 \(r_u,r_v\)。(平衡树维护序列,区间位移,究极分讨。)

  还是 \(\mathcal O(q\log^2 n)\),反正我不想写 qwq。

\(\mathscr{Code}\)

/*+Rainybunny+*/

#include <bits/stdc++.h>

#define rep(i, l, r) for (int i = l, rep##i = r; i <= rep##i; ++i)
#define per(i, r, l) for (int i = r, per##i = l; i >= per##i; --i)

typedef long long LL;
#define fi first
#define se second

inline char fgc() {
    static char buf[1 << 17], *p = buf, *q = buf;
    return p == q && (q = buf + fread(p = buf, 1, 1 << 17, stdin), p == q) ?
      EOF : *p++;
}

template <typename Tp = int>
inline Tp rint() {
    Tp x = 0, s = fgc(), f = 1;
    for (; s < '0' || '9' < s; s = fgc()) f = s == '-' ? -f : f;
    for (; '0' <= s && s <= '9'; s = fgc()) x = x * 10 + (s ^ '0');
    return x * f;
}

template <typename Tp>
inline void wint(Tp x) {
    if (x < 0) putchar('-'), x = -x;
    if (9 < x) wint(x / 10);
    putchar(x % 10 ^ '0');
}

template <typename Tp>
inline Tp imin(const Tp& u, const Tp& v) { return u < v ? u : v; }
template <typename Tp>
inline Tp imax(const Tp& u, const Tp& v) { return u < v ? v : u; }

const int MAXN = 1e5, IINF = 0x3f3f3f3f;
int n, ecnt, fa[MAXN + 5], val[MAXN + 5], head[MAXN + 5];
int dep[MAXN + 5], siz[MAXN + 5], son[MAXN + 5];
std::vector<int> adj[MAXN + 5];

int dfc[2], dfn[MAXN + 5][2], top[MAXN + 5], ref[MAXN + 5][2];
int clef[MAXN + 5][2], crig[MAXN + 5][2];
int spos[MAXN + 5][2];

struct Atom {
    LL sum, lmx, rmx, amx;
    inline Atom rev() const { return { sum, rmx, lmx, amx }; }
    inline Atom operator + (const Atom& t) const {
        return { sum + t.sum, imax(lmx, sum + t.lmx),
          imax(rmx + t.sum, t.rmx), imax(imax(amx, t.amx), rmx + t.lmx) };
    }
    inline Atom& operator *= (const Atom& t) { return *this = *this + t; }
    inline Atom& operator += (const Atom& t) { return *this = t + *this; }
};

struct SegmentTree {
    Atom uni[MAXN << 2];
    int len[MAXN << 2], tag[MAXN << 2];

    inline void pushup(const int u) {
        uni[u] = uni[u << 1] + uni[u << 1 | 1];
    }

    inline void build(const int u, const int l, const int r, const int id) {
        len[u] = r - l + 1, tag[u] = IINF;
        if (l == r) {
            uni[u].sum = val[ref[l][id]];
            uni[u].lmx = uni[u].rmx = uni[u].amx = imax(0, val[ref[l][id]]);
            return ;
        }
        int mid = l + r >> 1;
        build(u << 1, l, mid, id), build(u << 1 | 1, mid + 1, r, id);
        pushup(u);
    }

    inline void pushas(const int u, const int v) {
        tag[u] = v, uni[u].sum = 1ll * len[u] * v;
        uni[u].lmx = uni[u].rmx = uni[u].amx = imax(0ll, uni[u].sum);
    }

    inline void pushdn(const int u) {
        if (tag[u] != IINF) {
            pushas(u << 1, tag[u]), pushas(u << 1 | 1, tag[u]);
            tag[u] = IINF;
        }
    }

    inline void assign(const int u, const int l, const int r,
      const int al, const int ar, const int k) {
        if (al > ar) return ;
        if (al <= l && r <= ar) return pushas(u, k);
        int mid = l + r >> 1; pushdn(u);
        if (al <= mid) assign(u << 1, l, mid, al, ar, k);
        if (mid < ar) assign(u << 1 | 1, mid + 1, r, al, ar, k);
        pushup(u);
    }

    inline Atom query(const int u, const int l, const int r,
      const int ql, const int qr) {
        if (ql > qr) return { 0, 0, 0, 0 };
        if (ql <= l && r <= qr) return uni[u];
        int mid = l + r >> 1; pushdn(u);
        if (qr <= mid) return query(u << 1, l, mid, ql, qr);
        if (mid < ql) return query(u << 1 | 1, mid + 1, r, ql, qr);
        return query(u << 1, l, mid, ql, qr)
          + query(u << 1 | 1, mid + 1, r, ql, qr);
    }
} sgt[2];

inline void init(const int u) {
    siz[u] = 1, dep[u] = dep[fa[u]] + 1;
    for (int v: adj[u]) {
        init(v), siz[u] += siz[v];
        if (siz[v] > siz[son[u]]) son[u] = v;
    }
}

inline void retopF(const int u) {
    clef[u][0] = dfc[0] + 1;
    if (!top[u]) dfn[u][0] = ++dfc[0];
    for (int v: adj[u]) {
        if (v == son[u]) spos[u][0] = dfc[0];
        else dfn[v][0] = ++dfc[0];
    }
    crig[u][0] = dfc[0];
    if (son[u]) retopF(son[u]);
}

inline void retopR(const int u) {
    clef[u][1] = dfc[1] + 1;
    for (int i = int(adj[u].size()) - 1, v; ~i; --i) {
        if ((v = adj[u][i]) == son[u]) spos[u][1] = dfc[1];
        else dfn[v][1] = ++dfc[1];
    }
    if (!top[u]) dfn[u][1] = ++dfc[1];
    crig[u][1] = dfc[1];
    if (son[u]) retopR(son[u]);
}

inline void renum(const int u, const int tp) {
    top[u] = tp;
    if (u == tp) retopF(u), retopR(u);
    if (son[u]) renum(son[u], tp);
    for (int v: adj[u]) if (v != son[u]) renum(v, v);
}

inline int lca(int u, int v) {
    while (top[u] != top[v]) {
        if (dep[top[u]] < dep[top[v]]) v = fa[top[v]];
        else u = fa[top[u]];
    }
    return dep[u] < dep[v] ? u : v;
}

inline void assign(int u, int v, const int k, const int id) {
    while (top[u] != top[v]) {
        if (dep[top[u]] < dep[top[v]]) u ^= v ^= u ^= v;
        sgt[id].assign(1, 1, n, clef[top[u]][id], crig[u][id], k);
        if (son[u]) {
            sgt[id].assign(1, 1, n, dfn[son[u]][id], dfn[son[u]][id], k);
        }
        u = top[u];
        sgt[id].assign(1, 1, n, dfn[u][id], dfn[u][id], k);
        u = fa[u];
    }
    if (dep[u] < dep[v]) u ^= v ^= u ^= v;
    sgt[id].assign(1, 1, n, clef[v][id], crig[u][id], k);
    if (son[u]) sgt[id].assign(1, 1, n, dfn[son[u]][id], dfn[son[u]][id], k);
    if (v == top[v]) sgt[id].assign(1, 1, n, dfn[v][id], dfn[v][id], k);
    if (fa[v]) sgt[id].assign(1, 1, n, dfn[fa[v]][id], dfn[fa[v]][id], k);
}

inline void append(Atom& res, const int u, const int p, const int id) {
    int l = clef[u][id];
    if (dfn[p][id] < spos[u][id]) {
        res += sgt[id].query(1, 1, n, spos[u][id] + 1, crig[u][id]);
        res += sgt[id].query(1, 1, n, dfn[son[u]][id], dfn[son[u]][id]);
        res += sgt[id].query(1, 1, n, imax(dfn[p][id] + 1, l), spos[u][id]);
        res += sgt[id].query(1, 1, n, l, dfn[p][id] - 1);
    } else {
        res += sgt[id].query(1, 1, n, imax(dfn[p][id] + 1, l), crig[u][id]);
        res += sgt[id].query(1, 1, n, imax(spos[u][id] + 1, l), dfn[p][id] -1);
        if (son[u])
            res += sgt[id].query(1, 1, n, dfn[son[u]][id], dfn[son[u]][id]);
        res += sgt[id].query(1, 1, n, l, imin(spos[u][id], dfn[p][id] - 1));
    }
}

inline std::pair<Atom, int> climb(int u, const int tar, const int id) {
    Atom ret = { 0, 0, 0, 0 }; int p = 0;
    while (top[u] != top[tar]) {
        if (u != top[u]) {
            append(ret, u, p, id);
            ret += sgt[id].query(1, 1, n,
              crig[top[u]][id] + 1, clef[u][id] - 1);
            u = top[u];
            if (!id) {
                ret += sgt[0].query(1, 1, n, clef[u][0], crig[u][0]);
                ret += sgt[0].query(1, 1, n, dfn[u][0], dfn[u][0]);
            } else {
                ret += sgt[1].query(1, 1, n, dfn[u][1], dfn[u][1]);
                ret += sgt[1].query(1, 1, n, clef[u][1], crig[u][1]);
            }
        } else if (!id) {
            append(ret, u, p, 0);
            ret += sgt[0].query(1, 1, n, dfn[u][0], dfn[u][0]);
        } else {
            ret += sgt[1].query(1, 1, n, dfn[u][1], dfn[u][1]);
            append(ret, u, p, 1);
        }
        u = fa[p = u];
    }
    if (u != tar) {
        append(ret, u, p, id);
        ret += sgt[id].query(1, 1, n, clef[p = son[tar]][id], clef[u][id] - 1);
    }
    return { ret, p };
}

inline LL query(const int u, const int v) {
    int w = lca(u, v);
    auto su(climb(u, w, 1)), sv(climb(v, w, 0));
    auto ret(su.fi.rev());
    ret *= sgt[0].query(1, 1, n, dfn[w][0], dfn[w][0]);
    if (fa[w]) ret *= sgt[0].query(1, 1, n, dfn[fa[w]][0], dfn[fa[w]][0]);

    std::vector<std::pair<int, int> > tmp;
    if (su.se && su.se != son[w]) tmp.push_back({ dfn[su.se][0], 0 });
    if (sv.se && sv.se != son[w]) tmp.push_back({ dfn[sv.se][0], 0 });
    if (su.se != son[w] && sv.se != son[w]) tmp.push_back({ spos[w][0], 2 });
    tmp.push_back({ crig[w][0], 1 });
    std::sort(tmp.begin(), tmp.end());
    
    int las = clef[w][0] + (w != top[w]);
    for (auto& p: tmp) {
        ret *= sgt[0].query(1, 1, n, las, p.fi - !p.se);
        if (p.fi == spos[w][0] && p.se == 2) {
            ret = ret + sgt[0].query(1, 1, n, dfn[son[w]][0], dfn[son[w]][0]);
        }
        las = p.fi + 1;
    }
    ret *= sv.fi;
    return ret.amx;
}

int main() {
    rint(), n = rint();
    rep (i, 1, n) val[i] = rint();
    rep (i, 2, n) adj[fa[i] = rint()].push_back(i);

    init(1);
    dfn[1][0] = dfc[0] = dfn[1][1] = dfc[1] = 1;
    renum(1, 1);
    rep (i, 1, n) ref[dfn[i][0]][0] = ref[dfn[i][1]][1] = i;

    sgt[0].build(1, 1, n, 0), sgt[1].build(1, 1, n, 1);
    for (int q = rint(), op, u, v, k; q--;) {
        op = rint(), u = rint(), v = rint();
        if (!op) k = rint(), assign(u, v, k, 0), assign(u, v, k, 1);
        else wint(query(u, v)), putchar('\n');
    }
    return 0;
}

\(\mathscr{Details}\)

  附上数据生成细则:

1. O(n^2) brute-force | 10pts
    1.1. 10/10/10 (n=10/q=10/V=[-10,10])
    1.2. 1000/999/100   | op=1
    1.3. 999/1000/100   | only op[q]=1
    1.4. 1000/999/1000  | u=v.
    1.5. 1000/1000/1e9
2. chain | 10pts
    2.1. 1000/1000/1e9
    2.2. 1e5/(1e5-1)/1e9    | when op=0, |u-v|<10
    2.3. (1e5-1)/1e5/10     | when op=0, |u-v|<100
    2.4. 1e5/1e5/1000
    2.5. 1e5/1e5/1e9
3. in all operations, u=v holds | 10pts
    3.1. 1000/1000/1e9
    3.2. 1e5/(1e5-1)/10     | fa<=10, u,v are generated in [1,10] with pr=0.5
    3.3. (1e5-1)/1e5/100    | fa<=100, u,v are generated in [1,100] with pr=0.5
    3.4. 1e5/1e5/1000       | fa=712, u,v=712 with pr=0.412
    3.5. 1e5/1e5/1e9        | fa<=1e4
4. no modification | 15pts
    4.1. 1000/1000/1e9
    4.2. 1e5/(1e5-1)/100    | fa<=1e4
    4.3. (1e5-1)/1e5/1000   | fa<=1e4
    4.4. 1e5/1e5/1e9        | fa[u] is in [u-9,u)
    4.5. 1e5/1e5/100        | `fa[u] = u - 1 - (u & 1)`
5. only one query operation | 15pts
    5.1. 1000/1000/1e9
    5.2. 1e5/(1e5-1)/10
    5.3. (1e5-1)/1e5/100
    5.4. 1e5/1e5/1000       | `fa[u] = u - 1 - (u & 1)`, |u-v|<100
    5.5. 1e5/1e5/1e9
6. v>=0 | 10pts
    6.1. 1000/1000/1e9
    6.2. 1e5/(1e5-1)/1e9
    6.3. (1e5-1)/1e5/1e9
    6.4. 1e5/1e5/1e9        | fa<=10
    6.5. 1e5/1e5/1e9        | `fa[u] = u - 1 - (u & 1)`
7. when op=1, node 1 considered as root, u is v's ancestor | 20pts
    7.1. 1000/1000/1e9
    7.2. 1e5/(1e5-1)/10     | fa=712, v=712 with pr=0.412
    7.3. (1e5-1)/1e5/100    | `fa[u] = u - 1 - (u & 1)`, |u-v|<10 for op=0.
    7.4. 1e5/1e5/1000       | fa<=100
    7.5. 1e5/1e5/1e9        | fa[u] is in [u-127,u)
8. completed solution | 10pts
    8.1. 1000/1000/1e9
    8.2. 1e5/(1e5-1)/10     | fa=712, u,v=712 with pr=0.412
    8.3. (1e5-1)/1e5/100    | `fa[u] = u - 1 - (u & 1)`, |u-v|<10
    8.4. 1e5/1e5/1000       | fa<=100
    8.5. 1e5/1e5/1e9        | fa[u] is in [u-127,u)

  若无特殊说明,树形的生成方式为每个点随机选父亲。可以对着看一看自己为什么得分 / 挂分。(

posted @ 2022-12-23 23:05  Rainybunny  阅读(106)  评论(0编辑  收藏  举报