[P4597] Sequence

看懂了所有思路分析,就是不知道代码在干嘛。

特别感谢一下这篇题解 https://www.luogu.com.cn/article/84mah7rg 的作者,是它教会了我堆到底在干嘛。

所以直接讲做法,一个简单 dp 是 \(f_{i,j}\) 表示考虑了前 \(i\) 个数,最后一个数是 \(j\) 的最小代价,转移式为:

\[f_{i,j}=\min\limits_{1\leq k\leq j}f_{i-1,k}+|a_i-j| \]

有用的 \(j\) 只有 \(O(n)\) 个,我们可以做到 \(O(n^2)\) 的时间复杂度。

首先,\(f_i\) 是一个凸函数,其实很好理解,\(i=1\) 的时候是凸函数,每一次的操作为前缀取 \(\min\) 后加上一个折线一次函数,凸函数加凸函数仍然是一个凸函数,所以 \(f_i\) 是一个凸函数。

求最低点的值,我们只需要维护凸函数中斜率 \(\lt 0\) 的段,考虑用堆来维护折线函数,具体的:

堆中的元素 \(x\) 表示:\(x\) 是一个线段的右端点,且这条线段的斜率为 [我们需要从堆中 pop 出这个数的次数]。

很明显我们维护了所有斜率在 \([-Q.size(),-1]\) 之间的线段,当前最优点一定在 \(Q.top()\) 处,这个地方以后斜率都大于等于 \(0\) 了。

每一次我们加入一个在 \(a_i\) 处为 \(0\) 的折线,我们需要分类讨论 \(Q.top()\)\(a_i\) 的位置关系。

\(Q.top()\lt a_i\),此时对于前面所有的线段,其斜率都加上了 \(1\),其长度都不变,那么我们只需要将 \(a_i\) 放进堆中就可以完成以上的改变。

\(a_i\leq Q.top()\),覆盖了 \(a_i\) 的线段裂成两半,然后,对于 \(a_i\) 前面的线段,其斜率全部减去 \(1\),对于 \(a_i\) 后面的线段,其斜率全部加上 \(1\)(最后一段斜率为 \(-1\) 的斜率就变成了 \(0\),需要砍掉),那么我们只需要将 \(Q.top()\) pop 出来即可,同时在堆中放进两个 \(a_i\) 即可,这样,我们既将后面斜率变成 \(0\) 的部分砍掉了,同时将对应线段的斜率增加/减少了。

这里可能有读者觉得这个做法非常巧合,怎么能在如此简单的操作内做如此多的操作?其实我们的设计算法的思路是,维护斜率在 \([-n,-1]\) 之间的线段,同时用比一个数大的数的数量来描述这个线段的斜率,这两个东西一起的作用才使得这个算法看上去如此精致。

#include<bits/stdc++.h>
using namespace std;
#define N 500005
int n,a[N];long long ans;
int main(){
	scanf("%d",&n);
	for(int i=1;i<=n;i++)scanf("%d",&a[i]);
	priority_queue<int> Q;
	for(int i=1;i<=n;i++){
		Q.push(a[i]);
		if(Q.top()!=a[i]){
			ans+=Q.top()-a[i];
			Q.pop();Q.push(a[i]);
		}
	}
	printf("%lld",ans);
}
posted @ 2024-08-01 14:35  xcyyyyyy  阅读(9)  评论(0编辑  收藏  举报