前言
温馨提示:本文将会讲述树链剖分(会提到 LCT 的做法)和全局平衡二叉树两种方法,所以强烈建议两种方法都想学习的读者仔细阅读完本文!(真不要脸)
动态 DP,它的模板题之所以和动态树分治放在一起,是因为它们有两个共同特点:
- 它们在没有修改时都是用于维护树上信息的
- 它们都是带修改的,即动态的
所以,动态 DP 一般是用于解决动态维护树上信息的问题
这类问题最初只是可爱的树形 DP,却被丧心病狂的出题人套上了修改操作,褪去了原本善良的模样
那么,在学习动态 DP 之前,你需要掌握以下前置知识:
DP- 矩阵乘法(以及其对 DP 的优化)
- 树链剖分(包括链上的线段树操作)
- 二叉查找树(亦称二叉排序树)
阅读完以上内容,我们就开始吧!
正文
放上模板题(简单版):P4719 【模板】“动态 DP”&动态树分治
我们先来考虑这样一件事情:如果没有修改,这道题你应该怎么做?
(大概是人人都会吧,要不然你也不会点进来看到这句话)
我们可以设 f u , 0 / 1 f_{u,0/1} fu,0/1 表示如果点 u u u 不选/选,该子树的答案是多少
那么显然有转移方程:(点
v
v
v 为点
u
u
u 的子节点,
v
a
l
u
val_u
valu 为点
u
u
u 的点权)
f
u
,
0
=
∑
max
(
f
v
,
0
,
f
v
,
1
)
f
u
,
1
=
v
a
l
u
+
∑
f
v
,
0
\begin{aligned} & f_{u, 0} = \sum \max(f_{v, 0}, f_{v, 1}) \\ & f_{u, 1} = val_u + \sum f_{v, 0} \end{aligned}
fu,0=∑max(fv,0,fv,1)fu,1=valu+∑fv,0
临界情况显然:对于叶子节点
l
l
l,有
f
l
,
0
=
0
,
f
l
,
1
=
v
a
l
l
f_{l, 0} = 0, f_{l, 1} = val_l
fl,0=0,fl,1=vall
好,现在我们已经会
m
=
0
m = 0
m=0 的部分分了!(然而这题并没有给,太不照顾作者这样的蒟蒻了)
接下来我们考虑带修
可以发现单点的修改操作最多只会修改一条链上的 f f f 值,所以我们可以想到用树链剖分维护树上的信息
为什么用树链剖分?
- 众所周知,一个点到根节点的路径上,至多只会经过 log n \log_n logn 数量级的重链,这就为时间复杂度提供了保证
- 众所周知,重链的底端都是从叶子节点,那么做 DP 时我们不需要花额外的力气去为其设置初始状态,同时也保证了树形结构自底向上转移的优美性
- 众所周知,树链剖分的优美性在于破树为序,所有的操作在维护的 DFS 序上进行,因此我们可以很方便地用修改序列的方式修改树
That’s why I choose 树链剖分!
为了迎合轻重儿子的设定,我们再设一个 g u , 0 / 1 g_{u, 0/1} gu,0/1,其中 g u , 0 g_{u, 0} gu,0 表示点 u u u 的所有轻儿子可选可不选~~(任其生死)~~的轻子树答案之和, g u , 1 g_{u, 1} gu,1 表示点 u u u 的所有轻儿子都不选的轻子树答案之和(下面的转移方程中为了保持结构优美性,把 v a l u val_u valu 加到了 g u , 1 g_{u, 1} gu,1 中)
所以状态转移方程就可以更新为:(此时点
v
v
v 为点
u
u
u 的重儿子)
f
u
,
0
=
g
u
,
0
+
max
(
f
v
,
0
,
f
v
,
1
)
f
u
,
1
=
g
u
,
1
+
f
v
,
0
\begin{aligned} f_{u, 0} &= g_{u, 0} + \max(f_{v, 0}, f_{v, 1}) \\ f_{u, 1} &= g_{u, 1} + f_{v, 0} \end{aligned}
fu,0fu,1=gu,0+max(fv,0,fv,1)=gu,1+fv,0
(g 临界情况同 f)
结构是不是优美许多了?至少不带求和符号了
那么,如果我们能够把 DP 结构化,使得我们每次修改只是修改一个地方的转移状态而不关心它的 DP 值具体修改成了什么,那么我们就能够用树剖的链上修改和查询(实质是线段树的单点修改和区间查询)来完成这些操作
说到 DP 结构化,就不得不提到一个东西——矩阵乘法
既然第二维有
0
/
1
0/1
0/1 两种状态,那我们设出这个状态矩阵:
[
f
u
,
0
f
u
,
1
]
\left[\begin{matrix} f_{u, 0} \\ f_{u, 1} \end{matrix}\right]
[fu,0fu,1]
然而你会发现,这个方程并不满足矩阵乘法的定义,因为包含有
max
\max
max 运算
没有关系,我们可以尝试定义一种矩阵乘法:对于矩乘式
M
∗
N
=
R
M * N = R
M∗N=R,我们设:
R
i
,
j
=
max
k
(
M
i
,
k
+
N
k
,
j
)
R_{i, j} = \max_k (M_{i, k} + N_{k, j})
Ri,j=kmax(Mi,k+Nk,j)
可以证明这个矩乘是满足结合律的,读者不妨用规模很小的矩阵手算一下
(
R
∗
S
)
∗
T
(R * S) * T
(R∗S)∗T 和
R
∗
(
S
∗
T
)
R * (S * T)
R∗(S∗T) 的结果(带参数),看它们是否相同
那么,我们就可以用线段树维护这个矩乘了:(但是要注意顺序,链顶在线段树左侧,线段树从左往右的顺序也是重链自上而下延伸的顺序)
T × [ f v , 0 f v , 1 ] = [ f u , 0 f u , 1 ] T \times \left[\begin{matrix} f_{v, 0} \\ f_{v, 1} \end{matrix}\right] =\left[\begin{matrix} f_{u, 0} \\ f_{u, 1} \end{matrix}\right] T×[fv,0fv,1]=[fu,0fu,1]
现在我们就来推左侧的转移矩阵 T T T,可以发现这是一个 2 ∗ 2 2 * 2 2∗2 的矩阵
我们把矩阵内的元素直接写成代数运算的形式有:
T × [ f v , 0 f v , 1 ] = [ max ( T 1 , 1 + f v , 0 , T 1 , 2 + f v , 1 ) max ( T 2 , 1 + f v , 0 , T 2 , 2 + f v , 1 ) ] T \times \left[\begin{matrix} f_{v, 0} \\ f_{v, 1} \end{matrix}\right] =\left[ \begin{matrix} \max(T_{1, 1} + f_{v, 0}, T_{1, 2} + f_{v, 1}) \\ \max(T_{2, 1} + f_{v, 0}, T_{2, 2} + f_{v, 1}) \end{matrix} \right] T×[fv,0fv,1]=[max(T1,1+fv,0,T1,2+fv,1)max(T2,1+fv,0,T2,2+fv,1)]
结合转移方程,那么 T T T 就显而易见了:
T = [ g u , 0 g u , 0 g u , 1 − ∞ ] T = \left[ \begin{matrix} g_{u, 0} & g_{u, 0} \\ g_{u, 1} & -\infin \end{matrix} \right] T=[gu,0gu,1gu,0−∞]
可以发现叶子节点的转移矩阵的第一列刚好就是其状态矩阵,所以答案的状态矩阵就是根节点所在重链上所有转移矩阵的乘积,最后答案对 f r o o t , 0 f_{root, 0} froot,0 和 f r o o t , 1 f_{root, 1} froot,1 取一个 max \max max 即可
这样就做完了
你没发现不对吗?g 是如何算出来的?
好吧其实是本蒟蒻偷懒了没放出来
(点
v
v
v 为点
u
u
u 的轻儿子)
g
u
,
0
=
∑
max
(
f
v
,
0
,
f
v
,
1
)
g
u
,
1
=
∑
f
v
,
0
\begin{aligned} g_{u, 0} &= \sum \max(f_{v, 0}, f_{v, 1}) \\ g_{u, 1} &= \sum f_{v, 0} \end{aligned}
gu,0gu,1=∑max(fv,0,fv,1)=∑fv,0
然后你就会发现,把 g 代入转移方程,整个方程根本没变嘛!
其实,之所以把 g 的转移方程放在这里介绍~~,是为了避免你想多了~~,是因为我们可以直接用线段树求出某一个轻儿子 f 的修改值(因为每次最多只修改一个节点),再进一步求出 g,然后再更新转移矩阵,这样单次修改是带两个 log \log log 的,在简单版里面可以跑过
总时间复杂度: O ( 2 3 n log 2 2 n ) O(2^3 n \log^2_2 n) O(23nlog22n)
贴上代码:
#include<iostream>
#include<cstdio>
#include<cstdlib>
#include<cassert>
#include<algorithm>
#define MAXN 100005
#define INF 0x3F3F3F3F
using namespace std;
int n, m, a[MAXN], tot, hd[MAXN], nxt[MAXN << 1], to[MAXN << 1];
int si[MAXN], hs[MAXN], fa[MAXN], top[MAXN], ed[MAXN], dfncnt, dfn[MAXN];
int f[MAXN][2];
struct Matrix
{
int v[2][2];
Matrix(int A = -INF, int B = -INF) { v[0][0] = v[0][1] = A, v[1][0] = B, v[1][1] = -INF; return; }
} mt[MAXN];
Matrix operator * (const Matrix &a, const Matrix &b)
{
Matrix c;
for(register int k = 0; k < 2; k++) for(register int i = 0; i < 2; i++) for(register int j = 0; j < 2; j++) c.v[i][j] = max(c.v[i][j], a.v[i][k] + b.v[k][j]);
return c;
}
struct Segment_Tree
{
#define ls (now << 1)
#define rs (now << 1 | 1)
#define mid ((l + r) >> 1)
int nd[MAXN << 2];
Matrix tr[MAXN << 2];
inline void Pushup(int now) { return tr[now] = tr[ls] * tr[rs], void(); }
inline void Build(int now, int l, int r)
{
if(l == r) return tr[now] = mt[nd[l]], void();
return Build(ls, l, mid), Build(rs, mid + 1, r), Pushup(now);
}
inline void Modify(int now, int l, int r, int pos)
{
if(l == r) return tr[now] = mt[nd[l]], void();
return (pos <= mid ? Modify(ls, l, mid, pos) : Modify(rs, mid + 1, r, pos)), Pushup(now);
}
inline Matrix Query(int now, int l, int r, int L, int R)
{
if(L <= l && r <= R) return tr[now];
if(R <= mid) return Query(ls, l, mid, L, R);
if(L > mid) return Query(rs, mid + 1, r, L, R);
return Query(ls, l, mid, L, R) * Query(rs, mid + 1, r, L, R);
}
#undef ls
#undef rs
#undef mid
} st;
inline void Link(int u, int v) { return ++tot, nxt[tot] = hd[u], to[tot] = v, hd[u] = tot, void(); }
inline void DFS1(int u, int f)
{
si[u] = 1, fa[u] = f;
for(register int i = hd[u]; i; i = nxt[i])
{
int v = to[i];
if(v == f) continue;
DFS1(v, u), si[u] += si[v];
if(si[v] > si[hs[u]]) hs[u] = v;
}
return;
}
inline void DFS2(int u, int tp)
{
dfn[u] = ++dfncnt, st.nd[dfncnt] = u, top[u] = tp, ed[tp] = max(ed[tp], dfncnt);
f[u][0] = mt[u].v[0][0] = mt[u].v[0][1] = 0, f[u][1] = mt[u].v[1][0] = a[u];
if(hs[u])
{
DFS2(hs[u], tp);
f[u][0] = max(f[hs[u]][0], f[hs[u]][1]);
f[u][1] += f[hs[u]][0];
}
for(register int i = hd[u]; i; i = nxt[i])
{
int v = to[i];
if(v == fa[u] || v == hs[u]) continue;
DFS2(v, v);
f[u][0] += max(f[v][0], f[v][1]);
f[u][1] += f[v][0];
mt[u].v[0][0] += max(f[v][0], f[v][1]);
mt[u].v[0][1] = mt[u].v[0][0];
mt[u].v[1][0] += f[v][0];
}
return;
}
inline void Modify(int u, int w)
{
mt[u].v[1][0] += w - a[u], a[u] = w;
Matrix pr, nt;
while(true)
{
pr = st.Query(1, 1, n, dfn[top[u]], ed[top[u]]);
st.Modify(1, 1, n, dfn[u]);
nt = st.Query(1, 1, n, dfn[top[u]], ed[top[u]]);
u = fa[top[u]];
if(!u) break;
mt[u].v[0][0] += max(nt.v[0][0], nt.v[1][0]) - max(pr.v[0][0], pr.v[1][0]);
mt[u].v[0][1] = mt[u].v[0][0];
mt[u].v[1][0] += nt.v[0][0] - pr.v[0][0];
}
return;
}
int main()
{
scanf("%d%d", &n, &m);
for(register int i = 1; i <= n; i++) scanf("%d", a + i);
for(register int i = 1; i < n; i++)
{
int u, v; scanf("%d%d", &u, &v);
Link(u, v), Link(v, u);
}
DFS1(1, 0), DFS2(1, 1);
st.Build(1, 1, n);
while(m--)
{
int u, w; scanf("%d%d", &u, &w), Modify(u, w);
Matrix ans = st.Query(1, 1, n, dfn[top[1]], ed[top[1]]);
printf("%d\n", max(ans.v[0][0], ans.v[1][0]));
}
return 0;
}
前面说了,本文还会介绍 LCT 的做法~~(但我没有说给代码嗷)~~
其实和轻重链剖分差不太多,就是每个点上要多维护一个 f 数组(因为你无法用直接线段树求 f 数组的值)
为了方便计算,可以先把每个点都设为轻儿子,这样就可以很方便地初始化 g 数组了
Access 操作就类似树剖,不过每次 Splay 的时候要更新 f 数组,Splay 完就可以顺便更新 g 数组了
还有就是注意 Pushup 的时候矩乘的顺序为左子树-根节点-右子树
剩下的都是基本操作,没什么好说的
LCT 的时间复杂度是 O ( 2 3 n log 2 n ) O(2^3 n \log_2 n) O(23nlog2n),比轻重链剖分少了一个 log \log log,但实现起来常数巨大
做完上面那题,相信你一定会抖擞精神打开这一题:P4751 【模板】“动态DP”&动态树分治(加强版)
稍微改改数组大小,然后自信地提交上去~~,带着绝望和迷茫的眼神,收获满屏的 TLE、MLE 和 RE~~
如果你还不气馁,码了一遍 LCT,结局无非是——在漫长的卡常之路上,终屈服于 LCT 的巨大常数
此时,你点开了题解,映入眼帘的七个大字,如一道温暖的光照入你的心间,化开了绝望与迷茫堆积而成的雪——
全局平衡二叉树
首先,全局平衡二叉树的做法是基于 LCT 做法的,只是换了一种常数较小的数据结构(因为本题并不需要动态修改树的结构)
全局平衡二叉树在重链剖分的基础上,把重边看做实边,轻边看做虚边,其实就类似于静态的 LCT
构造的话,我们可以先重链剖分,类似 LCT,在每条链上以深度为关键字维护一颗 BST,同时让它的树根指向链顶的父亲
那么我们如何让它全局平衡呢?答案是把链上的每个点赋一个点权,权值为 s i z e + 1 size + 1 size+1( s i z e size size 特指点 u u u 的所有轻子树大小之和),然后每次以点权为关键字将带权重心作为根节点,同时把整条链剖成两条并分治(这里的思想其实有些类似于点分治)
可以把这样的操作抽象于下面这张图:

