ZROI 学习笔记之动态规划
都别催!!!等我有时间了例题和详细讲解都会补回来的!!!
CHANGE LOG
- 2023.10.11 补充细化 7 月 26 日单调队列 / 斜率优化 DP 部分。
7.24 区间 DP & 树形 DP
1. 区间 DP
- 合并:即将两个或多个部分进行整合,当然也可以反过来;
- 特征:能将问题分解为能两两合并的形式;
- 求解:对整个问题设最优值,枚举合并点,将问题分解为左右两个部分,最后合并两个部分的最优值得到原问题的最优值。
2. 树形 DP
- 经典问题:树的直径
- 换根 DP:《取消父子关系,再给人家当儿子》
wcy 硬垒
算法介绍:wcy 硬垒
命名原因:wcy 把扫描线合理外推,运用到甚至可能不算扫描线的方面,并称其为“扫描线的思想”,惨遭某人批评,于是,wcy 硬垒应运而生。
算法内容:类似扫描线的思想,将点集(区间端点集)按照某个坐标排序后分层,按层次从低到高逐层解决或层层合并,转换为一个或多个单层问题,再进行 DP 或查询操作。
例题:CF1372E,暑假模拟赛 D1T2。
(虽然这两道题 wcy 自己都没有把正解完全想出来,但由于他的想法与题解十分接近,他固执的称这就是所谓的 wcy 硬垒。)
7.25 状压 DP & 数位 DP
1. 状压 DP
状压 DP 是动态规划的一种,通过将状态压缩为整数来达到优化转移的目的。
—— OI WIKI
2. 数位 DP
3. Ex:线性基
7.26 DP 优化
1. 前置知识
1.1 单调队列
笔者觉得这个东西本质还是单调栈,所以我们从单调栈讲起。
-
什么是单调栈?
单调栈用于解决 NGE(Next Greater Element,下一个更大元素) 问题。
-
如何实现单调栈?
首先其必须是一个栈。考虑在每次入栈时将当前元素与栈顶元素比较,如果当前元素更小就老老实实入栈;但如果更大,那么该元素就应该是栈顶元素的 NGE,然后将栈顶元素弹出,直至当前元素比栈顶元素小。
这个做法应该不难理解,就是保持栈中的单调性。在我们举的例子中,就是维持栈中元素从底到顶单调不增。接下来,我们回到正题。
-
什么是单调队列?
单调队列用于解决 滑动窗口 问题,一般是 一个序列的每个定长连续子序列的最大 / 最小元素。
-
如何实现 / 其和单调栈的异同?
注意到单调栈中维护的其实就是 从最开始到当前位置的末项为当前位置的单调序列,比如解决下一个更小元素(就是刚才单调栈的例子反过来)的单调栈,其栈底的元素必然是目前为止最小的元素。
我们需要的单调队列,就是需要这么一个单调序列,只不过其栈底元素应该是 从子序列起点起的最小元素。那么,这和单调栈有什么差异的?
差一个 时效性,即栈底元素可能已经 不在要求的子序列中 了,这时我们就应该 弹出栈顶元素。发现什么了吗?
这是一个 双端队列。所以,我们只需要:
- 将栈顶作为队尾,将栈底作为队首;
- 插入元素时,将队尾劣于当前元素的元素弹出,再插入当前元素;
- 及时将队首失去时效性的元素弹出;
- 队首元素即是滑动窗口答案。
可以感性的理解为,队尾在存储后备人才,队首是先锋。而如果你作为后备人才时,比人家年纪大(位置靠前),还比人家菜(更劣),那你就该
pop
了,把人家送进去吧;如果你虽然年纪大,但是比人家牛,那就让人家做你的替补,你的先锋全部退休之时,就是你成为先锋的那一天。
滑动窗口作为一类优化的模型,经常被广泛套用,故掌握单调队列极其重要。
1.2 DP 规模的描述
我们通常用 \(t\)D / \(e\)D 来描述动态规划 规模 的类型,其中 \(t\) 表示问题大小,\(e\) 表示转移时依赖子问题的大小。即状态数 \(n^t\),每个状态依赖于 \(n^e\) 个前驱状态的信息。除非问题有特殊性质,解决 \(t\)D / \(e\)D 动态规划需要 \(O(n^{t+e})\) 的时间复杂度。
下文中可能会用这种描述来定义优化的常见应用场景。这里也同时给出一些常见的规模:
- \(1\)D / \(1\)D:最长上升子序列。
- \(2\)D / \(0\)D:最长公共子序列,普通背包问题。
- \(2\)D / \(1\)D:多源最短路 Floyd。
2. 基于决策单调性的优化
2.1 大前提:决策单调性
-
如何理解决策单调性?
通常情况下,决策单调性 体现在 1D 维度 上。形式化定义是,设 \(f\) 表示当前层,\(g\) 表示下一层,设 \(g_i\) 从 \(f_{p_i}\) 处转移且 \(p_i\) 最小 / 最大,则 \(i<j \implies p_i \leq p_j\)。
形象的理解,如果我们把每一阶段的状态与上一阶段决策这一状态的状态做连边(即只取状态转移时最优的状态转移画出边来),形成一个分层图,那么这里面每一条路径必然 不交。
-
判断:四边形不等式
四边形不等式:\(a<b<c<d \implies w(a,d)+w(b,c) \text{ worse than } w(a,c)+w(b,d)\),即 包含劣于相交。
设 \(f_j\) 可以从 \(f_i+w(i,j)\) 转移到得,如果转移中的贡献函数 \(w(u,v)\) 满足四边形不等式,那么 \(f\) 具有决策单调性。
利用反证法即可证明这一结论,此处省略。同时,决策单调性形象的理解也让我们很容易理解这一结论的正确性——因为单调性决策的最优转移路径 不交。
2.2 单峰性 - 单调队列优化
单峰性:对于某个状态的最优转移点,其左边的贡献向左单调递减,右边也向右单调递减。(一个单峰函数)
对于这样的状态,我们直接施展单调队列维护就可以了。\(O(n^2)\) 的转移可以降到 \(O(n)\)。
最经典的一类问题是,如果转移方程可以写成 \(f_i = \min { f_j + cost_{i,j} }\),且可以证明(到考场上可以换成猜想)这个问题具有决策单调性,(表现为若 \(f_i\) 转移自 \(f_j\),则对于任意 \(k>i\) 如果从 \([1,i-1]\) 中转移则 \(f_j\) 仍然最优。)那么一般就可以直接使用单调队列维护了。
2.3 分治法
- 常见于 \(2\)D/\(1\)D 的 DP。
- 对于每个区间,暴力寻找中点的决策点,然后递归寻找两边,由于决策单调性,中点左边的点决策点也必然在中点决策点左边。
- 时间复杂度 \(O(n \log n)\)。
2.4 斜率优化
常见于 \(1\)D/\(1\)D 的 DP。
一般来说是考虑能否将转移方程写成
即一些只和 \(i,j\) 中一个有关的项和与两者都有关的项。然后考虑写成
的形式。我们将他对比一次函数截距式:
便得出一个 \(y = f_j + cost_j, k = -F_i, x= F_j, b= f_i - cost_i\) 的 一次函数。
有什么用呢?
我们来看看这个一次函数有什么意义。
- 首先,对于每一个要求的状态 \(i\),我们要求的值 \(f_i = b + cost_i\),所以我们的目的就是 最大 / 小化截距。(最大还是最小看转移方程取 \(\min / \max\)。)
- 注意到斜率 \(k\) 只与 \(i\) 相关,而 \(i\) 已确定,所以 斜率 是定值。
- 最后,注意到这个空间中的每一个点对 \((x,y)\) 实际上是 \((F_j , f_j + cost_j)\),也就是说每一个点对应一个状态 \(j\)。
- 这样,转移就变成了 最优化定斜率过某一定点的直线的纵截距。最优化指最大 / 小化中的满足题意的一个。
这个问题如何解决?考虑哪个点会成为最优化截距时直线经过的点。对于每一个斜率 \(k\),这些有可能被选中的点就形成了一个凸包。于是我们 维护凸包 即可。
一般来说,我们解决的问题斜率是 单调增长 的,且插入的点的 \(x\) 坐标有序,即对于 \(i \ in [1,n]\),\(F_i\) 单调,且转移过程是 \(1 \to n\) 遍历,那么我们就可以直接使用 单调队列 维护;如果查询的斜率不单调,但 \(x\) 坐标单调,则可以使用单调栈维护凸包,在上面二分寻找查询斜率的直线与凸包的切点;如果没一个单调的,那就需要使用李超线段树或动态凸包维护了。
2.5 二分队列
- 可以解决状态转移需要同阶段其他状态的情况。(比如 \(dp(i,j)\) 需要由 \(dp(i,k)\) 推出。)
- 要求转移时的贡献能快速计算,一般是 \(O(1)\)。
- 设当前要求 \(dp_i\),考虑建立存储三元组 \((j,l,r)\) 的队列,表示 \(l \sim r\) 的最优决策点是 \(j\)。
- 每次判断队首三元组是否过时,若 \(r<i\) 则弹出队首。否则将 \(l\) 赋值为 \(i\)。
- 不想贺魏老师的博客了……有什么独到的理解了我再写。
3. 常见背包优化
3.1 缺一分治
- 优化背包 DP
- 每次向下递推时对另一个区间背包(所以要求你可撤销),比如处理 \([l,mid)\) 的时候把 \([mid,r)\) 背包了,这样等到了边界条件的时候,你就获得了一个必然不选 \(l\),对其他物品进行背包的 DP 结果了。
3.2 倍增背包
- 或者我们称为 二进制拆分 + 多重背包。
- 将一个物品的数量 \(w\) 转化为 \(2\) 的幂次,注意 不是 \(w\) 的 二进制表示!而是 \(2^0,2^1,2^2,\dots,2^k,w-\sum \limits_{i=0}^{k}2^i\),用这种方法拆分,就可以在 \(\log w\) 内用这几项的所有组合表达 \(1 \sim w\) 的所有选择了。
4. 数据结构优化
- 线段树
- 倍增
- 并查集
5. 期望 DP 的优化
- 观察性质
double
转int
7.27 矩阵乘法及其相关
1. 前置知识
1.1 矩阵乘法
-
具有结合律,但不具有交换律。(故可以使用快速幂)
-
基础运用:优化线性递推与线性 DP。(详见山东游记)
-
广义矩阵乘法:
\[C_{i,j} = \bigoplus \limits_{k=1}^n A_{i,k} \otimes B_{k,j} \]只要 \(\bigotimes\) 满足 结合律,且 \(\bigotimes\) 对 \(\bigoplus\) 满足 分配律,那么其就满足 结合律。常见的广义矩阵乘法比如:
- \(\max,+\) 卷积,比如求树的最大权独立集,即 DDP 的模板题。
- \(\operatorname{or},\operatorname{and}\) 卷积,图论的邻接矩阵在此广义矩阵乘法下的 \(k\) 次方描述了点 \(i\) 出发走 恰好 \(k\) 步可以到达的节点。
1.2 倍增思想
再重要不过了。用过倍增思想我们可以轻松的在 \(\log\) 的复杂度内求解幂次:
template <typename _Tp> _Tp ksm(_Tp base,int ind,int mod=INT_MAX) {
_Tp ans=1;
while(ind) {
if(ind&1) ans=ans*base%mod;
base=base*base%mod; ind>>=1;
}
return ans%mod;
}
亦或者快速求解 \(\sum \limits_{i=1}^{n-1} a^i\):(详见山东游记)
template <typename _Tp> _Tp fac_pow(_Tp base,int ind,int mod=INT_MAX) {
_Tp ans=0,now=1; int st[100] {0};
while(ind) {st[++st[0]]=ind&1,ind>>=1;}
while(st[0]) {
ans=(ans+(now*ans%mod))%mod,now=now*now%mod;
if(st[st[0]]) ans=(ans+now)%mod,now=(now*base)%mod;
st[0]--;
}
return ans;
}
熟练的利用倍增,和矩阵搭配会更香哦。
2. 动态 DP
一般指树上的动态 DP。
动态 DP 主要用于解决一类问题。这类问题一般原本都是较为简单的树上 DP 问题,但是被套上了丧心病狂的修改点权的操作。
—— Tweetuzki
- 写出转移方程,使用矩阵转移,但 \(n^3\) 的转移有点承受不了。
- 注意到 DS 可以快速转移状态,而树剖具有最大链深度 \(O(\log n)\) 的优点,将二者融会贯通。
- 黄色的树林中分出两条路:线段树与全局平衡二叉树。
3. Ex:多项式取模
关于特征多项式,笔者也不会 qwq,别指望笔者能写出什么,笔者只能写自己会的。
反正我们知道,对于一个函数,给定其递推式,如
那么移项之后,
这就是这个函数的特征多项式。其实准确来说,移项之后是
所以可以浅显的理解为,多项式对该多项式取模就相当于对 \(0\) 取模,结果不变,只是次数降到了特征多项式次数以下。(?我知道这样很不严谨,但我学艺不精 qwq,大家领会精神吧……)
所以,我们在每次乘法后对特征多项式取模,就可以把次数压住啦。举个例子,一个函数的递推式是 \(f_i=\sum \limits_{j=1}^{1000} f_{i-j}\),用 \(n^3\) 的矩阵乘法递推肯定要寄,这时候就可以令 \(x^i = f_i,x_i=\sum \limits_{j=1}^{1000} x^{i-j}\),搭配快速幂,直接用多项式乘法正常 \(n^2\) 卷就可以啦~
那么,如何取模呢?
考虑到取模其实本质上是一直减,我们考虑不断将原多项式减去特征多项式的倍数即可。从贪心的角度,我们考虑每次消掉最高次项。这里举个例子,\(x^5\) 对 \(x^2-x-x^0\) 取模:
\(x^0\) 可不一定是 \(1\) 哦,比如上面的 肥不拉几 斐波那契数列中 \(x^0=f_0=0\),斐波那契数列的特征多项式就是 \(x^2-x-x^0\),那么根据上面的取模就可以知道 \(fib_5=5fib_1+3fib_0=5\) 了。
7.28 欢乐 ACM
太欢乐了。
—— 总结一下上午
—— 我 D 后缀数组超纲了就会一半
—— agy 分治没治出来
—— zzm 数位 DP 假了
—— 痛失三道题……
made,怎么什么都不会……
本来想写解题报告来着,被恶心到了,鸽了吧。