Solution -「GLR-R3」谷雨(出题人题解)
\(\mathscr{Description}\)
Link.
给定一棵含有 \(n\) 个结点的树,点有点权。现进行 \(q\) 次操作,每次操作形如:
- 修改操作 给定 \(u,v,k\),对于所有在路径 \((u,v)\) 上,或者存在一个邻接点在路径 \((u,v)\) 上的结点 \(w\),将其点权变为 \(k\)。
- 询问操作 给定 \(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」轻重边 这道题入手讨论。我们可以把题目抽象为,给定一棵树,支持两种操作:
- 毛毛虫赋值;
- 路径和查询。
其中毛毛虫正如本题修改和查询中涉及的树结构。这里给出形式化定义:
毛毛虫是一个树上点集,由一条树上路径 \(P\) 描述。毛毛虫 \(C\) 表示路径 \(P\) 中的以及与 \(P\) 邻接的所有结点构成的点集。此时,称 \(P\) 为 \(C\) 的虫身,\(C\setminus P\) 为 \(C\) 的虫足。
自然地,毛毛虫和路径有诸多相似之处,那么可以想到用树上路径维护的一贯做法——树链剖分维护毛毛虫。可惜,直接树剖全然无从下手。所以,有一个蠢货(特指我)在赛场上磨蹭半天 DIY 出了一个“毛毛虫剖分”。对于想要进行毛毛虫维护的树,毛毛虫剖分用如下方式对其重标号:
-
首先重链剖分,求出重链相关信息。
-
从树根开始递归标号。若现在递归到 \(u\):
- 若 \(u\) 未被标号,则按顺序为其标号;
- 若 \(u\) 是重链头,遍历这条重链,按顺序为不在重链上但邻接这条重链的结点标号;
- 若 \(u\) 有重儿子,先递归重儿子;最后递归 \(u\) 的其他儿子。
考察这个妙妙标号方法的性质:
-
对于重链,除链头外,结点标号连续。
-
对于任意结点,它的轻儿子标号连续。
-
对于重链,不在这条重链但与其邻接的结点标号连续。
可见,利用这三个性质,毛毛虫剖分能够同时支持:
- 树上路径修改 / 查询;
- 树上毛毛虫修改 / 查询;
- 子树修改 / 查询。
时间复杂度基于标号后选用的数据维护方法,总的来说,该方法不弱于传统的重链剖分。当然,标号复杂的代价是常数因子,不过这种算法确实是能够通过 NOI 那题的。
\(\mathscr{Body}\)
什么鬼标题啊这是,不能用分割线吗。
用上文的毛毛虫剖分,我们似乎能够轻松解决这道题啦?
可惜,这种剖分方法将毛毛虫的结构乱序化,我们就算可以分别得到虫身和虫足的点集,却无法用同样优秀的时间去维护或修改顺序访问毛毛虫得到的序列。
受上文“毛毛虫剖分 1.0”的启发,我们立马弄一个“毛毛虫剖分 2.0”的重标号方法:
- 首先重链剖分,求出重链相关信息。
- 先为树根标号,然后从树根开始递归标号。若现在递归到结点 \(u\):
- 若 \(u\) 是重链头,从 \(u\) 出发遍历这条重链,若现在走到结点 \(v\):
- 若 \(v\) 不是重链头,为它标号;
- 顺序遍历 \(v\) 的轻儿子,为它们标号;
- 若 \(v\) 还有重儿子,则继续向重儿子遍历。
- 忽略这条重链,直接以任意顺序递归所有邻接于重链的结点。
- 若 \(u\) 是重链头,从 \(u\) 出发遍历这条重链,若现在走到结点 \(v\):
可见,毛毛虫剖分 2.0 无法支持路径修改 / 访问,但却支持顺序修改 / 访问毛毛虫。我们可以用线段树维护重标号后序列的最大子段和,然后爬重链进行修改和询问。
想一想对于一条垂直的链(端点存在祖先-后代关系)在这种标号方法下得到的序列信息是:从上到下遍历链,先加入链上结点,然后顺序加入链外儿子。这对应了 subtask 7 的 \(20\) 分。
但是!事情确实并不简单,正常情况下,我们做树剖询问需要两个路径端点各自爬到 LCA,然后将其中一个结点爬到的答案反序,最后将两个答案拼在一起。而这种编号方法反序的结果是:从下到上遍历链,先逆序加入链外儿子,后加入链上结点,这不符合题目要求的顺序!
因此,我们还需要设计一个与“毛毛虫剖分 2.0”对应的“毛毛虫剖分 2.1”:
- 首先重链剖分,求出重链相关信息。
- 先为树根标号,然后从树根开始递归标号。若现在递归到结点 \(u\):
- 若 \(u\) 是重链头,从 \(u\) 出发遍历这条重链,若现在走到结点 \(v\):
- 逆序遍历 \(v\) 的轻儿子,为它们标号;
- 若 \(v\) 不是重链头,为它标号;
- 若 \(v\) 还有重儿子,则继续向重儿子遍历。
- 忽略这条重链,直接以任意顺序递归所有邻接于重链的结点。
- 若 \(u\) 是重链头,从 \(u\) 出发遍历这条重链,若现在走到结点 \(v\):
在“毛毛虫剖分 2.1”下,询问垂直树链得到的序列信息是:从上到下遍历链,先逆序加入链外儿子,后加入链上结点。反序得到:从下到上遍历链,先加入链上结点,后顺序加入链外儿子。这样,我们才能用这一信息与另一条链合并。
概括地讲,标算为:用两种标号方法对树重标号,用线段树分别维护序列最大子段和。复杂度为 \(\mathcal O(q\log^2 n)\)。还有一大难点是细节,例如重链头与重链头的儿子们的先后顺序;LCA 处删去两个轻儿子贡献,加入 LCA 祖先和 LCA 重儿子贡献的顺序……难以一一罗列,具体可见代码中的处理方法。
本来想加上但是害怕被群殴,所以写在题解里的 hard version:追加两种操作:
-
修改操作 ……
-
询问操作 ……
-
链询问操作 给定 \(u,v\),询问路径 \((u,v)\) 上点权序列的最大子段和。(需额外维护“毛毛虫剖分 1.0”。)
-
优先级修改操作 每个点 \(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)
若无特殊说明,树形的生成方式为每个点随机选父亲。可以对着看一看自己为什么得分 / 挂分。(