即重链的每个点上都挂着若干颗轻子树,欲使得全局平衡,则需求出其带权重心(可以二分,当然也可以暴枚,不影响总复杂度)
这个时候会发现:修改如果暴力跳父亲,可以做到严格的 log 2 n \log_2 n log2n!证明也很简单,只需要一句话——以点 u u u 的父亲为根的子树大小至少是以它自己为根的子树大小的 2 倍,这样,从点 u u u 向上跳不超过 log 2 n \log_2 n log2n 次,子树大小就会达到 n n n,即跳到树根
这就是数据结构的奥妙——把暴力的方法通过与构造巧妙结合,达到一个趋于平衡的最优复杂度
所以,修改的时候我们可以直接暴力向上跳父亲(实际上就是 LCT 中的 Access 操作,只不过没有用 Splay 维护),类似 LCT 的做法,跳重边时(即在重链上跳时)维护 f 数组,跳轻边时维护 g 数组即可
贴上代码:
#include<iostream>
#include<cstdio>
#include<cstdlib>
#include<cassert>
#define MAXN 1000005
#define INF 0x3F3F3F3F
using namespace std;
int n, m, a[MAXN], Lastans;
int tot, hd[MAXN], nxt[MAXN << 1], to[MAXN << 1];
int si[MAXN], fa[MAXN], hs[MAXN], len, seq[MAXN];
inline void Link(int u, int v) { return ++tot, nxt[tot] = hd[u], to[tot] = v, hd[u] = tot, void(); }
struct Matrix
{
int v[2][2];
Matrix() { v[0][0] = v[0][1] = v[1][0] = v[1][1] = -INF; return; }
};
Matrix operator * (const Matrix &a, const Matrix &b)
{
Matrix c;
for(register int k = 0; k < 2; k++) for(register int i = 0; i < 2; i++) for(register int j = 0; j < 2; j++) c.v[i][j] = max(c.v[i][j], a.v[i][k] + b.v[k][j]);
return c;
}
struct Binary_Search_Tree
{
int rt, ls[MAXN], rs[MAXN], fa[MAXN], f[MAXN][2];
Matrix val[MAXN], sum[MAXN];
inline void Pushup(int now) { return sum[now] = sum[ls[now]] * val[now] * sum[rs[now]], void(); }
inline bool Checkroot(int u) { return ls[fa[u]] != u && rs[fa[u]] != u; }
inline int Build(int l, int r)
{
int cnt = 0, pre = 0, pos, now;
for(register int i = l; i <= r; i++) cnt += si[seq[i]] - si[hs[seq[i]]];
for(register int i = l; i <= r; i++) if((pre += si[seq[i]] - si[hs[seq[i]]]) >= (cnt >> 1)) { pos = i, now = seq[i]; break; }
if(l < pos) ls[now] = Build(l, pos - 1), fa[ls[now]] = now;
if(pos < r) rs[now] = Build(pos + 1, r), fa[rs[now]] = now;
return Pushup(now), now;
}
inline void Modify(int u, int w)
{
val[u].v[1][0] += w - a[u], a[u] = w;
while(u)
{
if(Checkroot(u))
{
if(fa[u])
{
val[fa[u]].v[0][0] -= max(sum[u].v[0][0], sum[u].v[1][0]);
val[fa[u]].v[1][0] -= sum[u].v[0][0];
}
Pushup(u);
if(fa[u])
{
val[fa[u]].v[0][0] += max(sum[u].v[0][0], sum[u].v[1][0]);
val[fa[u]].v[0][1] = val[fa[u]].v[0][0];
val[fa[u]].v[1][0] += sum[u].v[0][0];
}
}
else Pushup(u);
u = fa[u];
}
return;
}
} bst;
inline void DFS1(int u, int f)
{
si[u] = 1, fa[u] = f;
for(register int i = hd[u]; i; i = nxt[i])
{
int v = to[i];
if(v == f) continue;
DFS1(v, u), si[u] += si[v];
if(si[v] > si[hs[u]]) hs[u] = v;
}
return;
}
inline int DFS2(int u, int tp)
{
if(hs[u]) DFS2(hs[u], tp);
bst.val[u].v[0][0] = 0, bst.val[u].v[1][0] = a[u];
for(register int i = hd[u]; i; i = nxt[i])
{
int v = to[i];
if(v == fa[u] || v == hs[u]) continue;
int k = DFS2(v, v);
bst.fa[k] = u;
bst.val[u].v[0][0] += max(bst.sum[k].v[0][0], bst.sum[k].v[1][0]);
bst.val[u].v[1][0] += bst.sum[k].v[0][0];
}
bst.val[u].v[0][1] = bst.val[u].v[0][0];
if(u == tp)
{
len = 0; int v = u;
while(v) seq[++len] = v, v = hs[v];
return bst.Build(1, len);
}
return 0;
}
int main()
{
#ifdef FILE
freopen("Input.in", "r", stdin);
freopen("Output.out", "w", stdout);
#endif
scanf("%d%d", &n, &m);
for(register int i = 1; i <= n; i++) scanf("%d", a + i);
for(register int i = 1; i < n; i++)
{
int u, v; scanf("%d%d", &u, &v);
Link(u, v), Link(v, u);
}
DFS1(1, 0), bst.sum[0].v[0][0] = bst.sum[0].v[1][1] = 0, bst.sum[0].v[0][1] = bst.sum[0].v[1][0] = -INF, bst.fa[bst.rt = DFS2(1, 1)] = 0;
while(m--)
{
int u, w; scanf("%d%d", &u, &w), u ^= Lastans;
bst.Modify(u, w), printf("%d\n", Lastans = max(bst.sum[bst.rt].v[0][0], bst.sum[bst.rt].v[1][0]));
}
return 0;
}
173



被折叠的 条评论
为什么被折叠?



