Note -「动态 DP」学习笔记
\(\mathcal{Introduction}\)
\(\mathcal{Problem~1}\)
给定序列 \(\{a_n\}\),其中 \(a_i\in\mathbb Z\),求其最大子段和(不能为空)。
很显然的 DP——令 \(f_i\) 为以 \(i\) 为右端点的最大子段和,\(g_i\) 为 \([1,i]\) 内的最大子段和,有:
f_i=\begin{cases}
a_i&i=1\\
\max\{f_{i-1}+a_i,a_i\}&\text{otherwise}
\end{cases}\\
g_i=\begin{cases}
a_i&i=1\\
\max\{g_{i-1},f_i\}&\text{otherwise}
\end{cases}
\end{cases}
\]
\(\mathcal O(n)\) 搞定。
不过我们来深究一下这个转移形式。以 \(f\) 的转移为例,我们把它写成“矩阵乘法”:
a_i&a_i\\
-\infty&0
\end{bmatrix}
\begin{bmatrix}
f_{i-1}\\
0
\end{bmatrix}=
\begin{bmatrix}
f_i\\
0
\end{bmatrix}
\]
当然啦,这不是传统意义的矩乘,我们实际上定义:
a&b\\
c&d
\end{bmatrix}
\begin{bmatrix}
e\\
f
\end{bmatrix}=
\begin{bmatrix}
\max\{a+e,b+f\}\\
\max\{c+e,d+f\}
\end{bmatrix}
\]
不过这看似突发奇想的定义有什么实际作用呢?
联想到矩阵快速幂,但快速幂需要保证矩阵具有结合律,即对于任意矩阵 \(A,B\) 和向量 \(\boldsymbol x\) 都应满足:
\]
把上面的定义代入,就会发现这种矩乘仍满足结合律!而本质上,就是由于 \(+\) 运算对于 \(\max\) 运算具有分配率(\(a+\max\{b,c\}=\max\{a+b,a+c\}\))。
所以到底有什么用嘛 qwq!我们走进下一题。
\(\mathcal{Problem~2}\)
给定序列 \(\{a_n\}\),其中 \(a_i\in\mathbb Z\),支持单点修改,询问区间最大子段和(不能为空)。
状态定义和上一题完全一样,设询问区间 \((l,r)\),那么边界为 \(f_l=g_l=a_l\)。考虑转移的通项,我们用列向量 \(\begin{bmatrix}f_i\\g_i\\0\end{bmatrix}\) 表示一个状态,直接从矩乘的角度设计转移矩阵,那么:
f_i\\
g_i\\
0
\end{bmatrix}=
\begin{bmatrix}
a_i&-\infty&a_i\\
a_i&0&a_i\\
-\infty&-\infty&0
\end{bmatrix}
\begin{bmatrix}
f_{i-1}\\
g_{i-1}\\
0
\end{bmatrix}
\]
记 \(A_i=\begin{bmatrix}a_i&-\infty&a_i\\a_i&0&a_i\\-\infty&-\infty&0\end{bmatrix}\)。我们希望求到 \(\begin{bmatrix}f_r\\g_r\\0\end{bmatrix}\),那么不断用上述公式展开右侧最后一项直到到达边界,有:
f_r\\
g_r\\
0
\end{bmatrix}=
A_r
\begin{bmatrix}
f_{r-1}\\
g_{r-1}\\
0
\end{bmatrix}=
A_rA_{r-1}
\begin{bmatrix}
f_{r-2}\\
g_{r-2}\\
0
\end{bmatrix}=\cdots=
A_{r}A_{r-1}\cdots A_{l+1}
\begin{bmatrix}
a_l\\
a_l\\
0
\end{bmatrix}
\]
注意到 \(\begin{bmatrix}a_l\\a_l\\0\end{bmatrix}=A_l\boldsymbol 0\),其中 \(\boldsymbol0\) 指零向量。那么进一步化简得:
f_r\\
g_r\\
0
\end{bmatrix}=
A_rA_{r-1}\cdots A_l\boldsymbol0
\]
相当于求区间矩阵的乘积,而在上文中已经得出,这种矩阵乘法具有结合律!所以可以用线段树维护区间矩阵乘积,单点修改时暴力修改单个矩阵和 \(\mathcal O(\log n)\) 个乘积即可。
复杂度 \(\mathcal O(k^3n\log n)\),其中 \(k\) 为方阵的阶,\(k=3\)。
这里有必要阐明一个许多动态 DP 入门讲解没有提到的细节。在线段树维护时,我们自然而然地维护了区间左 \(\times\) 右的积。以 pushup
函数为例:
void pushup ( const int rt ) { mt[rt] = mt[rt << 1] * mt[rt << 1 | 1]; }
但是,我们需要的 \(A_rA_{r-1}\cdots A_l\) 是从右乘到左的积呀,我们所定义的矩乘在同阶方阵中真的具有交换律么?
答案是否定的!而这样做的正确性来源于题目本身——翻转整个区间,其最大子段和不变!如果某些题目不满足翻转区间答案不变的性质,是不能交换乘法顺序的!
\(\mathcal{Code}\)
#include <cstdio>
#include <cstring>
#include <assert.h>
const int MAXN = 5e4, NINF = 0xc0c0c0c0; // NINF即-INF。
int n, m, a[MAXN + 5];
inline int max_ ( const int a, const int b ) { return a < b ? b : a; }
struct Matrix {
int n, m, mat[3][3];
Matrix () {}
Matrix ( const int tn, const int tm ): n ( tn ), m ( tm ), mat {} {}
inline int* operator [] ( const int key ) { return mat[key]; }
inline Matrix operator * ( Matrix t ) {
assert ( m == t.n );
Matrix ret ( n, t.m );
memset ( ret.mat, 0xc0, sizeof ret.mat );
// 这里注意,根据乘法定义,零矩阵的所有元素为-INF。
for ( int i = 0; i < n; ++ i ) {
for ( int k = 0; k < m; ++ k ) {
for ( int j = 0; j < t.m; ++ j ) {
ret[i][j] = max_ ( ret[i][j], mat[i][k] + t[k][j] );
}
}
}
return ret;
}
} zero ( 3, 1 ); // zero是真正意义上的零向量,注意与零矩阵区别。
inline void makeMat ( Matrix& a, const int v ) { // 构造 Ai。
a[0][0] = a[0][2] = v, a[0][1] = NINF;
a[1][0] = a[1][2] = v;
a[2][0] = a[2][1] = NINF;
}
struct SegmentTree {
Matrix mt[MAXN << 2];
inline void pushup ( const int rt ) { mt[rt] = mt[rt << 1] * mt[rt << 1 | 1]; }
inline void init ( const int rt, const int l, const int r ) {
mt[rt] = Matrix ( 3, 3 );
if ( l == r ) return makeMat ( mt[rt], a[l] );
int mid = l + r >> 1;
init ( rt << 1, l, mid ), init ( rt << 1 | 1, mid + 1, r );
pushup ( rt );
}
inline void update ( const int rt, const int l, const int r, const int x, const int v ) {
if ( l == r ) return makeMat ( mt[rt], v );
int mid = l + r >> 1;
if ( x <= mid ) update ( rt << 1, l, mid, x, v );
else update ( rt << 1 | 1, mid + 1, r, x, v );
pushup ( rt );
}
inline Matrix query ( const int rt, const int l, const int r, const int ql, const int qr ) {
if ( ql <= l && r <= qr ) return mt[rt];
Matrix ret ( 3, 3 ); // 注意这里ret并不是单位矩阵,所以第一次更新应当直接赋值。
int mid = l + r >> 1, f = 0;
if ( ql <= mid ) ret = query ( rt << 1, l, mid, ql, qr ), f = 1;
if ( mid < qr ) {
if ( ! f ) ret = query ( rt << 1 | 1, mid + 1, r, ql, qr );
else ret = ret * query ( rt << 1 | 1, mid + 1, r, ql, qr );
}
return ret;
}
} sgt;
int main () {
zero[0][0] = zero[1][0] = NINF;
scanf ( "%d", &n );
for ( int i = 1; i <= n; ++ i ) scanf ( "%d", &a[i] );
sgt.init ( 1, 1, n );
scanf ( "%d", &m );
for ( int i = 1, op, l, r; i <= m; ++ i ) {
scanf ( "%d %d %d", &op, &l, &r );
if ( ! op ) sgt.update ( 1, 1, n, l, r );
else printf ( "%d\n", ( sgt.query ( 1, 1, n, l, r ) * zero )[1][0] );
}
return 0;
}
诸如此类,定义矩阵乘法进行 DP 转移,继而动态维护转移矩阵的算法,就是所谓动态 DP(DDP?)。
\(\mathcal{Training}\)
「CF 750E」New Year and Old Subsequence
\(\mathcal{Description}\)
Link.
给定一个长度为 \(n\) 的数字串 \(s\),\(q\) 次询问 \(s[l..r]\) 需要删除多少个字符使得 \(\texttt{"2017"}\) 是其子串而 \(\texttt{"2016"}\) 不是。
\(n,q\le2\times10^5\)。
\(\mathcal{Solution}\)
先考虑整个串,设 \(f(i,0\sim4)\) 表示第 \(1\sim i\) 内,已经与 \(\texttt{"2017"}\) 匹配了长度为 \(0\sim4\) 的子串时,最小的删除次数。分转移为 \(2,0,1,7,6\) 和其它数字设计转移矩阵即可。
这道题体现了“不满足翻转区间答案不变的性质,不能交换乘法顺序”这一点。
「洛谷 P4719」「模板」"动态 DP" & 动态树分治
\(\mathcal{Description}\)
Link.
给定一棵 \(n\) 个结点的带权树,\(m\) 次单点点权修改,求出每次修改后的带权最大独立集。
\(n,m\le10^5\)。
\(\mathcal{Solution}\)
不考虑修改,显然 DP。令 \(f(u,0/1)\) 表示选 / 不选结点 \(u\),\(u\) 子树内的带权最大独立集。那么:
\]
引入修改,我们自然想用数据结构维护转移。那么就需要进行树链剖分(不一定是重链剖分,这里以复杂度更小的 LCT 为例)。假设 \(u\) 的实儿子是 \(s_u\),我们单独维护 \(s_u\) 的贡献,而把其它儿子一起考虑。设 \(g(u,0/1)\) 表示选 / 不选结点 \(u\),结点 \(u\) 及其虚儿子的子树们的带权最大独立集。有:
\]
用它来表示 \(f\):
\]
写成矩乘:
\]
令 \(G_u=\begin{bmatrix}g(u,0)&g(u,0)\\g(u,1)&-\infty\end{bmatrix}\),每次修改,仅会影响 \(\mathcal O(\log n)\) 个链头,也即是 \(\mathcal O(\log n)\) 个虚实交替的位置的 \(G\) 需要修改,就可以维护了。
详细题解:my solution(含实现细节说明)。
「洛谷 P6021」洪水
\(\mathcal{Description}\)
Link.
给定一棵 \(n\) 个点的带点权树,删除 \(u\) 点的代价是该点点权 \(a_u\)。\(m\) 次操作:
- 修改单点点权。
- 询问让某棵子树的根不可到达子树内任意一片叶子的代价。
\(n,m\le2\times10^5\)。
\(\mathcal{Solution}\)
还是不考虑修改啦,列出 DP:
\]
单独拿出实儿子 \(s_u\):
\]
定义矩乘的 \(+\) 为加法,\(\times\) 为取 \(\min\),有:
\]
仍可用 LCT / 树剖维护。若使用 LCT,询问 \(u\) 子树时,应 \(\operatorname{access}\) 原树上 \(u\) 的父亲,再 \(\operatorname{splay}\) \(u\),就能保证当前 \(u\) 的实链全部在子树内,输出 \(u\) 维护的矩乘答案即可。
详细题解:my solution。
「SP 6779」GSS7
\(\mathcal{Description}\)
Link.
给定一棵 \(n\) 个点的带点权树,\(q\) 次操作:
- 路径点权赋值。
- 询问路径最大子段和(可以为空)。
\(n,q\le10^5\)。
\(\mathcal{Solution}\)
嘛……其实就是引例的题搬到树上 qwq。应该可以熟练地列出转移矩阵了叭,设 \(f(u)\) 为以 \(u\) 为端点的最大子段和,\(g(u)\) 为前缀最大子段和,\(s_u\) 为 \(u\) 的重儿子(这题来练练树剖 www),有:
\]
注意在树剖跳重链求答案的时候,必须注意矩乘顺序。例如对于路径 \((u,v)\),钦定 \(u\) 为路径起点,当 \(u\) 向上跳时,转移矩阵按 DFN 降序;当 \(v\) 向上跳时转移矩阵按 DFN 升序,所以线段树应维护两个乘法顺序的矩阵积。
这道题有些卡常(而且我常数貌似很大 qwq),所以手玩一下转移矩阵的幂,发现:
\]
就可以 \(\mathcal O(1)\) 求出矩阵幂了。
详细题解:my solution。
「NOIP 2018」「洛谷 P5024」保卫王国
\(\mathcal{Description}\)
Link.
给定一棵 \(n\) 个点的带点权树,\(q\) 次询问,每次钦定两个点必须选或不选,求此时点权和最小的选点方案,使得一条边两端至少有一个点被选择。
\(n,q\le10^5\)。
\(\mathcal{Solution}\)
板不板?哪里需要什么倍增?
\(f(u,0/1)\) 为不选 / 选 \(u\) 点时子树最优方案,\(g(u,0/1)\) 为不选 / 选 \(u\) 点时除实儿子 \(s_u\) 以外的最优方案,有:
\]
考虑表示“钦定”:将不选的点的点权增加 \(+\infty\),将必选的点的点权增加 \(-\infty\),显然最优方案必选此时点权极小的“必选点”,必不选点权极大的“不选点”。那么正常地 \(\operatorname{splay}(root)\) 得到整棵树的 DP 结果即可。注意要将点权复原。
详细题解:懒了 www,需要参考代码见云剪切板。
Note -「动态 DP」学习笔记的更多相关文章
- Note -「Lagrange 插值」学习笔记
目录 问题引入 思考 Lagrange 插值法 插值过程 代码实现 实际应用 「洛谷 P4781」「模板」拉格朗日插值 「洛谷 P4463」calc 题意简述 数据规模 Solution Step 1 ...
- Note -「单位根反演」学习笔记
\(\mathcal{Preface}\) 单位根反演,顾名思义就是用单位根变换一类式子的形式.有关单位根的基本概念可见我的这篇博客. \(\mathcal{Formula}\) 单位根反演的 ...
- 「Manacher算法」学习笔记
觉得这篇文章写得特别劲,插图非常便于理解. 目的:求字符串中的最长回文子串. 算法思想 考虑维护一个数组$r[i]$代表回文半径.回文半径的定义为:对于一个以$i$为回文中心的奇数回文子串,设其为闭区 ...
- 「Link-Cut Tree」学习笔记
Link-Cut Tree,用来解决动态树问题. 宏观上,LCT维护的是森林而非树.因此存在多颗LCT.有点像动态的树剖(链的确定通过$Access$操作),每条链用一颗$splay$维护.$spla ...
- 「FHQ Treap」学习笔记
话说天下大事,就像fhq treap —— 分久必合,合久必分 简单讲一讲.非旋treap主要依靠分裂和合并来实现操作.(递归,不维护fa不维护cnt) 合并的前提是两棵树的权值满足一边的最大的比另一 ...
- 「线性基」学习笔记and乱口胡总结
还以为是什么非常高大上的东西花了1h不到就学好了 线性基 线性基可以在\(O(nlogx)\)的时间内计算出\(n\)个数的最大异或和(不需要相邻). 上述中\(x\)表示的最大的数. 如何实现 定义 ...
- 「AC自动机」学习笔记
AC自动机(Aho-Corasick Automaton),虽然不能够帮你自动AC,但是真的还是非常神奇的一个数据结构.AC自动机用来处理多模式串匹配问题,可以看做是KMP(单模式串匹配问题)的升级版 ...
- 【Java】「深入理解Java虚拟机」学习笔记(1) - Java语言发展趋势
0.前言 从这篇随笔开始记录Java虚拟机的内容,以前只是对Java的应用,聚焦的是业务,了解的只是语言层面,现在想深入学习一下. 对JVM的学习肯定不是看一遍书就能掌握的,在今后的学习和实践中如果有 ...
- 「ExLucas」学习笔记
「ExLucas」学习笔记 前置芝士 中国剩余定理 \(CRT\) \(Lucas\) 定理 \(ExGCD\) 亿点点数学知识 给龙蝶打波广告 Lucas 定理 \(C^m_n = C^{m\% m ...
随机推荐
- git 不小心把某个文件给 add 了 的解决方法
1.我不小心把这两个文件给add 进来本地仓库 2.解决 进入指令框 ,执行 git rm --cached 文件名 如下图 注意,必须指定文件否则会删除所有
- 手机访问web网页,使得显示自适应
//禁止浏览器伸缩<meta name="viewport" content="user-scalable=0">//手机访问web网页,使得显示自 ...
- java 多态 总结
1.前言 引用教科书解释: 多态是同一个行为具有多个不同表现形式或形态的能力. 多态就是同一个接口,使用不同的实例而执行不同操作. 通俗来说: 总结:多态的抽象类与接口有点相似: 父类不需要具体实现方 ...
- java集合分类
Java中的集合包括三大类,它们是Set.List和Map, Set(集) List(列表) Map(映射) 它们都处于java.util包中,Set.List和Map都是接口,它们有各自的实现类.( ...
- 推荐一个github国内访问加速神器GitHub520
一.介绍 对 GitHub 说"爱"太难了:访问慢.图片加载不出来. 注: 本项目还处于测试阶段,仅在本机测试通过,如有问题欢迎提 issues 本项目无需安装任何程序,通过修改本 ...
- vi与vim编辑器与解决vim编辑异常
目录 一:vi与vim编辑器 二:解决vim编辑异常 一:vi与vim编辑器 vim是vi的升级版编辑器,就是vim比vi丰富一些. 1.安装vim 命令 yum install vim -y 2.打 ...
- CICD流程
1.开发者git提交代码至gitlab仓库 2.jenkins从gitlab拉取代码,触发镜像构建 3.镜像上传至harbor私有仓库 4.镜像下载至执行机器--k8s node kubelet 5. ...
- HTML 基础2
当浏览器读到一个样式表,它就会按照这个样式表来对文档进行格式化.有以下三种方式来插入样式表: 外部样式表 内部样式 内联样式 外部样式表 当样式需要被应用到很多页面的时候,外部样式表将是理想的选择.使 ...
- TensorFlow 深度学习中文第二版·翻译完成
原文:Deep Learning with TensorFlow Second Edition 协议:CC BY-NC-SA 4.0 不要担心自己的形象,只关心如何实现目标.--<原则>, ...
- Tomcat临时目录及java.io.tmpdir对应的目录
最近客户现场的技术支持接连反馈了一个问题:导入数据的时候,上传的excel会在服务器上生成一个临时文件,而这个临时文件都在 tomcat 的安装目录下,如果上传次数比较多的话,就会导致tomcat安 ...