注意,本博客的任何内容不应被滥用、随意转载。

斜率技巧

务必结合阅读 Slope Trick · USACO Guide。

  1. CF865D Buy Low Sell High

请你先完成该问题 \(n\le 5\times 10^3\) 的版本。

写出一个 dp 很容易。\(f_{i,j}=\max(f_{i-1,j-1}-p_i,f_{i-1,j},f_{i-1,j+1}+p_i)\ (j\in[0,i])\)。存在简单的变形:令 \(ans=-\sum\limits_{i=1}^n p_i\),转移修改为 \(f_{i,j}=\max(f_{i-1,j-1},f_{i-1,j}+p_i,f_{i-1,j+1}+2p_i)\),答案变成 \(ans+f_{n,0}\)。

定义 \(f_i\) 在 \(j\) 处的斜率为 \(d_{i,j}=f_{i,j}-f_{i,j+1}\),我们可以归纳证明 \(d_i\) 是递增的,即 \(f_i\) 凹。先继续阅读。

将转移分解为:

  1. 把 \(f_{i-1}\) 视为一个定义域为 \([0,i-1]\) 的函数,右移一位成为新的 \(f_i\),注意 \(f_{i,0},d_{i,0}\) 此时视为 \(-\infty\)。

  2. 对 \(j\in [0,i-1]\),同时执行 \(f_{i,j}\overset\max\gets f_{i,j+1}+p_i\)(等价于同时判定若 \(d_{i,j}< p_i\),执行 \(f_{i,j}\gets f_{i,j+1}+p_i\))两轮。

因斜率递增可以见得:若 \(k\) 为 \([0,i-1]\) 中最后一个满足 \(d_{i,k}< p_i\) 的位置,应对 \(j\in[0,k]\) 依次执行 \(f_{i,j}\gets f_{i,j+1}+p_i\)。考察对 \(d\) 的影响:对 \(j\in[0,k-1]\) 同时执行 \(d_{i,j}\gets d_{i,j+1}\),然后令 \(d_{i,k}\gets p_i\)。即在 \(d_i\) 这一有序数组中插入 \(p_i\)。

若用堆 \(q\) 维护 \(d_i\),\(2\) 类转移即在 \(q\) 中插入 \(p_i\) 两次。由于堆中没有下标限制,两次转移后 \(d_{i,-1}\) 被定义,此时应删去 \(q\) 中最小值。

考虑 \(ans\) 实际值为 \(f_{n,n}\),将 \(q\) 中值全部累加即对斜率求后缀和,得到 \(f_{n,0}\)。

#include<bits/stdc++.h>
#define LL long long
#define fi first
#define se second
using namespace std;

const int N=3e5+10;
int n,p;
priority_queue<int,vector<int>,greater<int>>q;
LL ans;

signed main(){
    //freopen(".in","r",stdin);
    //freopen(".out","w",stdout);
    scanf("%d",&n);
    for(int i=1;i<=n;i++){
        scanf("%d",&p);
        ans-=p;
        q.push(p);
        q.push(p);
        q.pop();
    }
    for(int i=1;i<=n;i++)ans+=q.top(),q.pop();
    printf("%lld\n",ans);
    return 0;
}

在此我们已对该优化思想建立一定认知:借助 dp 数组的凹凸性即其差分数组的单调性,将转移在维护差分数组的高效数据结构上演绎。

  1. P12074 OOI 2025 The arithmetic exercise

Arithmetic Exercise 题解——FFTotoro

(题外话:该题解体现了 dp 不仅可以构造操作以得到答案,也可以直接构造能够体现合法操作存在的答案)

这道题的转移与上题相似,有如下区别:第二维存在给出的上界 \(n\)、平移后转移不连续、答案是 \(f_{n,i}\)。该题解非常优秀,注意其代码的最后一步为求出斜率的最大前缀和。

关于凸性、暴力做法参见其它题解。

  1. LMIO19_bulves Potatoes ruhejisuanand fertilizers

