ddp 和 gbbst 口胡报告

· · 题解

动态 dp 入门

给道入门题。

题目:DTOJ #4579. Hello 2019

一个串是好的,当且仅当其包含子序列 9102 且不包含子序列 8102. (子序列不一定连续)

有个数字串 S,每次询问一个区间 (l,r),求至少从串中删掉多少个字符,方能使子串 S(l,r) 是好的。

题解:首先 reverse,然后记一个 $f_{i,0/1/2/3/4}$,表示恰好匹配 $0/1/2/3/4$ 个要删几个字符,然后用 $5\times 5$ 的矩阵转移,每次把 $[l,r]$ 内的矩阵乘起来。详细的建议自己推。 ## 动态树分治入门 题目:洛谷 P4719 【模板】"动态 DP"&动态树分治 先写出一般最大权独立集式子: $$ \begin{aligned} f_{u,0} =& \sum_{v}\max(f_{v,0}, f_{v,1})\\ f_{u,1} =& w_u+\sum_{v} f_{v,0} \end{aligned} $$ 其中 $f_{u,0/1}$ 表示 $u$ 不选/选的答案。 然后对于整棵树进行树剖,分开记重儿子和轻儿子的答案。 $g_{u,0}$:$u$ 的所有轻儿子全不选的总和加上 $w_u$。 $g_{u,1}$:$u$ 的所有轻儿子选或不选的最大值总和。 $f_{u,0}$:$u$ 不选的答案。 $f_{u,1}$:$u$ 选或不选的答案。 记 $v$ 为重儿子,有: $$ \begin{aligned} f_{u,0} =& f_{v,1} + g_{u, 1}\\ f_{u,1} =& \max(f_{v,1} + g_{u, 1}, f_{v,0} + g_{u,0}) \end{aligned} $$ 然后可以把 $f_u$、$g_u$ 和 $f_v$ 写成矩阵,要点是 $f_u$ 和 $f_v$ 长得一样,$g_u$ 只和 $u$ 有关: $$ \begin{bmatrix} f_{u, 0} & f_{u, 1} \end{bmatrix} = \begin{bmatrix} f_{v, 0} & f_{v,1} \end{bmatrix} \times \begin{bmatrix} -\infty & g_{u,0}\\ g_{u, 1} & g_{u,1} \end{bmatrix} $$ 当然这里矩阵乘法的定义是 $(+, \max)$。 这样一个**节点**到**重链顶端**的答案就是 $\begin{bmatrix}0 & 0\end{bmatrix}$ 和这条链的叶结点从底到顶的一堆矩阵相乘。 此时如果修改一个节点,只需改的它的 $g_u$ 矩阵。 然后跑到重链顶端,求出新矩阵,对链顶父亲的 $g_u$ 修改一下,然后继续跑。 实现上需要注意矩阵是从链底端乘到链顶端,倒序。 似乎只需要线段树上 $\texttt{query}$ 的时候是右子树乘左子树即可。 几个要点: 1. 扩展 $f_{u,1}$ 的定义:$u$ 选的答案 $\to$ $u$ 选或不选的答案。 2. 把除重儿子外的东西打包到 $g_u$ 考虑。 3. 来自[P4719 最高赞题解](https://www.luogu.com.cn/blog/Tweetuzki/solution-p4179):每条重链的链尾都是叶子节点,且只有叶子节点没有重儿子。这为动态规划的初始状态和转移方式做了保障。 [提交记录](https://www.luogu.com.cn/record/76042967) ## 全局平衡二叉树 全局平衡二叉树 lxl:**G**lobal **B**iased **T**ree(**GBT**) Yang Zhe:**G**lobal **B**alanced **BST**(**GBBST**) [洛谷 P4751 【模板】"动态DP"&动态树分治(加强版)](https://www.luogu.com.cn/problem/P4751) 数据范围已经不允许 $O(\log^2 n)$ 的做法了。 注意到上面那个做法的劣势在于:每次树剖查询的是若干条重链,而就算一条链的长度 $l=1$,单次查询重链复杂度都是 $O(\log n)$,因为是开一棵线段树维护所有节点的矩阵。总的合并复杂度就是 $O(\log^2 n)$。 考虑修改一下,对每条重链单独用一个形态不变的二叉树维护,网络上大部分博客都是非 leafy 结构,类似平衡树。但写成 leafy 的线段树也不会错。 > leafy 两倍常数 —— rsy 问题在于如果还是按照一般的线段树建树,每次找中点,$O(\log n)\sum O(\log l)$ 仍可以卡到 $O(\log^2 n)$。 考虑记: $$ \mathrm{Lsize}(u)=1+\sum_{v\in \mathrm{LightSon}(u)} \mathrm{size}_v $$ 即所有轻儿子 $\mathrm{size}$ 之和加上自己。 这时候,如果在重链上按照 $\mathrm{Lsize}$ 为权值,找到带权中点,然后递归到左右建(平衡/线段)树来维护信息,均摊下来修改的复杂度就是 $O(\log n)$ 的。 为什么?不知道。难道你知道为什么 Splay 均摊是 $O(q\log n )$ 的吗? 详细证明可见 [Yang Zhe - QTREE 解法的一些研究](https://wenku.baidu.com/view/75906f160b4e767f5acfcedb) [提交记录](https://www.luogu.com.cn/record/76069612)(无 IO 优化) 关于步骤: 1. 树链剖分。 2. 找出连接根节点的重链,对**这条重链**建立二叉搜索树,然后对连接这条重链的其他重链**递归**建立。 3. 一棵二叉搜索树根节点要维护这棵二叉树的**所有矩阵乘积**。 4. 修改 $a_u \gets x$: 1. 找到 $u$ 所对应的平衡/线段树节点。 2. 修改 $a_u$ 对应的矩阵。 3. 暴力往上跳,每次都 push_up。 4. 到根了,查看这棵平衡/线段树**所对应的重链**,找到链头的父亲,记作新的 $u$,没父亲就可以退出了。 5. 回到步骤 4.2。 关于实现: 1. 上文有说是对每条重链依照带权中点二分建立一棵平衡树,**这样是对的**,但是有一些其他的实现方式。比如大部分博客会把轻儿子向父亲的平衡/线段树节点连一条**虚边**。 2. 所谓**虚边**:因为它父亲所在的重链自己构成了一棵二叉树,其父亲无法变成多叉树,指向轻儿子,但是轻儿子可以指向父亲。即**虚边**,有**认父不认子**的特点。推荐这样的实现方式,这样在修改的时候就可以从 $u$ 节点跳直接到顶,省去不少麻烦。详细见代码。 ### Code ```cpp // Problem: P4751 【模板】"动态DP"&动态树分治(加强版) // From: Luogu // URL: https://www.luogu.com.cn/problem/P4751 // Time: 2022-05-19 21:21 // Author: lingfunny #include <bits/stdc++.h> #define LL long long using namespace std; const int mxn = 1e6+10, inf = 1e9; int n, m, lst, a[mxn], rt, f[mxn][2], g[mxn][2]; vector <int> G[mxn]; struct mat { static const int V = 2; int a[V][V]; mat(int a00 = 0, int a01 = -inf, int a10 = -inf, int a11 = 0) { a[0][0] = a00, a[0][1] = a01, a[1][0] = a10, a[1][1] = a11; } inline mat operator * (const mat &rhs) const { mat res; for(int i = 0; i < V; ++i) for(int j = 0; j < V; ++j) { res.a[i][j] = -inf; for(int k = 0; k < V; ++k) res.a[i][j] = max(res.a[i][j], a[i][k] + rhs.a[k][j]); } return res; } inline void show() { for(int i = 0; i < V; ++i) for(int j = 0; j < V; ++j) printf("%d%c", a[i][j], " \n"[j==V-1]); } } gm[mxn]; struct node { int lc, rc, anc; mat u, s; } nd[mxn]; inline void psup(int u) { node &o = nd[u]; o.s = nd[o.rc].s * o.u * nd[o.lc].s; // 反着乘,原因见上文 P4719 } int sz[mxn], lsz[mxn], dep[mxn], fa[mxn], son[mxn], top[mxn], End[mxn], dfn[mxn], mp[mxn], dfc; // lsz[u]: Lsize[u] // top/End: 重链顶/底 // mp: mp[dfn[u]] = u void dfs(int u, int f) { lsz[u] = sz[u] = 1, dep[u] = dep[f] + 1, fa[u] = f; for(int v: G[u]) if(v != f) { dfs(v, u), sz[u] += sz[v]; if(sz[v] > sz[son[u]]) son[u] = v; } lsz[u] = sz[u] - sz[son[u]]; } void dfs2(int u) { End[u] = mp[dfn[u] = ++dfc] = u; g[u][0] = a[u]; if(son[u]) top[son[u]] = top[u], dfs2(son[u]), End[u] = End[son[u]]; for(int v: G[u]) if(v != fa[u] && v != son[u]) top[v] = v, dfs2(v), g[u][0] += f[v][0], g[u][1] += f[v][1]; f[u][0] = f[son[u]][1] + g[u][1]; f[u][1] = max(f[u][0], f[son[u]][0] + g[u][0]); gm[u] = mat(-inf, g[u][0], g[u][1], g[u][1]); } int sbuild(int L, int R) { if(L > R) return 0; LL sum = 0, qsum = 0; for(int i = L; i <= R; ++i) sum += lsz[mp[i]]; for(int i = L, o; i <= R; ++i) { qsum += lsz[mp[i]]; if(qsum * 2 > sum) { o = mp[i]; node &u = nd[o]; u.u = gm[o]; u.lc = sbuild(L, i-1), nd[u.lc].anc = o; u.rc = sbuild(i+1, R), nd[u.rc].anc = o; psup(o); return o; } } return -114514; } int build(int Tp) { int Ed = End[Tp], X = sbuild(dfn[Tp], dfn[Ed]); for(int i = dfn[Tp]; i <= dfn[Ed]; ++i) { const int &u = mp[i]; for(int v: G[u]) if(v != son[u] && v != fa[u]) nd[build(v)].anc = u; } return X; } inline void modify(int u, int x) { gm[u].a[0][1] += x - a[u]; a[u] = x; nd[u].u = gm[u]; while(u) { int F = nd[u].anc; if(nd[F].lc != u && nd[F].rc != u && F) { // 如果当前节点到父亲的边是虚边 mat bef = nd[u].s; psup(u); mat aft = nd[u].s; int _f0 = max(bef.a[0][0], bef.a[1][0]), _f1 = max(bef.a[0][1], bef.a[1][1]), f0 = max(aft.a[0][0], aft.a[1][0]), f1 = max(aft.a[0][1], aft.a[1][1]); mat &r = nd[F].u; r.a[0][1] += f0 - _f0; r.a[1][0] += f1 - _f1, r.a[1][1] += f1 - _f1; gm[F] = r; } else psup(u); u = F; } } signed main() { scanf("%d%d", &n, &m); for(int i = 1; i <= n; ++i) scanf("%d", a+i); for(int i = 1, u, v; i < n; ++i) scanf("%d%d", &u, &v), G[u].push_back(v), G[v].push_back(u); dfs(1, 0), top[1] = 1, dfs2(1), rt = build(1); while(m--) { int x, y; scanf("%d%d", &x, &y); x ^= lst; modify(x, y); printf("%d\n", lst=max(nd[rt].s.a[0][1], nd[rt].s.a[1][1])); } return 0; } ``` ~~我能水一篇题解吗~~