LOJ 4210 「NOI2024」登山
当时在场上做了 3h 却还是只会 45 还是 50,现在重新来做 2h 就做出来了,看来多做题确实有用。
如果 day2 能有这样的水平总分就能上 500 了,不过这都是后话了。
令 \(dl_u = d_u - r_u, dr_u = d_u - l_u, dh_u = d_u - h_u\)。
考虑要求的答案是多个源点,一个汇点的形式,要对每个源点做的话肯定是不想要的。
所以考虑把路径倒过来,变成一个源点,多个汇点的形式。
于是设出 dp:令 \(f_u\) 为 \(1\to u\) 的合法路径的数量。
考虑转移时,会发现此时会有两种情况:\(u\to \operatorname{fa}_u\) 或者是 \(u\) 跳到子树。
但是发现如果同时考虑两种情况时转移的先后顺序就不好确定了,并且此时还需要额外判定 \(dh_u\) 这个条件,会显得非常混乱。
于是考虑把部分转移合成一个整体的转移。
首先先考虑一下这个路径的形态,不妨设 \(\to\) 代表往父亲走,\(\Rightarrow\) 代表往子树走。
因为 \(dh_i\) 的限制只与前面的 \(\Rightarrow\) 的前点相关,所以考虑每遇到一个 \(\Rightarrow\) 就分段。那么最后路径的形态会形如 \(1 = p_{1, e_1 = 1}\Rightarrow p_{2, 1}\to p_{2, 2}\to \cdots\to p_{2, e_2}\Rightarrow p_{3, 1} \cdots p_{k, 1}\to \cdots \to p_{k, e_k} = u\)。
此时再考虑到要满足限制至少有 \(d_{p_{1, e_1}} < d_{p_{2, e_2}} < \cdots < d_{p_{k, e_k}}\),也给定了一定的顺序。于是一个比较自然的想法就是考虑 \(1 = p_{1, e_1}\Rightarrow p_{2, e_2}\Rightarrow \cdots \Rightarrow p_{k, e_k} = u\) 的转移(并且这样转移也符合 \(f_u\) 的定义)。
令 \(b_i = p_{i, e_i}\)。
接下来大致转移的方向也确定了,所以就来考虑如何满足这个限制。
那么首先形式化的刻画出来,就是 \(\forall 1\le i < j\le k, 1\le c\le e_j, d_{b_i} < dh_{p_{j, c}}\)。
此时利用 \(d_{p_{1, e_1}} < d_{p_{2, e_2}} < \cdots < d_{p_{k, e_k}}\) 这个限制,所以这个 \(dh_{p_{j, c}}\) 的限制可以贪心的写为 \(\forall 1 < i\le k, 1\le c\le e_i, dh_{p_{i, c}} > d_{b_{i - 1}}\)。
同时能够发现此时这个限制刚好也与上面 \(1 = b_1\to b_2\to \cdots \to b_k = u\) 很好的结合了。
接下来就可以来具体考虑转移了。
首先刚刚已经得到了 dp 的转移顺序:由深度从低到高。
接下来考虑已经知道了 \(f_u\),要转移到 \(f_v(v\in \operatorname{subtree}(u))\)。
那么此时就还需要算这个从 \(u\to w\to v(w\in \operatorname{subtree}(v))\) 的贡献,但是会发现因为到了 \(w\) 后只能往上走,这其实就是 \(f_u\times sum_v\),其中 \(sum_v\) 表示合法的 \(w\) 的数量。
于是考虑求解这个 \(sum_v\)。
首先如果 \(dh_v\le d_u\),那么 \(v\) 一定不能经过,\(sum_v = 0\)。
否则说可以考虑 \(u\) 跳到 \(v\),也可以是跳到 \(v\) 的子树再跳上来,所以是 \(sum_v = [d_u\in [dl_v, dr_v]] + \sum\limits_{x\in \operatorname{son}_v} sum_x\)。
于是可以 \(\mathcal{O}(n)\) 递推出 \(sum_v\),对于每个 \(u\) 都能在 \(\mathcal{O}(n)\) 的复杂度下进行转移。此时时间复杂度为 \(\mathcal{O}(n^2)\)。
$\mathcal{O}(n^2)$ 代码
#include<bits/stdc++.h>
using ll = long long;
constexpr ll mod = 998244353;
constexpr int maxn = 1e5 + 10;
int n;
int fa[maxn], d[maxn], dl[maxn], dr[maxn], dh[maxn];
std::vector<int> son[maxn], ud[maxn];
ll f[maxn]; int sum[maxn];
void dfs(int u, int d, ll val) {
sum[u] = dl[u] <= d && d <= dr[u];
for (int v : son[u]) {
dfs(v, d, val);
sum[u] += sum[v];
}
if (d >= dh[u]) sum[u] = 0;
(f[u] += 1ll * sum[u] * val) %= mod;
}
inline void solve() {
scanf("%d", &n);
for (int i = 0; i <= n; i++) son[i].clear(), ud[i].clear();
ud[0] = {1};
for (int i = 2; i <= n; i++) {
scanf("%d%d%d%d", &fa[i], &dl[i], &dr[i], &dh[i]);
son[fa[i]].push_back(i);
d[i] = d[fa[i]] + 1, ud[d[i]].push_back(i);
dl[i] = d[i] - dl[i], dr[i] = d[i] - dr[i], std::swap(dl[i], dr[i]);
dh[i] = d[i] - dh[i];
}
memset(f, 0, sizeof(f)), f[1] = 1;
for (int d = 0; d <= n; d++) {
for (int u : ud[d]) {
dfs(u, d, f[u]);
}
}
for (int i = 2; i <= n; i++) printf("%lld ", f[i]); puts("");
}
int main() {
int t; scanf("%*d%d", &t);
for (; t--; ) solve();
return 0;
}
那么接下来考虑优化这个过程。
因为每个 \(u\) 的遍历是必要的,所以只能尝试去优化这个转移到 \(v\) 的过程。
首先就有个 \(f_v\leftarrow f_v + sum_v\times f_u\) 的过程,这便启发写出 \(\begin{bmatrix}f_v, sum_v\end{bmatrix}\) 的矩阵形式,那么就有 \(\begin{bmatrix}f_v, sum_v\end{bmatrix}\times \begin{bmatrix}1, &0\\ f_u, &1\end{bmatrix} = \begin{bmatrix}f_v + sum_v\times f_u, sum_v\end{bmatrix}\)。
又结合上所有 \(v\in \operatorname{subtree}(u)\) 都要被转移到,于是考虑线段树维护 dfn 序,那么操作就变成了 \([\operatorname{dfn}_u, \operatorname{rdfn}_u]\) 这个区间内乘矩阵了。
接下来就需要着重考虑这个 \(sum_v\) 的维护了。
此时回到这个 \(sum_v = \begin{cases}0 &dh_v\le d_u\\ [d_u\in [bl_v, br_v]] + \sum\limits_{w\in \operatorname{son}(v)} sum_w & d_u < dh_v\end{cases}\)。
此时考虑这个 \(dh_v\le d_u\) 这个 \(0\) 的意义。发现其实相当于是在树上直接去掉了这个点,变成了许多的有根森林。
那么此时在来考虑 \(d_u < dh_v\) 时 \([d_u\in [bl_v, br_v]] + \sum\limits_{w\in \operatorname{son}(v)} sum_w\) 这个值,把子树和转化为链加,相当于是对于 \(w(w\in \operatorname{subtree}(u))\) 可以用 \([d_u\in [bl_w, br_w]]\) 去贡献其祖先直到遇到一个被去掉的点(最深的祖先 \(v\) 满足 \(dh_v\le d\))。
于是可以对于每个 \(w\) 算其对祖先的贡献,那么就可以用树剖维护做到。
但是还有问题是不可能每一轮都重新对 \(w\) 算,而且删去这个操作也还没有处理。
此时再结合上 dp 的顺序是深度从低到高的,所以一个点只可能从存在变为被删去,\([d_u\in [bl_w, br_w]]\) 这个值也只会出现两次变化。
所以对于后买那那部分的 \([d_u\in [bl_w, br_w]]\),在 \(bl_w\) 这个时刻链 \(+1\),在 \(br_w + 1\) 这个时刻链 \(-1\) 就可以了。
对于找到最近的被去掉的祖先是简单的,假设 \(u\) 被删掉了,就只会影响 \([\operatorname{dfn}_u, \operatorname{rdfn_u}]\) 的点,且深度越大 \(\operatorname{dfn}\) 就越大,于是用一颗线段树维护最值,给 \([\operatorname{dfn}_u, \operatorname{rdfn_u}]\) 的点打上 \(\operatorname{dfn}_u\) 的 tag,查询单点查就可以了。
那么对于存在变删去,此时发现实际会影响的 \(sum\) 其实也只有 \(w\) 和往上的这条链,且修正的权值就是 \(- sum_w\)。
于是对于 \(sum\) 的修改也可以全部用树剖 + 线段树来做。
但是需要注意的是,此时矩阵要变为 \(\begin{bmatrix}f_u, sum_u, 1\end{bmatrix}\),如果是 \(f_u\leftarrow f_{u} + sum_u\times val\) 对应矩阵为 \(\begin{bmatrix}1, &0, &0\\ val, &1, &0\\ 0, &0, &1\end{bmatrix}\);如果是 \(sum_u\leftarrow sum_u + \Delta\) 对应矩阵为 \(\begin{bmatrix}1, &0, &0\\ 0, &1, &0\\ 0, &\Delta, &1\end{bmatrix}\)。
时间复杂度 \(\mathcal{O}(Tw^3n\log^2 n)\),其中 \(w = 3\)。
但是这样写出来会因为矩乘被卡常,一个更好的方式是观察这个矩阵,发现对角线恒为 \(1\),右上方恒为 \(0\)。
(刻画出 \(3\) 个点的带边权有向图,那么矩阵相当于是 \(1\stackrel{1}{\to}1, 2\stackrel{1}{\to}2, 3\stackrel{1}{\to}3, 2\stackrel{val}{\to}1, 3\stackrel{\Delta}{\to}2\),所以实际在变化的值只有 \(2\to 1, 3\to 1, 3\to 2\)。)
于是可以用 \((x, y, z)\) 表示 \((2, 1), (3, 1), (3, 2)\) 处的值,有 \((x, y, z)\times (x', y', z') = (x + x', y + y' + x z', z + z')\)。
于是就做到了时间复杂度 \(\mathcal{O}(Tw n\log^2 n)\),其中 \(w = 3\)。
2025.04.13 补。
其实把子树和转成链加这步是没有必要的。
因为实际上 \(u\) 也可以当作是对于子树内的每个点的贡献求和得到 \(f_u\)。
所以就可以用线段树维护 dfn 序下单点贡献,查询则是一个区间和。
这样做就很好的平衡了修改与查询的复杂度。
于是重点来到,那么对于一个删除的点来说,此时要强制把 \(sum\) 设置成 \(0\) 这个操作该怎么维护。
此时的一个想法是,假设设置 \(sum_u\) 为 \(0\),那么实际上就是自此之后 \(u\) 的所有祖先都不能再收到 \(u\) 的子树的贡献了。
此时又因为查询刚好是区间和,那么把线段树中维护的 \(u\) 单点的贡献改为了 \(- (sum_u - [d\in [dl_u, dr_u]])\),就能使得这个 \(u\) 子树的 \(sum\) 和一定为 \(0\) 了。
在处理 \([d\in [dl_u, dr_u]]\) 的时候,也需要注意一下算贡献的时候不仅要改 \(u\) 也改一下 \(u\) 的最深的被删去的祖先的值即可(保证这个子树贡献为 \(0\))。
#include<bits/stdc++.h>
using uint = unsigned;
constexpr uint mod = 998244353;
using info_ = std::array<uint, 3>;
const info_ info_i = {0, 0, 0};
/*
matrix : {1, 0, 0
[0], 1, 0
[1], [2], 1}
[0] = [0] + [0]'
[1] = [1] + [1]' + [2] * [0]'
[2] = [2] + [2]'
*/
inline info_ mul(const info_ &a, const info_ &b) {
info_ c;
(c[0] = a[0] + b[0]) >= mod && (c[0] -= mod);
c[1] = (1ull * a[2] * b[0] + a[1] + b[1]) % mod;
(c[2] = a[2] + b[2]) >= mod && (c[2] -= mod);
return c;
}
constexpr int maxn = 1e5 + 10;
int n;
int fa[maxn], d[maxn], dl[maxn], dr[maxn], dh[maxn];
int siz[maxn], dfn[maxn], dfp[maxn], rdfn[maxn], top[maxn], dn;
std::vector<int> son[maxn], ud[maxn], uh[maxn], ul[maxn], ur[maxn];
void dfs1(int u) {
dfn[u] = ++dn, dfp[dn] = u;
for (int v : son[u]) {
top[v] = v == son[u][0] ? top[u] : v;
dfs1(v);
}
rdfn[u] = dn;
}
namespace t1 {
int mx[maxn * 4];
inline void build(int k = 1, int l = 1, int r = n) {
mx[k] = 1;
if (l == r) return ;
int mid = l + r >> 1;
build(k << 1, l, mid), build(k << 1 | 1, mid + 1, r);
}
inline void update(int x, int y, int z, int k = 1, int l = 1, int r = n) {
if (x <= l && r <= y) return mx[k] = std::max(mx[k], z), void();
int mid = l + r >> 1;
if (x <= mid) update(x, y, z, k << 1, l, mid);
if (y > mid) update(x, y, z, k << 1 | 1, mid + 1, r);
}
inline int query(int x, int k = 1, int l = 1, int r = n) {
if (l == r) return mx[k];
int mid = l + r >> 1;
return std::max(mx[k], x <= mid ? query(x, k << 1, l, mid) : query(x, k << 1 | 1, mid + 1, r));
}
}
namespace t2 {
info_ tag[maxn * 4];
inline void pushtag(int k, const info_ &x) { tag[k] = mul(tag[k], x); }
inline void pushdown(int k) {
if (tag[k] != info_i) pushtag(k << 1, tag[k]), pushtag(k << 1 | 1, tag[k]), tag[k] = info_i;
}
inline void build(int k = 1, int l = 1, int r = n) {
tag[k] = info_i;
if (l == r) return ;
int mid = l + r >> 1;
build(k << 1, l, mid), build(k << 1 | 1, mid + 1, r);
}
inline void update(int x, int y, const info_ &z, int k = 1, int l = 1, int r = n) {
if (x <= l && r <= y) return pushtag(k, z), void();
pushdown(k);
int mid = l + r >> 1;
if (x <= mid) update(x, y, z, k << 1, l, mid);
if (y > mid) update(x, y, z, k << 1 | 1, mid + 1, r);
}
inline std::pair<uint, uint> query(int x, int k = 1, int l = 1, int r = n) {
if (l == r) return std::make_pair(tag[k][1], tag[k][2]);
pushdown(k);
int mid = l + r >> 1;
return x <= mid ? query(x, k << 1, l, mid) : query(x, k << 1 | 1, mid + 1, r);
}
}
inline void update(int x, uint delta) {
const info_ tag = {0, 0, delta};
int y = dfp[t1::query(dfn[x])];
while (top[x] != top[y]) {
t2::update(dfn[top[x]], dfn[x], tag);
x = fa[top[x]];
}
if (x != y) t2::update(dfn[y] + 1, dfn[x], tag);
}
uint f[maxn];
inline void solve() {
scanf("%d", &n);
for (int i = 0; i <= n; i++) son[i].clear(), ud[i].clear(), uh[i].clear(), ul[i].clear(), ur[i].clear();
ud[0] = {1};
for (int i = 2; i <= n; i++) {
scanf("%d%d%d%d", &fa[i], &dl[i], &dr[i], &dh[i]);
son[fa[i]].push_back(i);
d[i] = d[fa[i]] + 1;
dl[i] = d[i] - dl[i], dr[i] = d[i] - dr[i], std::swap(dl[i], dr[i]);
dh[i] = d[i] - dh[i];
}
for (int u = n; u >= 1; u--) {
siz[u] = 1;
for (int &v : son[u]) {
siz[u] += siz[v];
if (siz[v] > siz[son[u][0]]) std::swap(v, son[u][0]);
}
}
dn = 0, top[1] = 1, dfs1(1);
t1::build(), t2::build();
for (int i = 2; i <= n; i++) {
ud[d[i]].push_back(i);
uh[dh[i]].push_back(i);
ul[dl[i]].push_back(i);
ur[dr[i]].push_back(i);
}
memset(f, 0, sizeof(f)), f[1] = 1;
for (int d = 0; d <= n; d++) {
for (int u : uh[d]) {
auto [val, sz] = t2::query(dfn[u]);
f[u] = val, update(u, mod - sz);
t1::update(dfn[u], rdfn[u], dfn[u]);
}
for (int u : ul[d]) update(u, 1);
for (int u : ud[d]) {
const info_ tag = {f[u], 0, 0};
t2::update(dfn[u], rdfn[u], tag);
}
for (int u : ur[d]) update(u, mod - 1);
}
for (int i = 2; i <= n; i++) printf("%u ", f[i]); puts("");
}
int main() {
int t; scanf("%*d%d", &t);
for (; t--; ) solve();
return 0;
}
浙公网安备 33010602011771号