【典】CF713C 堆维护凸壳优化dp

其实是一个五倍经验题:

CF713C *2300

P4597

CF13C *2200

P2893

P4331

有一个长度为 \(n\) 的序列 \(a\),求一个非递减序列 \(b\) ,使得 \(\sum_{i=1}^n |a_i-b_i|\)

\(n\le 10^6,a_i\le 10^9\)


part 1 \(O(n^2)\)

数据范围最小的是两个 CF 题,\(O(n^2)\) 都可以过。

考虑一个简单的 dp,\(f_{i,j}\) 表示前 \(i\) 个数,第 \(i\) 个数选 \(j\) 的最小值。

\[f_{i,j}=\min_{k=1}^j f_{i-1,k}+|a_i-j| \]

维护前缀最小值就可以得到 \(O(na_i)\) 的做法。

但是可以发现一个显然的性质,最后得到的 \(b\) 中的数一定都在 \(a\) 中出现过,否则可以通过简单的调整法,也就是对于排序后的一个区间 \((a'_i,a'_{i+1})\) 中间有数在 \(b\) 里,那么可以一个前缀变成 \(a'_i\),后缀变成 \(a'_{i+1}\) 即可。

所以直接离散化就可以优化到 \(O(n^2)\)


part 2 \(O(n\log n)\) 可并堆

考虑这样一个问题,如果把一堆数变成一个数,要求绝对值和最小咋做。

经典结论直接中位数即可。

那么考虑最后的 \(b\) 一定可以看成是若干个区间,每个区间都是对应 \(a\) 的中位数最优。

但是可能不满足不递减。

所以用可并堆维护,如果后面的中位数比前面小就合并。

每次加一个数,这个数单独成堆,然后不断与前面合并即可。


part3 \(O(n\log n)\) 堆+斜率

最强的一个做法,主要是因为好写。

依然考虑上面的 dp 做法,我们改一下,改成另 \(f_{i,j}\) 表示 \(a_i\le j\) 的最优解。

那么状态也很好搞:

\[f_{i,j}=f_{i-1,j}+|a_i-j|\\ f_{i,j}=\min (f_{i,j},f_{i,{j+1}}) \]

然后我们把 \(f_i\) 的图像画出来,可以发现每次这个图像是加上一个绝对值函数然后再取一个后缀 \(\min\)

最后的答案相当于就是最下面的拐点。

下面来维护一下这个过程。

jyh3m8.png

这是最开始 \(f_1\) 的图形,拐点就是 \(a_1\)

然后我们在右边加一个点(\(a_2>a_1\))。

jyhRpR.png

可以发现,左边的斜率全部减一,但此时答案不变。

因此,如果一直在右边加点,会形成一个这样的图形:

jy4DgI.png

也就是一个凸壳。

接下来考虑在左边加上一个点。

可以发现就是右边所有线段的斜率加一,右边斜率全部减一。

jyxIDe.png

那么对答案的贡献就是下面黑线和上面红线之间的距离。

可以发现就是最下面拐点的坐标与 \(a_i\) 的差,维护拐点的话就相当于删掉最后面一个。

考虑一个拐点本质上就是使得左边斜率是右边斜率减一,因此直接用一个堆维护即可。

上面的情况直接加入这个拐点,下面的情况就把最大值弹了。

考虑方案怎么构造。

可以发现发现每次只需要让 \(a_i\) 等于最优的拐点即可,也就是堆顶,但是这个可能会被后面的更新,所以取后缀最小值即可。

code


P8476 \(\color{blue}\bigstar\)

给定非负整数序列 \(\{a_n\}\),定义函数 \(f(x,y)\)

\[f(x,y)=\begin{cases} x-y,&x\ge y\\ C,&x< y \end{cases}, \]

其中 \(C\) 是给定常数。请构造一个不增非负整数序列 \(\{b_n\}\),最小化

\[\sum_{i=1}^nf(b_i,a_i). \]

你仅需输出这一最小化的结果。

\(n\le 10^6,a_i\le 10^9\)

做法类似,考虑 \(f\) 的图像就是前面一段是 \(C\) 后面是一个斜率为 \(1\) 的直线,每次加上这个图像之后后缀取 \(\min\)

考虑对于一个 \(a_i\),右边的变化就是所有线的斜率加一,前面就相当于全部加 \(C\),然后和 \(a_i\) 的值取 \(\min\)

可以发现这个取 \(\min\) 不能用堆维护,那么用线段树维护离散化后的点值,前缀就可以直接加然后暴力取 \(\min\),后缀就直接加的就是离 \(a_i\) 的距离,因此可以先加 \(a_j\) 然后区间减 \(a_i\)

比较阿拉丁,还好一次过了。

code


P3642 \(\color{blue}\bigstar\)

给一棵树,边有边权,每次操作可以把一条边加一或者减一,需要满足边权非负,求让所有叶子深度相同所需要的最少操作次数。

\(n\le 3\times 10^5\)

开始有一个憨憨想法,就是直接算出每个点的深度,然后直接 dp,但这个有一个问题,就是权值需要非负。

考虑设 \(f_{i,j}\) 表示 \(i\) 子树内点的深度为 \(j\) 的答案。

考虑向上面转移,假设边权是 \(c_i\) ,显然先向右边平移 \(c_i\) 个单位,

然后下一步就是一个点对左右两边的贡献,那么这个贡献就是一个绝对值函数。

v0rBad.png

长这样。

然后考虑整体函数的移动是怎么样的,\(l\) 的右边整体右移 \(c_i\)\([l,l+c_i]\) 是一个斜率为 \(-1\) 的东西,左边的相当于向上平移了 \(c_i\)

考虑用堆维护,维护拐点即可。

合并多个,考虑两个函数相加,一段的斜率就是两个加起来,那么合并之后把最大值弹掉就可以维护右边是斜率为 \(1\) 的结构了。

至于答案,考虑 \(0\) 处的答案是边权之和,然后可以轻松求出了。

code


ABC217Ex *3112 \(\color{blue}\bigstar\)

有一个人开始在 \(0\) 处,每个时刻可以向左走一格或者向右走一格,有 \(n\) 次攻击,时间为 \(t_i\),位置是 \(x_i\),攻击有两种,当前位置为 \(p\),一种伤害为 \(\max(0,p-x_i)\),另一种是 \(\max(0,x_i-p)\)

求最少受到的伤害。

\(n\le 2\times 10^5\)

不是很难。

考虑一个时间间隔 \(t_i-t_{i-1}\) 就是一个点向左右去更新,如果最小值区间为 \([l,r]\),那么就分别 \(l\) 左移,\(r\) 右移时间长度即可。

加一个函数就分在区间左边还是右边即可。

具体的,开两个堆维护 \([l,r]\) 左边和右边的拐点,平移不太好维护,那么就对于右边的维护 \(x-t\),左边维护 \(x+t\) 即可,这样需要用的时候在算上 \(t\) 即可。

注意特判 \(x\) 这个地方走不到对答案的贡献。

code


posted @ 2022-07-11 13:33  houzhiyuan  阅读(247)  评论(0编辑  收藏  举报