CF280E - Sequence Transformation
给定一个不降整数序列
利用凸函数性质维护导数
我们设
然后有转移式,设
我们发现
然后如果
所能转移到的函数段单调下降,
所能转移到的函数段包含顶点,
所能转移到的函数段单调上升,
这是很好证明的,在凸函数上这些都是显然的。然后我们就发现,如果是导数的话:
这就很好了!因为我们发现,新的函数就是把原来的函数从中间断开,然后左边往右平移
然后我们发现,我们每次只是平移,然后加上一次函数,所以导函数也一直是一次函数,这就很好分段维护了。考虑分段维护当前所有的
讲一下卡了我很久的一个问题,就是我们的函数可能不是连续的。我一开始认为它一定是连续的,所以在发现函数不连续且没有
最后我们以
const int N=6000;
const ld limit=0.000000001;
ll n,x[N+5],q,a,b;
ld y[N+5];
struct slope{
ld k,b,l,r;
slope(ld K,ld B,ld L,ld R){
k=K,b=B,l=L,r=R;
}
slope(){
k=0,b=0,l=0,r=0;
}
ld zerop(){
return -b/k;
}
};
slope dp[3*N+5],f[3*N+5];
ld k[N+5];
int m,cnt;
int main(){
scanf("%lld%lld%lld%lld",&n,&q,&a,&b);
rep(i,1,n)scanf("%lld",&x[i]);
dp[m=1]=slope(2,-2*x[1],1,q);
rep(i,2,n){
cnt=0;
k[i]=dp[1].l;
rep(j,1,m){
ld c=dp[j].zerop();
if(dp[j].l<c+limit && c<dp[j].r+limit)k[i]=c;
if(j!=1&&dp[j-1].k*dp[j].l+dp[j-1].b+limit<0){
if(dp[j].k*dp[j].l+dp[j].b>limit)k[i]=dp[j].l;
}
}
if(k[i]>q+limit)k[i]=q;
rep(j,1,m){
if(dp[j].r<k[i]+limit){//r<=k
f[++cnt]=slope(dp[j].k,dp[j].b-a*dp[j].k,dp[j].l+a,dp[j].r+a);
}else if(dp[j].l>k[i]+limit){//l>k
f[++cnt]=slope(dp[j].k,dp[j].b-b*dp[j].k,dp[j].l+b,dp[j].r+b);
}else{//l<=k<r
if(dp[j].l+limit<k[i]){//l<k
f[++cnt]=slope(dp[j].k,dp[j].b-a*dp[j].k,dp[j].l+a,k[i]+a);
}
f[++cnt]=slope(0,0,k[i]+a,k[i]+b);
f[++cnt]=slope(dp[j].k,dp[j].b-b*dp[j].k,k[i]+b,dp[j].r+b);
}
}
m=cnt;
rep(j,1,m){
dp[j]=f[j];
dp[j].k=dp[j].k+2;
dp[j].b=dp[j].b-2*x[i];
}
}
ld ans=dp[1].l;
rep(j,1,m){
ld c=dp[j].zerop();
if(dp[j].l<c+limit && c<dp[j].r+limit)ans=c;
if(j!=1&&dp[j-1].k*dp[j].l+dp[j-1].b+limit<0)
if(dp[j].k*dp[j].l+dp[j].b>limit)ans=dp[j].l;
}
if(ans>q+limit)ans=q;
per(i,1,n){
y[i]=ans;
if(ans-b+limit>k[i])ans=ans-b;
else if(ans-a<k[i]+limit)ans=ans-a;
else ans=k[i];
}
ld sum=0;
rep(i,1,n){
sum=sum+(x[i]-y[i])*(x[i]-y[i]);
printf("%.8Lf ",y[i]);
}
printf("\n%.8Lf\n",sum);
return 0;
}
//Nyn-siyn-hert
二次函数规划
金老师讲的做法,妙妙。
这个算法的核心是这样的。假设当
我们需要实现一个功能,一个函数 ld solve(int i,ld l,ld r,ld A,ld B)
,表示返回一个
我们发现,想要求出
然后考虑如何求取规划。
首先证明一个引理:
- 假设存在一个前
位的子问题的最优解 ,并且 ( 是 的最优解),那么一定存在一个前 位的子问题的最优解 满足 。
这个结论看起来是显然的,不过我们还是证一下。(金老师上课没讲,所以这个做法是我自己搞的,需要上一个做法的结论。不知道金老师的证法需不需要)
首先考虑最优解的
然后假设
同理,又有
- 假设存在一个前
位的子问题的最优解 , ( 是 的最优解),那么一定存在一个前 位的子问题的最优解 满足 。
这个证明过程和上面是一样的,就不证了。
那么,现在我们尝试规划
然后,如果
如果
每次最多倒退
const ld eps=1e-8;
int n,x[6005],q,a,b;
ld dp[6005],y[6005],ans;
inline bool lt(ld x,ld y){
return x+eps<y;
}
inline bool gt(ld x,ld y){
return x>y+eps;
}
inline bool le(ld x,ld y){
return x<y+eps;
}
inline ld solve(int i,ld l,ld r,ld A,ld B){
if(lt(l,1))l=1;
if(lt(q,r))r=q;
ld cyr=-B/(2*A);
if(lt(r,cyr))cyr=r;
if(lt(cyr,l))cyr=l;
if(i==1||(le(dp[i-1]+a,cyr)&&le(cyr,dp[i-1]+b))){
return cyr;
}else if(gt(dp[i-1]+a,cyr)){
return solve(i-1,l-a,r-a,A+1,B-2*x[i-1]+2*a*A)+a;
}else{
return solve(i-1,l-b,r-b,A+1,B-2*x[i-1]+2*b*A)+b;
}
}
signed main(){
ios::sync_with_stdio(false);
cin.tie(0);
cin>>n>>q>>a>>b;
rp(i,n)cin>>x[i];
dp[1]=x[1];
rep(i,2,n){
if(le(dp[i-1]+a,x[i])&&le(x[i],dp[i-1]+b)){
dp[i]=x[i];
}else if(gt(dp[i-1]+a,x[i])){
dp[i]=solve(i,1,q,1,-2*x[i]);
}else{
dp[i]=solve(i,1,q,1,-2*x[i]);
}
}ld res=dp[n];
cout<<fixed<<setprecision(8);
per(i,1,n){
y[i]=res;
ans+=(res-x[i])*(res-x[i]);
if(gt(dp[i-1]+a,res))res=res-a;
else if(gt(res,dp[i-1]+b))res=res-b;
else res=dp[i-1];
}
rep(i,1,n)cout<<y[i]<<" ";
cout<<endl;
cout<<ans<<endl;
return 0;
}
//Nyn-siyn-hert
优化导数维护
我们考虑使用类似
首先看到 split
,这个在无旋平衡树很多时候是核心操作,但是这里我们只需要插入,还不是按照下标插入。而我们发现我们还有一个功能是找零点,那么为什么不把这两个功能合并起来呢?
具体而言就是,我们用两个函数 splitl
和 splitr
,一个把右端点取值大于等于 splitr
一次得到 splitl
把左端点取值大于等于
如果
如果
然后考虑我们需要进行的操作,一个是子树整体平移,一个是子树整体加一次函数。我们可以设计三个
然后考虑合并
但是合并
具体而言,本来的
而我们只需要在 split
之后给
然后考虑 merge
的问题。我们发现,我们的树还有一个好处,就是我们不需要上传什么东西。我们不需要维护区间信息。那么在没有 pushup
的情况下,我们的 merge
就会好些很多。
最后是插入新段。对于 merge
,避免了开一堆新的单点子树然后套好多层 merge
的情况。
我们直接 split
出最终的零点,倒推答案。
const ld eps=1e-8;
int n,X[6005],q,a,b;
ld dp[6005],y[6005],ans;
inline bool lt(ld x,ld y){
return x+eps<y;
}
mt19937 rng(time(0));
struct node{
ld k,b,l,r,addk,addb;
int ls,rs,mov;
node(){}
node(ld _k,ld _b,ld _l,ld _r){
mov=0,addk=0,addb=0,ls=0,rs=0;
k=_k,b=_b,l=_l,r=_r;
}
}tr[114514];
int cnt=0,root;
#define ls(x) tr[x].ls
#define rs(x) tr[x].rs
inline void addtg(int x,ld k,ld b,int m){
if(!x)return;
tr[x].addb+=-m*tr[x].addk+b;
tr[x].addk+=k;
tr[x].mov+=m;
}
inline void pushdown(int x){
if(!x)return;
tr[x].l+=tr[x].mov;
tr[x].r+=tr[x].mov;
tr[x].b-=tr[x].mov*tr[x].k;
tr[x].k+=tr[x].addk;
tr[x].b+=tr[x].addb;
addtg(ls(x),tr[x].addk,tr[x].addb,tr[x].mov);
addtg(rs(x),tr[x].addk,tr[x].addb,tr[x].mov);
tr[x].addk=0,tr[x].addb=0,tr[x].mov=0;
}
inline void splitl(int x,int &l,int &r){
if(!x)return (void)(l=r=0);
pushdown(x);
ld val=tr[x].k*tr[x].l+tr[x].b;
if(le(0,val)){
r=x,splitl(ls(x),l,ls(x));
}else l=x,splitl(rs(x),rs(x),r);
}
inline void splitr(int x,int &l,int &r){
if(!x)return (void)(l=r=0);
pushdown(x);
ld val=tr[x].k*tr[x].r+tr[x].b;
if(lt(0,val)){
r=x,splitr(ls(x),l,ls(x));
}else l=x,splitr(rs(x),rs(x),r);
}
inline int merge(int l,int r){
pushdown(l);
pushdown(r);
if(!l)return r;
if(!r)return l;
int key=rng()%2;
if(key){
tr[r].ls=merge(l,tr[r].ls);
return r;
}else {
tr[l].rs=merge(tr[l].rs,r);
return l;
}
}
inline int find_first(int x){
return tr[x].ls?find_first(tr[x].ls):x;
}
signed main(){
ios::sync_with_stdio(false);
cin.tie(0);
cin>>n>>q>>a>>b;
rp(i,n)cin>>X[i];
cnt++,root=1,tr[0].l=q;
tr[1]=node(2,-2*X[1],1,q);
rep(i,2,n){
int l,x,r,R;
splitr(root,l,r),splitl(r,x,r);
addtg(l,0,0,a),addtg(r,0,0,b);
R=find_first(r);
if(!x){
x=++cnt,dp[i-1]=tr[R].l;
tr[x]=node(0,0,tr[R].l+a,tr[R].l+b);
root=merge(merge(l,x),r);
}else{
ld cyr=-tr[x].b/tr[x].k;
dp[i-1]=cyr;
if(lt(tr[x].l,cyr)){
tr[++cnt]=node(tr[x].k,tr[x].b-a*tr[x].k,tr[x].l+a,cyr+a);
tr[x].ls=cnt;
}
if(lt(cyr,tr[x].r)){
tr[++cnt]=node(tr[x].k,tr[x].b-b*tr[x].k,cyr+b,tr[x].r+b);
tr[x].rs=cnt;
}tr[x].k=0,tr[x].b=0,tr[x].l=cyr+a,tr[x].r=cyr+b;
root=merge(merge(l,x),r);
}
addtg(root,2,-2*X[i],0);
}
cout<<fixed<<setprecision(8);
int l,x,r,R;
splitr(root,l,r);
splitl(r,x,r);
R=find_first(r);
ld res=(x?(-tr[x].b/tr[x].k):tr[R].l);
if(lt(q,res))res=q;
per(i,1,n){
y[i]=res;
ans+=(res-X[i])*(res-X[i]);
if(gt(dp[i-1]+a,res))res=res-a;
else if(gt(res,dp[i-1]+b))res=res-b;
else res=dp[i-1];
}
rep(i,1,n)cout<<y[i]<<" ";
cout<<endl;
cout<<ans<<endl;
return 0;
}
//Nyn-siyn-hert
两个做法之间的共同点
实际上,我们发现,这道题最关键的地方就是“对前
三个做法中,这个东西都至关重要。
在第一个和第三个做法中,这个点是原函数的顶点,导函数的零点,有了它才能决定新函数在哪里断开。
在第二个做法中,这个点是预处理的前缀问题的最优解。当一个前缀问题中,前缀的前缀前缀的最优解和前缀的后缀的最优规划互不冲突,就找到了当前子问题的解。然后就可以往后推了。
这是数学归纳思想,或者说递推思想在
这道题我学到了什么
其实,最主要的是第三个做法,搞清楚了多个互相影响的
然后,就是用分段函数 dp
的方法。不知道是不是我做题太少,没有遇到其他这样的题。
最后,是子问题规划的思想。但是在现在的我看来这个做法就像一个纸飞机,只有别人当面给我折叠一遍,我才能理解这个方法。否则这种巧妙的做法是我完全无法想到,下次遇到类似题目也难以再一次做出的。所以它对现在的我来说反而价值不大,不知道未来的自己能不能真正掌握这种思想。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 25岁的心里话
· 按钮权限的设计及实现