复杂 dp 最优化问题

凸优化

为了简单维护每一个区间的斜率,我们一般在端点上维护斜率的差分,由此有直接维护法(每个线段直接维护这个线段相对上一个线段斜率的变化量)和堆叠法(每两个元素之间斜率固定为 \(1\),用多个相同元素表示超过 \(1\) 的增量)。相对来说,前一种便于维护斜率变化量大的情况,第二种便于维护斜率情况复杂但变化量小的情况(如叠加系数小的绝对值函数)。

CF373E.Watching Fireworks is Fun

题意:一个城镇有 \(n\) 个区域,从左到右编号为 \(1\sim n\),每个区域之间距离 \(1\) 个单位距离。有 \(m\) 个烟火要放,给定放的地点 \(a_i\),时间 \(t_i\),如果你当时在区域 \(x\),那么你可以获得 \(b_i - \vert a_i - x\vert\) 的开心值。你每个单位时间可以移动不超过 \(d\) 个单位距离。你的初始位置是任意且初始时刻为 \(1\),求你通过移动能获取到的最大的开心值。\(n\leq 10^9\)\(m\leq 2\times 10^5\)

首先有一个 dp:设 \(f_{i,j}\) 表示考虑前 \(i\) 个烟花,当前在位置 \(j\) 的减少量最小值。设 \(d\)\(i-1\sim i\) 的最大移动距离,有 \(f_{i,j}=\min_{k=j-d}^{j+d}f_{i-1,k}+|a_i-j|\)。单调队列优化可以做到 \(O(nm)\)

考虑去掉 \(n\) 的一维。具体地,对于每一个 \(f_i\),其函数图像是一个斜率处处不超过 \(m\) 的下凸壳。原因是对下凸壳上每一个点做区间取 \(\min\) 和加绝对值函数(也是下凸函数)后,结果仍然是下凸壳。

于是可以使用 Slope trick。由于最小值处斜率为 \(0\),所以我们使用两个堆分别维护斜率为负和斜率为正的分界点。对于区间取 \(\min\) 操作,左侧的分界点的变化等价于:每个分界点减少 \(d\);右侧等价于所有分界点增加 \(d\),可以打全局 tag 解决;对于叠加绝对值函数操作,考虑 \(a_i\) 位置:设左侧最大的分界点为 \(L\),右侧最小分界点为 \(R\)

  • \(a_i\leq L\),则 \(a_i\) 左侧分界点不变,对应斜率减小 \(1\);右侧分界点不变,对应斜率增大 \(1\),也即在 \(a_i\) 处会出现一个空斜率。只需要在左堆中插入两次 \(a_i\) 即可;同时,需要弹出 \(L\) 并将其加入右堆,因为原来斜率为 \(0\) 的段变成了 \(1\)\(a_i\geq R\) 类似的。
  • \(a_i\in (L,R)\),则左堆中所有段的斜率减少 \(1\),右段中所有段的斜率增加 \(1\),只需要分别在两边插入 \(a_i\) 即可。

在插入过程中需要动态维护函数的最小值,这是容易的,只需要每次找到新的斜率为 \(0\) 的点是 \(L\) 还是 \(R\) 即可。

[HDU6757]Hunting Monsters

题意:有 \(n\) 项计划,每项计划实行前投入为 \(a_i\),实行后收益为 \(b_i\)。对于 \(k\in[1,n]\),求出:任选 \(k\) 个项目按任意顺序依次完成,使得全过程中钱数非负,则最初至少需要拥有多少钱。\(n\leq 2\times 10^5\)

首先解决顺序问题。对于 \(a_i\leq b_i\)\(a_i>b_i\) 分开讨论:我们一定先选择 \(a_i\leq b_i\) 的项目,这部分按照 \(a_i\) 排序,选择的一定是一个前缀;对于 \(a_i>b_i\) 的项目,邻项交换可知按照 \(b_i\) 从大到小的顺序选最优。

于是有 \(O(n^2)\) 做法:\(O(n)\) 预处理 \(g_i,d_i\) 表示前面部分选择 \(i\) 个东西,最小初始价值为 \(g_i\),增加量为 \(d_i\);对于后半部分,设 \(f_{i,j}\) 表示考虑完 \(i\) 个及之后的所有,选了 \(j\) 个的最小初始值,转移是 \(f_{i,j}=\min(f_{i+1,j},\max(0,f_{i+1,j-1}-b_i)+a_i)\),可以 \(O(n^2)\) 求出答案;最后对每一个 \(k\) 枚举前面部分选了多少即可。