考虑构造答案。若 \(i\) 处最终有 \(a_i'\) 个肥料,那么操作次数最小值为 \(\sum\limits_{i=1}^{n-1}|\sum\limits_{j=1}^i a_j-a'_j|\),可以理解为拆贡献到每一个 \(i\to i+1\) 的间隙。此外要求 \(a'_i\ge b_i\)。

设 \(d_i=a_i-b_i,d'_i=a'_i-b_i\),\(s_i=\sum\limits_{j=1}^{i}d'_j-d_j\),\(f_{i,j}\) 表示已处理前 \(i\) 位且 \(s_i=j\) 时的最小代价。因 \(i\to i+1\) 处的贡献为 \(|j|\),要求 \(d'_i\ge 0\),使用前缀最小值优化后得到转移 \(f_{i,j}=\min(f_{i,j-1},f_{i-1,j+d_i}+|j|)\)。

#include<bits/stdc++.h>
#define LL long long
#define fi first
#define se second
using namespace std;

const int N=3e3+10,M=3e4+10;
int n,a[N],b[N],f[2][M<<1],s,d[N];

signed main(){
    // freopen(".in","r",stdin);
    // freopen(".out","w",stdout);
    scanf("%d",&n);
    for(int i=1;i<=n;i++){
        scanf("%d%d",&a[i],&b[i]);
        s+=a[i];
        d[i]=a[i]-b[i];
    }
    assert(n<=3000&&s<=30000);
    memset(f,0x3f,sizeof(f));
    for(int i=M;i<=M+s;i++)f[i&1][i]=0;
    for(int i=1;i<n;i++)
        for(int j=M-s;j<=M+s;j++)
            f[i&1][j]=min(f[i&1][j-1],abs(j-M)+f[i&1^1][min(M+s,max(M-s-1,j+d[i]))]);
    printf("%d\n",f[n&1^1][min(M+s,max(M-s-1,M+d[n]))]);
    return 0;
}

将转移分解为:

  1. 把 \(f_{i-1}\) 左移 \(d_i\) 位作为新的 \(f_i\)。
  2. 令 \(f_{i,j}\overset+\gets |j|\),即对函数 \(f_i\) 全局加 \(|x|\)。
  3. 取前缀 \(\min\)。

1 类转移通过将后续所有 2 类转移右移 \(d_i\) 位实现。设 \(D_i=\sum\limits_{j=1}^i d_j\) 即在 \(f_{i-1}\to f_i\) 的转移中将全局加 \(|x|\) 改为加 \(|x-D_i|\)。可以看到 \(f_i\) 是若干个绝对值函数的和函数再做一些前缀 \(\min\),最终仍为一个凹包。

以上是 \(f_i\) 的函数图像。黄色数字表示正在递减的斜率 \(f_{i,j}-f_{i,j+1}\)。右侧绿点是要求的 \(f_{n,D_n}\)。由于 \(D_n\ge 0\),不需要表示 \(f_{i,j<0}\),故左侧绿点表示 \(f_{i,0}\) 的值。红点表示斜率的变化点,设其点集为 \(q\),最终 \(f_{n,D_n}=f_{n,0}-\sum\limits_{x\in q}\min(x,D_n)\)。

全局加 \(|x-D_i|\),即将 \(D_i-1\) 及其左侧斜率 \(+1\),\(D_i\) 及其右侧斜率 \(-1\),加入两位于 \(D_i\) 的红点表示斜率变化 \(-2\) 实现。这时最右侧段斜率为 \(0\),此段末端及右侧斜率被视为 \(-1\),删除最右侧红点即实现维护前缀 \(\min\)。

#include<bits/stdc++.h>
#define LL long long
#define fi first
#define se second
using namespace std;

const int N=5e5+10;
int n,a[N],b[N];
LL ans,d[N];
priority_queue<LL>q;

signed main(){
    scanf("%d",&n);
    for(int i=1;i<=n;i++){
        scanf("%d%d",&a[i],&b[i]);
        d[i]=a[i]-b[i]+d[i-1];
    }
    for(int i=1;i<n;i++){
        if(d[i]<0)ans-=d[i],d[i]=0;
        ans+=d[i];
        q.push(d[i]);
        q.push(d[i]);
        q.pop();
    }
    while(q.size()){
        ans-=min(d[n],q.top());
        q.pop();
    }
    printf("%lld\n",ans);
    return 0;
}

你必须注意,差分的方向是可以自由选定的。左减右、右减左没有本质区别。如下题即右减左。

  1. P3642 APIO2016 烟花表演

毫不避讳地说,这篇题解——UltiMadow 几乎是完美的。

唯一的瑕疵在于过于感性地描述 4 个转移的得出,以及缺少一些堆操作的解释。

在此给出解释。你可以改一改第一篇题解给出的图片,然后把上图中黑色部分视为 \(|w-x+y|\),绿点视为 \(x\),选中它们并缓缓从左向右移动,跟随题解分类讨论。

当你确信自己理解了全部 4 个转移后,探讨实际的堆操作。

  1. \(f_v(x)+w\ (x<L)\) 是整体上移,不改变斜率。
  2. \(f_v(L)+w-x+L\ (x\in[L,L+w))\) 内部斜率明显为 \(-1\),\(L\) 处斜率为 \((f_v(L)+w-L+L)-(f_v(L-1)+w)=f_v(L)-f_v(L-1)\) 即不变。
  3. \(f_v(L)\ (x\in [L+w,R+w])\) 内部斜率为 \(0\),\(L+w\) 处斜率为 \(f_v(L)-[f_v(L)+w-(L+w-1)+L]=-1\)。
  4. \(f_v(R)+x-R-w\ (x\in (R+w,+\infty))\) 内部斜率为 \(1\),\(R+w+1\) 处斜率为 \(f_v(R)+R+w+1-R-w-f_v(L)=1\)。

删除 \(L\) 及其后的所有拐点导致 \(L+1\) 及其右侧所有斜率为 \(-1\)。插入 \(L+w\) 导致 \(L+w+1\) 及其右侧所有斜率为 \(0\),插入 \(R+w\) 导致 \(R+w+1\) 及其右侧所有斜率为 \(1\)。达成目的。

由于将 \(u\) 的 \(c\) 个子节点合并时,获取了 \(c\) 个形如 \(R+w\) 的拐点,对 \(u\) 的拐点堆弹出 \(c-1\) 次后堆顶为 \(R\),再弹出一次亦得到 \(L\)。

\(f_{1,0}=\sum w\),按照题目 3 应再对拐点坐标累减到 \(L\) 为止,得到凹包底部。

#include<bits/stdc++.h>
#define LL long long
#define fi first
#define se second
using namespace std;

const int N=1e6+10;
int n,m,rt[N],p[N],w[N],l[N],r[N],d[N],idx;
LL ans,h[N];

mt19937 rnd(time(0));

int mrg(int x,int y){
    if(!x||!y)return x+y;
    if(h[x]<h[y])swap(x,y);
    if(rnd()&1)swap(l[x],r[x]);
    l[x]=mrg(l[x],y);
    return x;
}
void pop(int &x){
    x=mrg(l[x],r[x]);
}
int nw(LL x){
    h[++idx]=x;
    return idx;
}

signed main(){
    //freopen(".in","r",stdin);
    //freopen(".out","w",stdout);
    scanf("%d%d",&n,&m);
    for(int i=2;i<=n+m;i++){
        scanf("%d%d",&p[i],&w[i]);
        d[p[i]]++;
        ans+=w[i];
    }
    for(int i=n+m;i>1;i--){
        while(d[i]>1)d[i]--,pop(rt[i]);
        LL R=h[rt[i]];pop(rt[i]);
        LL L=h[rt[i]];pop(rt[i]);
        rt[i]=mrg(rt[i],mrg(nw(L+w[i]),nw(R+w[i])));
        rt[p[i]]=mrg(rt[p[i]],rt[i]);
    }
    while(d[1])d[1]--,pop(rt[1]);
    while(rt[1])ans-=h[rt[1]],pop(rt[1]);
    printf("%lld\n",ans);
    return 0;
}
  1. 一道私题(X23632)

题解在此:

#include<bits/stdc++.h>
#define LL long long
#define fi first
#define se second
#define mp make_pair
using namespace std;

const int N=2e5+10;
int n;
LL now;
pair<int,int>a[N];
priority_queue<int>q;

signed main(){
    freopen("forge.in","r",stdin);
    freopen("forge.out","w",stdout);
    scanf("%d",&n);
    for(int i=1;i<=n;i++)scanf("%d%d",&a[i].fi,&a[i].se);
    sort(a+1,a+n+1,[](auto u,auto v){return mp(u.se,-u.fi)<mp(v.se,-v.fi);});
    // for(int i=1;i<=n;i++)printf("%d %d\n",a[i].fi,a[i].se);
    q.push(-1e9-1);
    for(int i=1;i<=n;i++){
        int A=a[i].fi,B=a[i].se-a[i-1].se;
        while(1){
            if(B>-q.top())printf("%lld ",now-=q.top()),B+=q.top(),q.pop();
            else{
                int tmp=-q.top();q.pop();
                now+=B;if(tmp!=1e9+1)tmp-=B;
                q.push(-tmp);
                break;
            }
        }
        q.push(-A);
    }
    while(q.size()>1)printf("%lld ",now-=q.top()),q.pop();
    puts("");
}
posted @ 2026-02-27 21:41  zeehS  阅读(32)  评论(2)    收藏  举报
要想见证尘封的秘密,就要承受最严厉的惩罚。