结合律优化
概述
- 结合律优化通过将冗长的转移用结合律压缩,高效实现...取某一段的 dp 结果?也可以带修。
实现原理
-
首先,对应 DP 要满足结合律,即各部分转移的形式相互独立。典型的反例譬如求可行性的背包问题。
-
然后上线段树。视情况不同,可能是基于矩阵的 DDP 或纯粹靠复杂线段树的...线段树优化 DP?另外还有全局平衡二叉树和 LCT 实现,但我都不会。
-
如果不带修,也可以使用倍增实现。
例题
从最大子段和讲起
-
题意:求 \(a_n\) 的最大子段和,带 \(m\) 次单点修改。
-
数据范围:\(n,m\leqslant 10^5\)。应该也能 \(10^6\)?
-
先谈谈静态意义下的朴素 dp。
-
状态设计:\(dp_i\) 表示以 \(i\) 结尾的最大子段和。
-
初始化:\(dp_0=0\)。
-
转状态转移方程:\(dp_i=\max(dp_{i-1}+v_i,v_i)\)。
-
-
OK,这是一个很朴素的 \(O(n)\)。显然 \(O(nm)\) 不可接受,但我们看到它的转移条件基本互相不依赖,满足结合律。
-
故首先考虑 DDP。考虑把每个转移都做成矩乘的形式:
-
状态矩阵:\(\begin{bmatrix}dp_i & ans & 0\end{bmatrix}\)。特别地,一开始 \(ans=-inf\)。
-
乘法法则:\(res_{i,j}=\max_{k=1}^m(A_{i,k}+B_{k,j})\)。显然,其满足结合律。
-
转移矩阵:\(\begin{bmatrix}v_{i+1} & 0 & 0 \\ -inf & 0 & 0 \\ v_{i+1}& -inf &0\end{bmatrix}\)。
-
解释一下,这里希望令 \(ans=max(ans,dp_i)\),这样真正的答案 \(Ans=\max(ans,res_1)\),\(res\) 是最后的答案矩阵。
-
为什么求这个 \(ans\),而不是利用线段树对 \(dp_i\) 求 \(\max\)?嗯...DDP 的 sgt 的功能是把转移矩阵结合起来,不能求出每个 \(dp_i\),显然这样做的复杂度下界就是 \(O(n)\)。DDP 的本质上是把中间转移和中间结果压掉了。
-
-
总复杂度 \(O(m\log n\times 3^3)\),大概能过 \(10^5\) 的数据。
-
我们发现这个矩阵非常空,足有 \(4\) 格没有意义。说明我们有点浪费,设法深挖线段树的潜力,我们搞一个纯用复杂线段树维护的:
-
在每个节点维护 \(mx,lf,rg,sum\) 表示区间最大子段和,左起最大子段和,右起最大子段和,区间和。
-
写一个复杂的 pu,这里不想写了可以参看 \(\text{P4513 小白逛公园}\) 的代码。询问的时候,返回值设计成一个 node,套用这种合并方式。
-
核心在于什么呢?相当于我们把 dp 的形式又改了改,以重构它原来的从左向右的顺序,变成了类似于 \(((),()),((),())\) 的形式。这就满足只靠线段树维护的需求。
-
22.10.10 T3 mountains
- 版权原因,请移步查看。
P4719 【模板】"动态 DP"&动态树分治
-
题意:给出一棵 \(n\) 个点的带点权树,有 \(m\) 次单点修改点权的操作,请在每次修改后求最大权独立集的权。
-
数据范围:\(n,m\leqslant 10^5\)。
-
老规矩,先考虑怎么静态 DP。这个东西显然非常 tdp,考虑这么做:
-
状态设计:\(dp_{i,0/1}\) 表示考虑完 \(i\) 的子树,选/不选 \(i\) 的情况下的最大权独立集的权。
-
初始化:\(dp_{i,0}=0,dp_{i,1}=w_i\)。
-
状态转移方程:
-
\(dp_{i,0}=\sum\limits_{j\in \text{sons of i}} \max(dp_{j,0},dp_{j,1})\)。
-
\(dp_{i,1}=\sum\limits_{j\in \text{sons of i}}dp_{j,0}+w_i\)。
-
-
-
这个东西是 \(O(n)\) 的,很好。我们考虑把它改成 \(O(n\log)\) 的在线做法。
-
然而我们显然不能对着树直接做结合——单链的情况下,结合就会到达 \(n\) 次,无法接受。
-
注意到这个转移中各个儿子其实互相没有影响。我们考虑把树的结构改成更可合并一些的结构:轻重剖!
-
如果我们能做到仅在每个虚边上做一次合并,那么我们的复杂度显然就对了。
-
所以考虑重新设计 DP。
-
状态设计:\(f_{i,0/1},g_{i,0/1}\)。其中 \(f\) 相当于 \(dp\),\(g\) 则表示只考虑轻儿子和自己的情况下的最大权。
-
初始化:区别不大,略。
-
状态转移方程:
-
\(g_{i,0}=\sum\limits_{j\in \text{light sons of i}} \max(f_{j,0},f_{j,1})\)。
-
\(g_{i,1}=\sum\limits_{j\in \text{light sons of i}}f_{j,0}+w_i\)。
-
\(f_{i,0}=g_{i,0}+\max(f_{hs,0},f_{hs,1})\)。
-
\(f_{i,1}=g_{i,1}+f_{hs,0}\)。
-
-
-
这里不妨假设我们手上已经有了全套的 \(g\)。设法把 \(f\) 的转移写成矩乘的形式:
-
状态矩阵:\(\begin{bmatrix}f_{i,0} & f_{i,1}\end{bmatrix}\)。这里 \(i\) 可以认为就是 \(i+1\) 的 \(hs\)。
-
乘法法则:\(res_{i,j}=\max_{k=1}^m(A_{i,k}+B_{k,j})\)。
-
转移矩阵:\(\begin{bmatrix} g_{i+1,0} & g_{i+1,1}\\ g_{i+1,0} & -\infty\end{bmatrix}\)。嗯,这是和原转移方程等价的。
-
-
那么我们只要将树剖好,单点修改的时候先修矩阵做到链头,然后用增量法暴力算一下 \(g_{fa_{top}}\),再修一下矩阵,一路做回去就对了。
-
复杂度 \(O(na^3\log^2 n)\),但是树剖实在是快得飞起。
-
这东西把我写吐了...
-
每个重链要单独开 sgt 否则和我们需要的合并方式不一样(除非用区间询问,但那也太鬼了)。
-
对链维护的其实是转移矩阵 \(G\) 的连乘,\(F\) 需要的时候才拎出来。事实上只维护了重链头和尾处的 \(F\)...当然,实现是比较随意的事情。
-
把 \(F\) 初始化成一元以便于它和自己的 \(G\) 相乘。本来想的是按需求放各种一元,但实际上好像没有怎么用到。
-
不断地记录 \(w,f,g\) 以便于使用增量法,或者说算 \(\Delta\)。
-
总之就是非常恶心。
-
P5024 [NOIP2018 提高组] 保卫王国
-
题意:给出一棵 \(n\) 个点的带点权树。给出 \(m\) 个询问,每个询问会钦定 \(2\) 个点分别必选/不选,求该前提下的最小权点覆盖的权。
-
数据范围:\(n,m\leqslant 10^5\)。
-
怎么这么喜欢把 DDP 搬到树上啊!还都是很图论基础概念的东西...
-
我们还是来设法先设计一个静态 DP。
-
状态设计:\(dp_{i,0/1}\) 表示考虑完 \(i\) 的子树,选/不选 \(i\) 的前提下,最小权点覆盖的权。
-
初始化:\(dp_{leaf,1}=w_i\)。
-
状态转移方程:
-
\(dp_{i,0}=\sum\limits_{j\in \text{sons of i}} dp_{j,1}\)。
-
\(dp_{i,1}=\sum\limits_{j\in \text{sons of i}} \min(dp_{j,0},dp_{j,1})+w_i\)。
-
-
-
我真是欲言又止止言又欲...这不就是上面那个的镜像吗...哦不全是是吧?
-
尽管贺自己的东西并没什么,但我们还是最好把这玩意搞懂吧...深蓝,给我推式子!(???)
-
还是考虑用重剖来解决这玩意。首先乱七八糟重剖一下,然后我们重新设计 \(dp\) 状态:
-
状态设计:\(f_{i,0/1},g_{i,0/1}\)。意义参前。
-
初始化:略。
-
状态转移方程:
-
\(g_{i,0}=\sum\limits_{j\in \text{sons of i}} f_{j,1}\)。
-
\(g_{i,1}=\sum\limits_{j\in \text{sons of i}} \min(f_{j,0},f_{j,1})+w_i\)。
-
\(f_{i,0}=g_{i,0}+f_{hs,1}\)。
-
\(f_{i,1}=g_{i,1}+\min(f_{hs,0},f_{hs,1})\)。
-
-
-
确实是...很一样呢,哈哈哈(棒读)。让我看看剧本...哦该拆矩阵了是吧...好...
-
状态矩阵:\(\begin{bmatrix} f_{i,0} & f_{i,1}\end{bmatrix}\)。
-
乘法法则:唔,这里用 \(\min\) 就可以了。
-
转移矩阵:\(\begin{bmatrix} +\infty & g_{i+1,1} \\ g_{i+1,0} & g_{i+1,1}\end{bmatrix}\),这里认为 \(i=hs_{i+1}\)。
-
-
后面显然都一样了。你就当是一道非常难写的水紫吧...毕竟上面那份代码还在可以改改就用对吧,啊哈哈哈(棒读)。
-
什么,不是单点修改?什么不是单点修改,就是单点修改!必选就是 \(-\infty\),必不选就是 \(+\infty\),这不就完了?
-
主要难搞的地方在于这个修改是...嗯...临时性的,你懂我意思吧?得记录一下,用完改回来。
为什么不用复杂线段树呢?
-
两个理由:
-
难以实现;
-
缺乏优势。
-
-
先谈实现难度。考察最大子段和和 mountains 两道题,发现两者的 DP 形式其实较简单,这一简单的含义如下:
-
存在一种较为简洁的方式,可以将多次转移合并为一次。
-
以最大子段和为例:
\[\begin{aligned} dp_i & = \max(dp_{i-1}+v_i,v_i) \\ & = \max(\max(dp_{i-2}+v_{i-1},v_{i-1})+v_i,v_i) \\ &=\max(dp_{i-2}+v_{i-1}+v_i,v_{i-1}+v_i,v_i)\end{aligned} \] -
-
转头考察 P4719,首先我们得两个一起转移因为它们是互转的,然后!DP 状态将会非!常!恶!心!
-
\(g\) 的定义将变成“轻儿子和重链上不超出对应 sgt 节点管辖范围的部分”的最大权独立集的权。
-
只有这样,我们才能符合线段树的形式地把重链 DP 出来,即扬弃链底-链头的顺序,改为在链上分治的 sgt 风格。
-
-
再谈缺乏优势。
-
大部分情况下,结合律优化用复杂 sgt 的转移近似于状态数的 \(^2\),而矩乘近似于 \(^3\)。
-
实际情况中一般都会有溢出。
- 先谈矩乘。
-
较为典型的例子,譬如某些需要即时结算 \(ans\) 的题目,此时只能把 \(ans\) 也塞进矩阵,容易证明这会造成很多浪费(很多与 \(ans\) 相关的转移都是无效转移,用 \(+-\infty\) 顶掉的)。
-
类似地,有时转移的系数 \(w\) 之类的也得塞进去。甚至有时状态矩阵里要放 \(0\) 来占位...
-
有并行转移的 DP 就更不用说了,举个例子,\(3^2+5^2\) 的复杂度能暴涨到 \(8^3\)。
-
矩乘有这么多无效转移,很大程度上就说明了它是一种很粗放的实现方式。但如果矩阵很小,那么它的表现还是挺不错的。
-
再谈复杂 sgt。
-
复杂 sgt 本身的分治要求有时对 DP 本身的固有顺序破坏很大。譬如这里就要被迫重新定义,还得同时跑 \(f,g\),作为对比矩乘只需要做 \(G\)。
-
转移本身系数大点是很正常的事情。
-
-
- 先谈矩乘。
-
综上,这里 \(2^3\) 并不比 \(2^2\) 差多少,这还没有考虑 sgt 自己的各种额外系数。所以啊,矩乘码量大却简单啊!
-
P8820 [CSP-S 2022] 数据传输
- 参看树形 DP-难以形容的其他树上依赖式问题。