先优化 dp 部分。首先有 \(f_{i,j}\leq f_{i+1,j}\leq f_{i+1,j+1}\)。考虑拆开 \(\min\)

  • 对于 \(f_{i+1,j}\leq a_i\),显然直接取 \(f_{i,j}=f_{i+1,j}\)
  • 对于 \(f_{i+1,j-1}-b_i+a_i\geq f_{i+1,j}\),也取 \(f_{i,j}=f_{i+1,j}\)
  • 发现上面两个情况恰好完全包含前者为 \(\min\) 的情况。于是剩余情况都取 \(\max(0,f_{i+1,j-1}-b_i)+a_i\)。对这部分内容进行继续考虑:当 \(f_{i+1,j}\leq b_i\) 时,由于 \(b_i<a_i\),有 \(j\) 处一定取原值不变。因此可能存在 \(f_{i+1,j-1}<b_i\) 的该种转移只可能有 \(1\) 处,也即从一个 \(f_{i+1,j-1}\leq b_i\) 转移到 \(f_{i,j}\),且 \(f_{i+1,j}>b_i\)

\(f_i\) 看做一个函数,初始时仅有一个单点。不妨每次删去值小于等于 \(b\) 的那个部分,剩下的部分若存在一个值小于等于 \(a\),则显然不存在需要对 \(0\)\(\max\) 的转移,此时是对一个 \((0,0)\)\((1,a_i-b_i)\) 的下凸函数做 min+ 卷积;若不存在,则需要在全局抬升 \(a_i-b_i\) 。若存在对 \(0\)\(\max\) 的转移,还要将最左侧点改为 \(a_i\)。在此类情况下,即使有抬升,也是增大。故全过程中被保留的 \(f\) 是下凸函数,不妨用优先队列维护点及对应函数值差分(也即斜率)。注意到在弹完所有小于等于 \(b\) 的部分后,我们只需要在插入前将基准值设为 \(b_i\),然后直接往堆里插入 \(a_i-b_i\),即可解决上述所有情况。

于是我们可以快速求出 \(f\) 的值。对于每个 \(k\),考虑优化求答案过程。

枚举前半部分选的个数 \(i\),我们显然有 \(ans_k=\min_i\max(g_i,f_{k-i}-d_i)\)。注意到 \(\max\) 左侧不降,右侧不增,于是可以二分两侧的差值找到答案。总时间复杂度做到 \(O(n\log n)\)

[ABC383G] Bar Cover

这一类选恰好 \(k\) 个的问题,大胆猜测它是凸的。注意到 \(k\) 很小,考虑归并合并凸包。设 \(f_{l,r,k,a,b}\) 表示在 \([l,r]\) 内选了 \(k\) 个,且左侧至少 \(a\) 个没被占用,右侧至少 \(b\) 个没被占用的最大贡献,利用线段树结构合并即可,需要实现闵可夫斯基和、全局加、对所有凸包取 \(\min\)。注意到长度为 \(len\) 的区间内凸包点数为 \(O(\frac{len}{k})\),一个位置的合并需要执行 \(O(k^3)\) 次,总复杂度 \(O(nk^2\log n)\)。由于有全局加、单点比较求 \(\min\) 的操作,凸包上维护实际值比维护斜率更容易。

链剖分优化

长链剖分优化:给每条重链分配空间,通过记录每个点在空间中的起始点实现重链上的位移操作。不妨记录 \(len_i\) 表示重链上点的数量,则若第二维表示到当前点的深度,需要开恰好 \(len_i\) 的空间,\(0\sim len_i-1\)。具体实现可以是:开一个内存池 \(temp\) 以及一个标志 \(nwp\) 表示当前使用到内存池的哪个位置。同时对于每个 \(f_i\) 维护其起点的指针(一般也是第二维为 \(0\) 的位置),跳到重儿子直接 ++,到轻儿子时直接新开即可。

CF1009F Dominant Indices

题意:对于树上每一个 \(x\) 求一个距离 \(k\),最大化 \(x\) 子树内距离 \(x\)\(k\) 的点数。

\(f_{x,i}\) 表示树上 \(x\) 子树内距离 \(x\)\(i\) 的点数,\(ans_x\) 表示 \(x\) 处的答案。处理时,直接继承重儿子的 \(f\)\(ans\)\(f\) 通过向下时向后位移提前预留该点空间实现),然后再依次合并每一个轻儿子深度的答案并更新 \(ans\) 即可。总复杂度为链长和,即 \(O(n)\)

P7581 「RdOI R2」路径权值(distance)

