slope trick

简介

考虑一种二维 dp,它的第二维比较巨大,如果直接做会超时或者炸空间。

这种 dp 大多形如 \(f_{i,j}\) 表示前 \(i\) 个东西,第 \(i\) 个位置放了 \(j\) 的最优代价一类的东西。

那么我们是无法在第一维上进行任何操作的,所以我们要考虑对第二维做文章。

例如说,如果第二维的操作每次是给你一个 \(p\),把 \(f_{i,p}\)\(f_{i,1\le i\le p}\)\(\min\),其它东西原样照搬 \(f_{i-1}\),你就可以使用线段树维护第二维去搞。

当然这个东西有个更厉害的名字:整体 dp。但是整体 dp 并不是我们今天要探讨的内容,我们要说的是 slope trick。

比方说,你发现上面我们用线段树维护的东西,如果第二维特别巨大,比如到了 \(10^9\) 甚至 \(10^{18}\),那线段树基本上就没救了。如果你注意到或者说主观认为,这个 dp 数组在 \(i\) 相同时,关于第二维是凸的,你就可以用 slope trick。

然后因为关于第二维是凸的,相当于你相邻两项的差分数组是单调的(递增或递减),我们把这个差分数组叫做斜率

然后 slope trick 还分两种维护方式。分别是维护斜率的拐点和直接维护相邻两个位置的斜率。

维护拐点一般是在,定义域非常大,但是斜率的值域并不大的时候使用的。

而维护斜率一般是在,定义域并不大,但是斜率的值域很大的时候用的。

如果这两个东西同时能用,这里笔者建议使用维护拐点。

维护拐点

Sequence

比较显然的,你设 \(f_{i,j}\) 表示已经填了 \(i\) 个数,第 \(i\) 个数填了 \(j\) 的最小代价。

那转移就是 \(f_{i,j}\leftarrow \min(f_{i-1,k<j}+|j-a_i|)\)

这个东西还是挺显然是凸的的。但是你发现这个式子有点难看,所以你简单改一下。首先把所有 \(a_i\) 减去 \(i\),这样严格上升就变成了单调不降。

\(f_{i,j}\) 表示已经填了 \(i\) 个数,第 \(i\) 个数 \(\le j\) 的最小代价。转移只需要在上面的东西上改为 \(f_{i,j}\leftarrow f_{i-1,j}+|j-a_i|\) 最后再 \(f_{i,j}\leftarrow \min(f_{i,j},f_{i,j-1})\)

然后考虑这俩东西都有什么影响。第一个转移相当于往我们现在的下凸壳上面加了个 V 形函数,也就是说前面部分斜率 \(-1\),后面部分斜率 \(+1\)

然后是第二个转移,把所有函数值对前面取 \(\min\),那也就是说,你下凸壳后面上升的一段没了,全都变成了最低点的值。

所以你维护一个大根堆,里面放所有斜率的拐点(注意如果同一个地方斜率从 \(k_1\) 变为 \(k_2\),那你要把这个点放 \(k_2-k_1\) 次)。然后每次往里扔两个 \(a_i\),再弹出堆顶即可。

显然你最后的堆顶就是 \(b_n\) 的取值,然后假设说前面的 \(b_i\) 的值是 \(a_i\) 做完操作后的堆顶,你考虑怎么把 \(b_i\) 变成我们要输出的方案。

显然如果 \(b_{i+1}\ge b_i\),那你就能取堆顶,直接取就行了;否则,你取的值越小,你的贡献就越大,所以要取的尽量大,也就是说直接取 \(b_{i+1}\) 就是对的。

