slope trick

我真的什么也不会。

slope trick 是一种技巧,可以用来维护这样的函数:是凸函数且斜率为整数。

引入

不妨以一道题作为引入:CF13C:

给定序列 \(a_1\cdots a_n\),求出非降序列 \(b_1\cdots b_n\),并最小化 \(\sum|a_i-b_i|\)

显然地,设一个 dp:\(f_{i,j}\) 表示考虑了 \(a_1\)\(a_i\)\(b_i=j\) 的最小代价,有朴素转移:

\(f_{i,j}=\min\limits_{k\le j}f_{i-1,k}+|a_i-j|\)

我们现在要证明 \(f_i\) 为凸的。考虑若凸性对于 \(f_{i-1}\) 成立(显然 \(f_1=|a_1|\),是凸的),我们的转移实际上是做了这样一件事:

\(f_{i-1}\) 斜率大于 \(0\) 的一段推平,这相当于取前缀最小值,显然这一步后函数仍具有凸性。

\(f_{i-1}\) 加上 \(|a_i-j|\) 以得到 \(f_i\),凸性函数相加为凸性函数。

\(f_i\) 关于 \(j\) 是有凸性的。

凸函数的表示

进行一个曲的差:如何表示一个凸性函数?以下凸壳为一个例子:

函数的四段斜率分别为 \(-2,-1,0,1\),斜率最大的部分的函数是 \(y=x-5(x\le9)\)

维护一个拐点的可重集合,如果函数在 \(x\) 处斜率变化了 \(d\),我们在集合中插入 \(d\)\(x\),如图函数的集合是 \(\{3,5,7\}\),用斜率最大的线段 \(y=x-5(x\le9)\)\(\{3,5,7\}\) 来表示该凸壳。意思是凸壳斜率最大的部分是 \(y=x-5(x\le9)\),从右到左,在 \(x=\{3,5,7\}\) 处斜率减少了 \(1\)

当然,如果最左边的线段斜率变成了 \(-3\),集合会变成 \(\{3,3,5,7\}\),表示 \(x=3\) 处斜率减少了 \(2\)

显然这种方式就可以唯一确定一个凸函数,若两凸函数相加,最终结果呈现为斜率最大的线段相加,两者的拐点集合取并。

关于解题

继续思考以上的这道题。我们要求的实际上是 \(f_n\) 中,使得斜率变得等于 \(0\) 的拐点的函数值。

需要发现性质:每次转移时先将函数的后半部分推平,最大斜率为 \(0\),在此基础上加入绝对值函数,最大斜率变为 \(1\),就是说删掉拐点集合最大的一个数就可以得到最后是水平的函数。

考虑维护出了 \(f_{i-1}\) 后半部分推平后的函数,斜率最大的函数是 \(y=y_0\),那么对于前 \(i-1\) 个数,答案就是 \(y_0\)。维护这个 \(res=y_0\)。显然这个问题里,斜率已知,可以只靠维护 \(res\) 来得到最右面的线段。

不妨模拟转移:第一步为加上函数 \(|a_i-x|\)。其最右的部分是 \(y=x-a_i\),拐点集合是 \(\{a_i,a_i\}\),设 \(f_{i-1}\) 最大的拐点是 \(s\)。如果看不懂请画个图。

1.若 \(s\le a_i\),相当于在原函数最后水平的部分大于 \(a_i\) 的斜率加一,整个函数小于 \(a_i\) 部分斜率减一,最小值应当是 \(x=a_i\) 处取得,此时 \(res\) 不变,在集合中插入两个 \(a_i\) 即可。

2.若 \(a_i\le s\),显然此时 \(res\) 会发生变化。小于 \(a_i\) 的部分斜率减一,没有成为新最小值的可能。\(a_i\) 右面的只有大于 \(s\) 的部分斜率取正,故最小值会在 \(x=s\) 处取得,给 \(res\) 加上 \(s-a_i\) 即可。

3.插入两个 \(a_i\)

4.推平函数斜率大于 \(0\) 的部分,不难发现这就是弹出集合中的最大值(此时函数斜率最大为 \(1\))。

用堆维护那个集合,很容易就能写出代码。

#include<iostream>
#include<queue>
int n, rs, x; std::priority_queue<int> q;
int main(){
    std::cin >> n; while(n--)
        std::cin >> x, q.push(x),
        q.push(x), rs += q.top() - x, q.pop();
    std::cout << rs;
}

真的很好用啊。

做题笔记:

CF13C 的经验:P2893 CF713C P4597 P4331(比较恐怖)。

哎呀,要去睡觉了,明天再写。

posted @ 2024-04-10 21:19  xlpg0713  阅读(25)  评论(0)    收藏  举报