题目大意
给定一个
n
个数的序列
其中
∀1≤i<n,ai≤ai+1
。你需要最小化
∑i=1|ai−xi|
,输出这个值。
1≤n≤5×105,xi≤109
题目分析
60分
首先显然一定有一种最优解使得
∀1≤i≤n,ai∈{x}
。
那么我们直接
dp
,设
fi,j
表示填到
ai
,
ai
的取值在
{x}
中排名为
j
的最优答案。可以写出:
处理前缀 min 之后,这个可以 O(n2) 转移。
100分
Algorithm 1
令
gi,j
表示
fi,j
关于
j
的前缀
可以发现如果将
gi,j
看成一个以
j
为自变量的函数(
为什么呢?观察
dp
方程,
g
函数一定是由原本的
那么我们考虑模拟这个不断加入绝对值函数的过程。
如上图,在
x=4
之前的所有线段都是单调递减的,在这段区间内绝对值函数也是单调递减的,因此它们的相对形态不会发生变化。但是在
x=4
这个横坐标这里,线段
DE
被分成了斜率不相等的两部分,并且从此往后,有些函数可能会被加成斜率为正的函数。因为函数的斜率是递增的,所以我们可以二分出第一条斜率为正的线段,然后将这个线段以及后面所有线段都变成同一个平板,它的纵坐标就是第一条斜率为正的线段的左端点的纵坐标。
使用线段树或者
splay
等数据结构来维护这个“下凸壳”,记录每条线段的斜截式方程就好了。
时间复杂度
O(nlogn)
。
Algorithm 2
然而还有更加方便的算法。
可以发现,我们的凸壳可以通过这样的方式表示:
有若干个点
(pi,0)
,定义函数
令凸壳为 f(x) ,那么 f(x)=ans+∑ni=1gi(x) 。 ans 为最后平板的纵坐标,也是当前的答案。
你也可以理解为这些点每个点向左上引出一条斜率为 −1 的射线,然后水平向右引出一条射线组成若干个函数,这些函数的和再加上平板的高度。
因此,只要我们知道这些关键点,再知道平板的高度(即当前的答案),我们就可以知道这个凸壳的具体形态。
考虑插入一个数 xi 的时候会发生什么,首先显然它会给 f(x) 引入一条从 (xi,0) 向左上角射出的 45∘ 的射线,那么它右边往上翘的部分怎么处理呢?首先显然平板左端点一定是 max{pi} 分两种情况讨论:
Case 1: xi≥max{pi}
这说明了什么呢?我当前插入的绝对值函数拐点在平板上。
显然这条线的加入并不会造成平板以及(整个函数)的抬升。而且这样的话
xi
的绝对值函数向右上角翘的那一部分就没有起任何作用,因为它比平板高,会被取
min
变成平板。因此我们不需要做任何处理。
Case 2: xi<max{pi}
这种情况有点麻烦。
首先它一定会使平板抬升,抬升多少呢?显然是
max{pi}−xi
,即平板左端点上升的高度。
其次,
xi
的绝对值函数向右上角引出的
45∘
的射线会和原本平板左端点向左上角引出的
45∘
射线的效果相叠加,变成一条水平线,因此原本平板左端点向左上角引出的射线和
xi
向右上角引出的射线被一条从
xi
向左上角引出的
45∘
射线等效代替了。我们将
xi
加入,原端点删掉就好了。
整个过程只需要使用堆来维护最大的 {pi} 。总时间复杂度 O(nlogn) ,代码短得过分。
代码实现
先贴一份 splay 实现的,@Samjia2000。
#include<iostream>
#include<algorithm>
#include<cstdio>
#include<cstring>
#include<cmath>
#include<set>
#include<map>
#define fo(i,a,b) for(int i=a;i<=b;i++)
#define fd(i,a,b) for(int i=a;i>=b;i--)
using namespace std;
typedef long long LL;
typedef double db;
int get(){
char ch;
while(ch=getchar(),(ch<'0'||ch>'9')&&ch!='-');
if (ch=='-'){
int s=0;
while(ch=getchar(),ch>='0'&&ch<='9')s=s*10+ch-'0';
return -s;
}
int s=ch-'0';
while(ch=getchar(),ch>='0'&&ch<='9')s=s*10+ch-'0';
return s;
}
const int N = 500010;
const int INF = 1.1e+9;
struct point{
int w,k,adk;
LL b,adb;
int s[2];
}tree[N];
int fa[N];
int tot,root,n;
void add(int x,int adk,LL adb){
tree[x].k+=adk;
tree[x].b+=adb;
tree[x].adk+=adk;
tree[x].adb+=adb;
}
void down(int now){
if (tree[now].adk==0&&tree[now].adb==0)return;
int x=tree[now].s[0];
if (x)add(x,tree[now].adk,tree[now].adb);
x=tree[now].s[1];
if (x)add(x,tree[now].adk,tree[now].adb);
tree[now].adk=tree[now].adb=0;
}
int findle(int now,int v){
if (!now)return 0;
down(now);
if (tree[now].w==v)return now;
if (tree[now].w>v)return findle(tree[now].s[0],v);
else{
int o=findle(tree[now].s[1],v);
if (o)return o;
return now;
}
}
int findlg(int now,int v){
if (!now)return 0;
down(now);
if (tree[now].w>=v)return findlg(tree[now].s[0],v);
else{
int o=findlg(tree[now].s[1],v);
if (o)return o;
return now;
}
}
int getlst(int now){
down(now);
if (!tree[now].s[1])return now;
return getlst(tree[now].s[1]);
}
void split(int x,int t){
down(x);
fa[tree[x].s[t]]=0;
tree[x].s[t]=0;
}
int k,a[N];
void clear(int x){
k=0;
while(x){
a[++k]=x;
x=fa[x];
}
fd(i,k,1)down(a[i]);
}
int pd(int x){
return tree[fa[x]].s[1]==x;
}
void rotate(int x){
int y=fa[x],z=fa[y];
if (z){int t=pd(y);tree[z].s[t]=x;}
int t=pd(x);
if (tree[x].s[t^1])fa[tree[x].s[t^1]]=y;
tree[y].s[t]=tree[x].s[t^1];
tree[x].s[t^1]=y;
fa[x]=z;fa[y]=x;
}
void splay(int x,int t){
clear(x);
while(fa[x]!=t){
if (fa[fa[x]]){
if (pd(x)==pd(fa[x]))rotate(fa[x]);
else rotate(x);
}
rotate(x);
}
}
int main(){
freopen("chen.in","r",stdin);
freopen("chen.out","w",stdout);
n=get();
tot=root=1;
fo(i,1,n){
int v=get();
int x=findle(root,v),y;
if (tree[x].w==v){
y=findlg(root,v);
swap(x,y);
splay(y,0);
split(y,0);
splay(x,0);
}
else{
y=++tot;
splay(x,0);
int z=tree[x].s[1];
split(x,1);
tree[y].w=v;
tree[y].k=tree[x].k;
tree[y].b=tree[x].b;
tree[y].s[1]=z;
fa[z]=y;
}
add(x,-1,v);
add(y,1,-v);
down(x);
tree[x].s[1]=y;
fa[y]=x;
int t=getlst(x);
root=x;
if (tree[t].k>0){
splay(t,0);
root=t;
int t_=findlg(root,tree[t].w);
if (tree[t_].k==0){
splay(t_,0);
split(t_,1);
root=t_;
}
else{
tree[t].b=LL(tree[t].k)*tree[t].w+tree[t].b;
tree[t].k=0;
}
}
}
printf("%lld\n",tree[getlst(root)].b);
fclose(stdin);
fclose(stdout);
return 0;
}
再来一份堆的做法来做对比。
#include <algorithm>
#include <iostream>
#include <cstdio>
#include <cctype>
#include <set>
using namespace std;
typedef long long LL;
int read()
{
int x=0,f=1;
char ch=getchar();
while (!isdigit(ch)) f=ch=='-'?-1:f,ch=getchar();
while (isdigit(ch)) x=x*10+ch-'0',ch=getchar();
return x*f;
}
const int N=50050;
multiset<int> t;
LL ans;
int n;
int main()
{
freopen("chen.in","r",stdin),freopen("chen.out","w",stdout);
n=read();
for (int i=1,x;i<=n;i++)
{
x=read();
t.insert(x);
if (x<*t.rbegin())
{
ans+=(*t.rbegin())-x;
multiset<int>::iterator it=t.end();
t.erase(--it),t.insert(x);
}
}
printf("%lld\n",ans);
fclose(stdin),fclose(stdout);
return 0;
}