\(f_{x,i,0/1/2}\) 表示 \(x\) 子树内到 \(x\) 边数为 \(i\) 的点数/距离和/答案和,离线询问,转移如下:

\[f_{x,i,0}\gets f_{j,i-1,0}\\ f_{x,i,1}\gets f_{j,i-1,1}+w\times f_{j,i-1,0}\\ f_{x,i,2}\gets f_{j,i-1,2}+f_{x,i,0}\times f_{j,i-1,1}+f_{j,i-1,0}\times f_{x,i,1}+f_{j,i-1,0}\times f_{x,i,0}\times w \]

\(0,2\) 的转移式没有附加值或附加值与至少一个轻儿子有关,可以直接长剖维护,复杂度正确,但 \(1\) 的转移式由于有一个 \(w\times f_{j,i-1,0}\),不能直接长剖维护。有如下两种方式去掉其影响:

  • 改变 \(f_{x,i,1}\) 的定义为这些点到的距离和,其转移可以预处理 \(dis\) 求出,\(f_{x,i,2}\) 使用 \(1\) 时,减去 \(dis_x\times f_{x,i,0}\) 即可。
  • 考虑使用 \(tag_x\) 表示当前数组中的 \(f_{x,i,1}\) 的实际值还要加上 \(tag_x\times f_{x,i,0}\)(或者也可以记成乘上 \(f_{hson_x,i,0}\)。考虑做 \(1\) 的转移时的变化:轻儿子的合并直接按照真实值并入,重儿子的要合并 \(tag\),也即原来是加的值是 \(tag_{hson_x}\times f_{hson_x,i-1,0}\),现在还要加上 \(tag_{x}\times f_{hson_{x},i-1,0}\)。可以令 \(tag_x\gets tag_{hson_x}\),但我们要转成 \(\times f_{x,i,0}\),你发现多加的是 \(x\) 所有轻儿子的 \(size\) 乘新 \(tag\) 的值,每一轮合并结束后减去即可。

杂题选做

P11303 [NOISG 2021 Finals] Pond

题意:数轴上给定 \(n\) 个点 \(x_1\sim x_n\),从 \(x_k\) 出发访问所有点,最小化每个点首次被访问时经过的距离总和。\(n\le 3\times 10^5\)

我不希望记录整个过程经过的距离是多少,因此考虑做一个经典的拆贡献,对于每一段路程考虑其会被几个点算到。于是有一个区间 dp:\(f_{l,r}\) 表示走完 \(l,r\),此部分造成的总贡献最小值。每次枚举一个端点转移,做到 \(O(n^2)\)

注意到 \(l\le k,r\ge k\),这个区间 dp 的贡献形式又不能上网格图,很难优化。考虑能不能拆成左右两侧单独做?

具体地,我们重新分配贡献,首先点 \(i\) 有基础贡献 \(|x_i-x_k|\),除此之外,不妨设 \(i<k\),则每一次 \(x\in [i+1,k]\to y>k\) 的移动都会造成一来一回 \(2x_y-x_x\) 的贡献。因此,我们设 \(f_i\) 表示到达 \(i\) 时的最小额外贡献,则我们可以写出一个和“对侧已经算到哪里”无关的转移式子:

\[\begin{align} f_j=f_i+(i-1)\times 2(x_j-x_i)\tag {$j>k,i\le k$}\\ f_j=f_i+(n-i)\times 2(x_i-x_j)\tag {$j<k,i\ge k$}\\ \end{align} \]

由于转移顺序有环,于是可以把这个看成边权跑 dij。这还是 \(O(n^2)\)。注意到同一个点出发的边权是一次函数形式,不妨直接使用李超树维护每个值当前的最小值,在 dij 时找到当前未被确定的点中函数值 \(\min\) 最小的点即可。

具体地,对左右两侧分别开一棵李超树,注意到左边斜率为负,右边斜率为正,一次函数 \(\min\) 的最小值一定在定义域最左或最右取到,此处斜率正负性又相同,因此一定是左边最靠右没更新的和右边最靠左没更新的这两个点之间的某一个被更新,于是维护左右两侧下一个没更新的点分别是谁,每次单点查值找更小的一个来更新。更新一个点后,把这个点对应的直线插入对侧李超树。

考虑最后答案是什么。注意到一旦走到一个端点,则两侧都不再增加额外贡献,故为 \(\min(f_1,f_n)\) 加上每个点的基础贡献。总复杂度 \(O(n\log n)\)

posted @ 2025-03-15 23:41  烟山嘉鸿  阅读(90)  评论(0)    收藏  举报