点击查看代码
#include<bits/stdc++.h>
#define int long long
#define N 1000005
#define mod 998244353
#define pii pair<int,int>
#define x first
#define y second
#define pct __builtin_popcount
#define mpi make_pair
#define inf 2e18
using namespace std;
int T=1,n,a[N],b[N];
void solve(int cs){
    if(!cs)return;
    cin>>n;
    priority_queue<int>q;
    int res=0;
    for(int i=1;i<=n;i++){
        cin>>a[i];
        a[i]-=i;
        q.push(a[i]);
        q.push(a[i]);
        q.pop();
        b[i]=q.top();
    }
    res+=abs(a[n]-b[n]);
    for(int i=n-1;i;i--){
        b[i]=min(b[i],b[i+1]);
        res+=abs(a[i]-b[i]);
    }
    cout<<res<<'\n';
    for(int i=1;i<=n;i++){
        cout<<b[i]+i<<'\n';
    }
}
signed main(){
	ios::sync_with_stdio(0);
	cin.tie(0);cout.tie(0);
	// cin>>T;
	// init(3e5);
	for(int cs=1;cs<=T;cs++){
		solve(cs);
	}
    // cerr<<clock()*1.0/CLOCKS_PER_SEC<<'\n';
	return 0;
}

Safety

首先我们写出朴素 dp:设 \(f_{i,j}\) 表示已经放好前 \(i\) 个位置,第 \(i\) 个位置的高度是 \(j\) 的最小代价。

那么转移就是:\(f_{i,j}\leftarrow\displaystyle\min_{k=j-H}^{j+H}f_{i-1,k}+|s_i-j|\)。然后不难发现,这个东西是下凸的。可以尝试使用 slope trick 做。

我们观察该 dp 的定义域与斜率的值域。发现定义域很大,但斜率的值域并不大,所以我们考虑维护拐点。

用 slope trick 做第一步应当先把式子拆成几部分,并观察这几部分在函数上的影响。

所以我们拆成 \(f_{i,j}\leftarrow \min f_{i-1,k}\)\(f_{i,j}\leftarrow |j-s_i|\) 两步来看。

不难发现第一步就是,把最低段的左端点和右端点往两边扩展 \(H\)

第二也是显然且套路的,相当于往函数上加一个 V 状物。那我们直接把 \(s_i\) 左边的部分斜率全部 \(-1\),右边的部分斜率全部 \(+1\) 即可。

于是我们维护对顶堆。那么操作一就是直接对两个堆打标记,操作二就是在某个堆插入两个 \(s_i\) 并把这个堆的堆顶转移到另一个堆。

点击查看代码
#include<bits/stdc++.h>
#define int long long
#define N 1000005
#define mod 998244353
#define pii pair<int,int>
#define x first
#define y second
#define pct __builtin_popcount
#define mpi make_pair
#define inf 2e18
using namespace std;
int T=1,n,h,a[N];
void solve(int cs){
    if(!cs)return;
    cin>>n>>h;
    for(int i=1;i<=n;i++){
        cin>>a[i];
    }
    priority_queue<int>q1;
    priority_queue<int,vector<int>,greater<int>>q2;
    int t1=0,t2=0;
    q1.push(a[1]);
    q2.push(a[1]);
    int res=0;
    for(int i=2;i<=n;i++){
        t1-=h;
        t2+=h;
        if(a[i]>=q1.top()+t1&&a[i]<=q2.top()+t2){
            q1.push(a[i]-t1);
            q2.push(a[i]-t2);
        }
        else if(a[i]<q1.top()+t1){
            res+=q1.top()+t1-a[i];
            q1.push(a[i]-t1);
            q1.push(a[i]-t1);
            q2.push(q1.top()+t1-t2);
            q1.pop();
        }
        else{
            res+=a[i]-q2.top()-t2;
            q2.push(a[i]-t2);
            q2.push(a[i]-t2);
            q1.push(q2.top()+t2-t1);
            q2.pop();
        }
    }
    cout<<res<<'\n';
}
signed main(){
	ios::sync_with_stdio(0);
	cin.tie(0);cout.tie(0);
	// cin>>T;
	// init(3e5);
	for(int cs=1;cs<=T;cs++){
		solve(cs);
	}
    // cerr<<clock()*1.0/CLOCKS_PER_SEC<<'\n';
	return 0;
}
posted @ 2025-10-27 09:09  zxh923  阅读(5)  评论(0)    收藏  举报