单调队列和斜率优化 dp

本文参考了 OI Wiki 和 《算法竞赛进阶指南》。

引入:单调队列

定义

单调队列是一种可以在两头弹出元素,只在队尾插入元素的双端队列。

单调队列的元素满足某种单调性。在插入新的元素前,需要去掉原来的元素中不符合单调性的元素,然后加上新的元素。故而其解决的问题需要有某种单调性,不满足单调性的元素必在将来不能作为可以计入答案的元素。这样导致每一次在队头总会有满足最值性质的元素。

例题:滑动窗口

代码

点此查看代码
#include <bits/stdc++.h>
using namespace std;
const int maxn=1000010;
int n,k,i,l,r;
int a[maxn],q[maxn];
int main(){
    scanf("%d%d",&n,&k);
    for(i=1;i<=n;++i) scanf("%d",a+i);
    l=q[1]=1;
    for(i=1;i<k;++i){
        while(l<=r&&a[i]<=a[q[r]]) --r;
        q[++r]=i;
    }
    for(i=k;i<=n;++i){
        if(i-q[l]==k) ++l;
        while(l<=r&&a[i]<=a[q[r]]) --r;
        q[++r]=i;printf("%d ",a[q[l]]);
    }
    l=1;r=0;putchar('\n');
    for(i=1;i<k;++i){
        while(l<=r&&a[i]>=a[q[r]]) --r;
        q[++r]=i;
    }
    for(i=k;i<=n;++i){
        if(i-q[l]==k) ++l;
        while(l<=r&&a[i]>=a[q[r]]) --r;
        q[++r]=i;printf("%d ",a[q[l]]);
    }
}

单调队列优化 dp

适用条件

形如 \(dp_i=u(i)+\min_{j\in[l_i,r_i]}v(j)\) (其中若 \(v(j)\)\(dp_j\) 有关,则有 \(r_i<i\))。

\(r\) 单调不降,则 dp 式的计算中,可以处理一个 \(v(j)\) 单调递增的单调队列。在计算 \(dp_i\) 时,先在队列上二分找到第一个不小于 \(l_i\) 的元素,其对应的值即为 \(dp_i\) 的最优决策。在将 \(\forall j\in[r_i,r_{i+1}]\) 入队前将 \(\forall k<j,v(k)\ge v(j)\)\(k\) 弹出队列,再将 \(j\) 从小到大入队。

由于之后 \(\forall p>i,k\in[l_p,r_p]\),则必有 \(j\in[l_p,r_p]\),而始终有 \(v(k)\ge v(j)\),故而 \(j\)\(k\) 能作为某个合法决策时总是可以替代 \(k\)

由于每个元素保证只会入队/出队一次,则最后转移的总时间复杂度是 \(O(n\log n)\) 的。

如果不能保证 \(r\) 单调不降,则此题型可以用线段树优化 dp 解决。

当然,如果同时保证 \(l\) 单调不降,则可以在转移 \(dp_i\) 之前排除过时队头,可以做到转移的总时间复杂度是 \(O(n)\) 的。

例题1:CF372C Watching Fireworks is Fun

题意

一个城镇有 \(n\) 个区域,从左到右从 \(1\) 编号为 \(n\),每个区域之间距离 \(1\) 个单位距离。

节日中有 \(m\) 个烟花要放,给定放的地点 \(a_i\),时间 \(t_i\),如果你当时在区域 \(x\),那么你可以获得 \(b_i-|a_i-x|\) 的开心值。

你每个单位时间可以移动不超过 \(d\) 个单位距离。

你的初始位置是任意的(初始时刻为 \(1\)),求你通过移动能获取到的最大的开心值。

\(n\le 1.5\times 10^5,m\le 300\)

解法

\(dp_{i,j}\) 为在烟花 \(i\) 燃放时处于位置 \(j\) 的最大快乐值。

考虑 \(dp_{i,j}\) 能够从哪些 \(dp_{i-1,k}\) 值转移而来。显然需要 \(|k-i|\le(t_i-t_{i-1})d\)。故而转移有 \(dp_{i,j}=b_i-|a_i-j|+\max_{k\in[i-(t_i-t_{i-1})d,i+(t_i-t_{i-1})d]} dp_{i-1,k}\)

代码

点此查看代码
#include <bits/stdc++.h>
using namespace std;
#define ll long long
const int maxm=310;
const int maxn=150010;
int n,m,d,l,r,i,j;
int q[maxn];
ll dp[2][maxn],*dp1=dp[0],*dp2=dp[1];
ll a,b,t,lst=1,del,det;
template<typename T>inline T Abs(const T &a){return a>0?a:-a;}
int main(){
    scanf("%d%d%d",&n,&m,&d);
    for(i=1;i<=m;++i){
        scanf("%lld%lld%lld",&a,&b,&t);
        del=(t-lst)*d;
        l=r=q[1]=1;det=b-a;
        for(j=1;j<=n;++j){
            if(j<=a) ++det;else --det;
            while(l<=r&&q[l]+del<j) ++l;
            dp2[j]=dp1[q[l]]+det;
            while(l<=r&&dp1[j+1]>dp1[q[r]]) --r;
            q[++r]=j+1;
        }
        l=r=1;q[1]=n;
        det=b-Abs(n+1-a);
        for(j=n;j;--j){
            if(j<a) --det;else ++det;
            while(l<=r&&q[l]-del>j) ++l;
            dp2[j]=max(dp2[j],dp1[q[l]]+det);
            while(l<=r&&dp1[j-1]>dp1[q[r]]) --r;
            q[++r]=j-1;
        }
        lst=t;
        swap(dp1,dp2);
    }
    a=-11451419198101926;
    for(i=1;i<=n;++i) a=max(a,dp1[i]);
    printf("%lld\n",a);
    return 0;
}

例题2:Fence

题意

\(N\) 块木板从左到右排成一行,有 \(M\) 个工匠对这些木板进行粉刷,每块木板至多被粉刷一次。

\(i\) 个木匠要么不粉刷,要么粉刷包含木板 \(S_i\) 的,长度不超过 \(L_i\) 的连续的一段木板,每粉刷一块可以得到 \(P_i\) 的报酬。

不同工匠的 \(S_i\) 不同。

请问如何安排能使工匠们获得的总报酬最多。

\(N\le 16000,M\le 100\)

解法

考虑维护 \(dp_{i,j}\) 表示考虑第 \(i\) 位工匠,和第 \(1\sim j\) 块木板时的最大收益。

考虑 \(dp_{i,j}\) 可以从哪些 \(dp\) 值转移而来。

  • \(i\) 位工匠可以不粉刷木板,此时 \(dp_{i,j}=\max(dp_{i,j},dp_{i-1,j})\)
  • \(j\) 块木板可以不粉刷,此时 \(dp_{i,j}=\max(dp_{i,j},dp_{i,j-1})\)
  • \(i\) 位工匠粉刷包括第 \(S_i\sim j\) 的木板,此时 \(\begin{aligned}dp_{i,j}&=\max(dp_{i,j},\max_{k\in[j-L_i,S_i-1]}(dp_{i-1,k}+(j-k)P_i))\\&=\max(dp_{i,j},\max_{k\in[j-L_i,S_i-1]}(dp_{i-1,k}-kP_i)+jP_i)\end{aligned}\)

此时可以直接预处理 \([S_i-L_i,S_i-1]\) 的决策集合,对于每一个 \(dp_{i,j}(j\in[S_i,S_i+L_i-1])\) 作转移即可。

注意需要对所有工匠按 \(\boldsymbol{S_i}\) 升序排序,可能有某位工匠在中间新粉刷木板的情况。

代码

点此查看代码
#include <bits/stdc++.h>
using namespace std;
const int maxn=16100;
const int maxm=110;
int dp[2][maxn],v[maxn],q[maxn];
int *f=dp[0],*g=dp[1];
int n,m,i,j,b,l,p,s,lt,rt; 
struct gj{
    int l,p,s;
    inline bool operator <(const gj &a)const{return s<a.s;}
}N[maxm];
int main(){
    scanf("%d%d",&n,&m);
    for(i=1;i<=m;++i) scanf("%d%d%d",&N[i].l,&N[i].p,&N[i].s);
    sort(N+1,N+m+1);
    for(i=1;i<=m;++i){
        l=N[i].l,p=N[i].p,s=N[i].s;
        for(j=1;j<=n;++j) v[j]=g[j]-j*p;
        swap(f,g);
        for(j=1;j<s;++j) g[j]=max(g[j-1],f[j]);
        lt=1;rt=0;
        for(j=max(s-l,0);j<s;++j){
            while(rt&&v[q[rt]]<=v[j]) --rt;
            q[++rt]=j;
        }
        b=min(s+l-1,n);
        for(j=s;j<=b;++j){
            g[j]=max(g[j-1],f[j]);
            if(q[lt]<j-l) ++lt;
            g[j]=max(g[j],j*p+v[q[lt]]);
        }
        for(j=b+1;j<=n;++j) g[j]=max(g[j-1],f[j]);
    }
    printf("%d",g[n]);
}

例题3:Cut the Sequence

题意

给定一个长度为 \(N\) 的序列 \(a\),要求把该序列分成若干段,在满足“每段中所有数的和”不超过 \(M\) 的前提下,让“每段中所有数的最大值”之和最小。

试计算这个最小值。

\(N\le 10^5,M\le 10^{11},1\le a_i\le 10^6\)

解法

\(dp_i\) 表示对 \(a_1\sim a_i\) 进行划分,则转移有 \(dp_i=\min_{pre_j\ge pre_i-M}(dp_j+\max_{k=j+1}^ia_k)\),其中 \(pre_i=\sum_{j=1}^ia_j,pre_0=0\)。考虑由 \(\max_{k=j+1}^ia_k\) 递推到 \(\max_{k=j+1}^{i+1}a_k\)

考虑将 \(dp_j+\max_{k=j+1}^ia_k\) 拆开,判断两个决策之间的优劣。下面只比较 \(\boldsymbol j\) \(\boldsymbol{j-1}\) 决策而非其他决策,其中 \(\boldsymbol{j<i,pre_{j-1}\ge pre_i-M}\)

首先显然有 \(dp\) 单调不降,故而有 \(dp_{j-1}\le dp_j\)。如果 \(\max_{k=j}^ia_k\le \max_{k=j+1}^ia_k\)(也就是 \(\max(\max_{k=j+1}^ia_k,a_j)=\max_{k=j+1}^ia_k\),即为 \(\max_{k=j+1}^ia_k\ge a_j\)),则 \(dp_{j-1}+\max_{k=j}^ia_k\le dp_j+\max_{k=j+1}^ia_k\),则 \(j-1\) 一定优于 \(j\)。故而在 每次查找 \(i\) 的可能最优决策前,先把 \(\forall j(\max_{k=j+1}^ia_k\ge a_j,pre_{j-1}\ge pre_i-M)\)\(j\) 移出单调队列。如果之前已经将 \(\forall j(\max_{k=j+1}^{i-1}a_k\ge a_j,pre_{j-1}\ge pre_{i-1}-M)\)\(j\) 移出队列,则这次需要把 \(\forall j(a_i\ge a_j,pre_{j-1}\ge pre_i-M)\)\(j\) 移出队列。

综上,可以维护一个 \(a_j\) 递减的单调队列 \(q\),在将 \(i\) 入队前移出所有非法 \(j\),在队列中保存可能的最优解,再将 \(i\) 入队。同时,\(\forall j,q_j\ne i,\max_{k=q_j+1}^ia_k\) 即为 \(a_{q_{j+1}}\)。(可以用反证法证明此结论。)

至于找出 \(\min_{pre_j\ge pre_i-M}(dp_j+\max_{k=j+1}^ia_k)\)(即 \(\min(dp_{q_j}+a_{q_{j+1}})\)),可以同时维护一个 std::multiset,以 \(dp_{q_j}+a_{q_{j+1}}\) 为键值,维护队列中所有元素对应的 \(\min(dp_j+\max_{k=j+1}^ia_k)\)。(同时可以使用优先队列 + 懒惰删除)

注意若令 \(\boldsymbol{c_i{\;=\;}}\bold{argmax}\boldsymbol{_{j=0}^ipre_i-pre_j\le M}\),则 \(\boldsymbol{dp_i}\) 初值为 \(\boldsymbol{dp_{c_i}+a_{q_0}}\)(因为 \(\boldsymbol{c_i}\) 始终为一个合法且可能的最优决策,处理队列为空的情况,此时显然 \(\boldsymbol{a_{q_0}=\;}\bold{max}\boldsymbol{_{k=c_i+1}^ia_k}\),如此则需要在将 \(\boldsymbol i\) 入队前将 \(\boldsymbol{c_i}\) 移出队列)。

代码

点此查看代码
#include <bits/stdc++.h>
using namespace std;
const int maxn=100010;
int n,i,l,r,u;
int a[maxn],q[maxn];
long long m,dp[maxn],pre[maxn];
multiset<long long> s;
int main(){
    scanf("%d%lld",&n,&m);
    l=1;s.insert(11451419198101926LL);
    for(i=1;i<=n;++i){
        scanf("%d",a+i);
        pre[i]=pre[i-1]+a[i];
        if(a[i]>m){
            printf("-1");
            return 0;
        }
        while(pre[i]-pre[u]>m) ++u;
        while(l<=r&&q[l]<=u){
            if(l<r) s.erase(s.find(dp[q[l]]+a[q[l+1]]));
            ++l;
        }
        while(l<=r&&a[i]>=a[q[r]]){
            if(l<r) s.erase(s.find(dp[q[r-1]]+a[q[r]]));
            --r;
        }
        if(l<=r) s.insert(dp[q[r]]+a[i]);
        q[++r]=i;dp[i]=min(*s.begin(),dp[u]+a[q[l]]);
    }
    printf("%lld",dp[n]);
}

例题4:多重背包

题意

有一个容积为 \(W\) 的背包,同时有 \(n\) 种物品,第 \(i\) 种物品有 \(c_i\) 个,每个的体积为 \(v_i\),价值为 \(w_i\)。求可以得到的最大价值。\(1\le n\le 100,n\le \sum c_i\le 10^5,0\le W\le 4\cdot 10^4,\)

解法

这个题似乎没有人卡二进制优化,反正二进制优化就比单调队列优化慢了一倍 可能是常数的原因

考虑把每一种物品分开考虑。

\(dp_{i,j}\) 为考虑完第 \(1\sim i\) 种物品后,体积之和为 \(j\) 的物品价值之和最多是多少。

转移方程如下:

\[dp_{i,j}=\max_{k=0}^{c_i}(dp_{i-1,j-k\cdot v_i}+k\cdot w_i) \]

此时如果令 \(p=j\text{ mod }v\),只考虑 \(\{p+0v,p+1v,p+2v,\cdots\}\),则这个方程中的 \([j-c_iv_i,j]\) 可以看成是上述集合内的一段连续段。此时对于 \(k\cdot w_i\),可以把原式看成是

\[dp_{i,j+kv_i}=\max_{p=k-c_i}^k(dp_{i-1,j+pv_i}-pw_i)+kw_i \]

其中 \(j\in[0,v_i)\)

代码

点此查看代码
//二进制
#include <bits/stdc++.h>
using namespace std;
int C[10010],V[10010];
int dp[40010],mx;
int n,w,c,v,m,tmp,j=0,top;
int main(){
    scanf("%d%d",&n,&w);
    for(int i=0;i<n;++i){
        scanf("%d%d%d",&c,&v,&m);
        while(m>=(1<<j)){
            C[top]=c*(1<<j);
            V[top++]=v*(1<<j);
            m-=(1<<(j++));
        }
        if(m!=(j=0)){
            C[top]=c*m;
            V[top++]=v*m;
        }
    }
    for(int i=0;i<top;++i){
        for(int j=w;j>=V[i];--j){
            tmp=dp[j-V[i]]+C[i];
            if(tmp>dp[j]) dp[j]=tmp;
            if(mx<dp[j]) mx=dp[j];
        }
    }
    printf("%d",mx);
    return 0;
}
点此查看代码
//单调队列
#include <bits/stdc++.h>
using namespace std;
const int maxn=40010;
int n,v,w,c,l,r,i,j,k,a,b;
int q[maxn],dp[2][maxn],val[maxn];
int *N=dp[0],*M=dp[1];
int main(){
    scanf("%d%d",&n,&a);
    while(n--){
        scanf("%d%d%d",&w,&v,&c);
        for(i=0;i<v;++i){
            l=1;r=0;k=0;b=i-v*c;
            for(j=i;j<=a;j+=v) val[j]=N[j]-(k++)*w;
            k=0;
            for(j=i;j<=a;j+=v){
                if(q[l]<b) ++l;
                while(l<=r&&val[q[r]]<=val[j]) --r;
                q[++r]=j;M[j]=val[q[l]]+w*(k++);b+=v;
            }
        } 
        swap(N,M);
    }
    b=0;
    for(j=0;j<=a;++j) b=max(b,N[j]);
    printf("%d",b);
} 

引入:斜率(请自学初中数学)

引入:函数的凹凸性

定义一个定义在 \(D\) 上的函数 \(f\) 是凸函数当且仅当其二阶差分恒非负(形式化即为:\(\forall i,j\ (i-j,i,i+j\in D),f(i)-f(i-j)\ge f(i+j)-f(i)\)),其是凹函数当且仅当其二阶差分恒非正(形式化即为:\(\forall i,j\ (i-j,i,i+j\in D),f(i)-f(i-j)\le f(i+j)-f(i)\))。

斜率优化 dp

适用条件

形如 \(b(i)=\min_{j\in[L_i,R_i]}(y(j)-k(i)x(j))\) 的式子(其中条件和单调队列优化 dp 相似)。

做法

可以把 \((x(j),y(j))\) 看作一个点,把 \([L_i,R_i]\) 的这些点作为一个点集,这样问题即转化成了:对每一个这样的点作一条斜率恒为 \(k(i)\) 的直线,这些直线在纵轴上的截距的最小值是多少。

对于任意两点 \((x(a),y(a)),(x(b),y(b))\),此时若有 \(x(a)\le x(b)\),且令 \(K(a,b)=\frac{y(b)-y(a)}{x(b)-x(a)}\)(即为两点之间的斜率),则 \(a\) 优于 \(b\) 当且仅当 \(k(i)<K(a,b)\)

证明:若 \(y(a)-k(i)x(a)<y(b)-k(i)x(b)\),则 \(y(a)-y(b)<k(i)(x(a)-x(b))\),而 \(x(a)-x(b)<0\),故而 \(k(i)<\frac{y(a)-y(b)}{x(a)-x(b)}\)

同时,若三个点 \(a,b,c\) 形成了一个非下凸曲线,也就是 \(K(a,b)\ge K(b,c)\),则若 \(b\) 同时优于 \(a,c\) ,则必有 \(k(i)<K(a,b)\)\(k(i)>K(b,c)\),而这两者一定不会同时成立,可以直接在决策集合内删去 \(b\)

故而最后可能形成最优解的点集一定形成一个下凸壳的形状,对应在单调队列中必有相邻两点的斜率单调递减。同时找到一个最优点等效于用一条斜率为 \(k(i)\) 的直线截这个下凸壳,对应在单调队列中找出一个点 \(j\) 满足 \(\forall a<j,k(i)\le K(a,j),\forall b>j,k(i)>K(j,b)\)。可以直接在队列上二分,找到对应的点。

\(k\) 函数单调不增,则可以直接删除斜率过小的点,可以去掉一个 \(\log\);若 \(x\) 函数单调,则可以直接顺次加点;否则需要在任意位置加入点,需要用到平衡树维护凸壳。

例题1:玩具装箱

题意

\(n\) 个玩具,第 \(i\) 个玩具价值为 \(c_i\)。要求将这 \(n\) 个玩具排成一排,分成若干段。对于一段 \([l,r]\),它的代价为 \((r-l+\sum_{i=l}^r c_i-L)^2\)。其中 \(L\) 是一个常量,求分段的最小代价。\(1\le n\le 5\times 10^4, 1\le L, c_i\le 10^7\)

解法

\(dp_i\) 为考虑前 \(i\) 个物品的最小代价,则转移方程有

\[\begin{aligned}dp_i&=\min_{j=1}^{i-1}(dp_{j}+(i-j-1+\sum_{k=j+1}^ic_k-L)^2)\\&=\min_{j=1}^{i-1}(dp_j+(i-j-1+pre_i-pre_j-L)^2)\\&=\min_{j=1}^{i-1}(dp_j+(i+pre_i-L-1)^2-2(i+pre_i-L-1)(j+pre_j)+(j+pre_j)^2)\end{aligned} \]

其中 \(pre_i=\sum_{j=1}^ic_j\)

此时令 \(dp_j+(j+pre_j)^2\)\(y(j)\)\(dp_i-(i+pre_i-L-1)^2\)\(b(i)\)\(2(i+pre_i-L-1)\)\(k(i)\)\(j+pre_j\)\(x(j)\),则方程转为 \(b(i)=\min_{j=1}^{i-1}(y(j)-k(i)x(j))\)。可以直接使用上述方式即可。

细节见代码。

代码

点此查看代码
#include <bits/stdc++.h>
using namespace std;
const int maxn=50010;
int n,L,l,r,c,i,j;
int q[maxn];
long long k,dp[maxn],pre[maxn],xt[maxn],yt[maxn];
long long fz[maxn],fm[maxn];
__int128_t t1,t2;
int main(){
    scanf("%d%d%d",&n,&L,&c);
    dp[1]=1LL*(c-L)*(c-L);
    fm[0]=xt[1]=1+c;
    fz[0]=yt[1]=dp[1]+xt[1]*xt[1];
    q[1]=r=1;pre[1]=c;//此处 0 点需要入队,因为这个点可能是其他点转移的最优决策
    for(i=2;i<=n;++i){
        scanf("%d",&c);
        pre[i]=pre[i-1]+c;
        xt[i]=i+pre[i];
        k=(xt[i]-L-1)*2;
        while(l<r){
            t1=(__int128_t)fm[q[l]]*k;
            if(t1>fz[q[l]]) ++l;
            else break;
        }
        j=q[l];
        dp[i]=yt[j]-k*xt[j]+((k>>1)*(k>>1));
        yt[i]=dp[i]+xt[i]*xt[i];
        while(l<r){
            t1=(yt[q[r]]-yt[q[r-1]])*(xt[i]-xt[q[r]]);
            t2=(xt[q[r]]-xt[q[r-1]])*(yt[i]-yt[q[r]]);
            if(t1>=t2) --r;
            else break;
        }
        fz[q[r]]=yt[i]-yt[q[r]];
        fm[q[r]]=xt[i]-xt[q[r]];
        fz[i]=11451419198101926;
        q[++r]=i;
    }
    printf("%lld",dp[n]);
    return 0;
}

例题2:玩具装箱 · 改

题意

\(n\) 个玩具,第 \(i\) 个玩具价值为 \(c_i\)。要求将这 \(n\) 个玩具排成一排,分成若干段。对于一段 \([l,r]\),它的代价为 \((r-l+\sum_{i=l}^r c_i-L)^2\)。其中 \(L\) 是一个常量,求分段的最小代价。\(1\le n\le 5\times 10^4;1\le L\le 10^5;-10^5\le c_i\le 10^5\)

### 解法 1

转移方程同上。但是新加入的决策的对应坐标横坐标不单调。

考虑将现有的凸壳按照新的 \(x_i\) 拆开成两部分。然后如果对于新的点,存在两个原凸壳上的点与之形成了一个上凸形,则这个点应该剔除,否则在两部分凸壳两侧删除掉若干个成为上凸点的点即可。不用判断横坐标是否相同的情况。 可以用平衡树维护。

p.s. 用 std::set 也可以实现这样的操作。

解法 1 代码

点此查看代码
#include <bits/stdc++.h>
using namespace std;
const int maxn=100010;
const long long inf=1145141919810192608;
mt19937 Rand(time(0));
int n,l,i,j,k,c,xp,yp,px,pt,pr;
int tot,root;
int pre[maxn];
long long dp[maxn],xt[maxn],yt[maxn];
struct node{
    int idx,ls,rs;
    unsigned key;
    long long fz,fm;
}tr[maxn];
#define idx(p) tr[p].idx
#define ls(p) tr[p].ls
#define rs(p) tr[p].rs
#define key(p) tr[p].key
#define fz(p) tr[p].fz
#define fm(p) tr[p].fm
#define xt(p) xt[idx(p)]
#define yt(p) yt[idx(p)]
void vSplit(const int p,int &x,int &y){
    if(!p){
        x=y=0;
        return;
    }
    if(xt[idx(p)]<xt[i]){
        x=p;
        vSplit(rs(p),rs(p),y);
    }
    else{
        y=p;
        vSplit(ls(p),x,ls(p));
    }
}
int Merge(const int x,const int y){
    if(!(x&&y)) return x|y;
    if(key(x)<key(y)){
        rs(x)=Merge(rs(x),y);
        return x;
    }
    else{
        ls(y)=Merge(x,ls(y));
        return y;
    }
}
int main(){
    scanf("%d%d",&n,&l);
    root=tot=1;
    key(1)=Rand();
    fz(1)=inf;
    for(i=1;i<=n;++i){
        fz(i+1)=inf;
        scanf("%d",&c);
        pre[i]=pre[i-1]+c;
        xt[i]=i+pre[i];
        k=(xt[i]-l-1)<<1;
        pt=root;j=-1;
        for(;;){
            if((fm(pt)*k>fz(pt)&&fm(pt)>=0)||
               (fm(pt)*k<fz(pt)&&fm(pt)<0)){
                if(!rs(pt)) break;
                pt=rs(pt);
            }
            else{
                j=pt;
                if(!ls(pt)) break;
                pt=ls(pt);
            }
        }
        if(j<0) j=pt;
        dp[i]=yt(j)-k*xt(j)+(1LL*(k>>1)*(k>>1));
        yt[i]=dp[i]+xt[i]*xt[i];
        vSplit(root,xp,yp);
        pr=0;pt=yp;px=xp;
        while(ls(pt)) pr=pt,pt=ls(pt);
        while(rs(px)) px=rs(px);
        if((pt&&px)&&((yt[i]-yt(px))*(xt(pt)-xt[i])>=
                      (yt(pt)-yt[i])*(xt[i]-xt(px)))) goto end;
        key(++tot)=Rand();
        idx(tot)=i;
        while(yp){
            pt=yp;pr=0;
            while(ls(pt)) pr=pt,pt=ls(pt);
            if(!(rs(pt)||pr)) break;
            if(((fz(pt)*(xt(pt)-xt[i])<=
                 fm(pt)*(yt(pt)-yt[i]))&&
                (fm(pt)>0))||
               ((fz(pt)*(xt(pt)-xt[i])>=
                 fm(pt)*(yt(pt)-yt[i]))&&
                (fm(pt)<0))){
                if(pr) ls(pr)=rs(pt);
                else{
                    yp=rs(yp);
                    rs(pt)=0;
                }
            }
            else break;
        }
        if(yp){
            fz(tot)=yt(pt)-yt[i];
            fm(tot)=xt(pt)-xt[i];
            yp=Merge(tot,yp);
        }
        else yp=tot;
        pt=0;
        while(xp){
            pt=xp;pr=0;
            while(rs(pt)) pr=pt,pt=rs(pt);
            if(ls(pt)){
                px=ls(pt);
                while(rs(px)) px=rs(px);
            }
            else px=pr;
            if(!px) break;
            if(((fz(px)*(xt[i]-xt(pt))>=
                 fm(px)*(yt[i]-yt(pt)))&&
                (fm(px)>0))||
               ((fz(px)*(xt[i]-xt(pt))<=
                 fm(px)*(yt[i]-yt(pt)))&&
                (fm(px)<0))){
                if(pr) rs(pr)=ls(pt);
                else{
                    xp=ls(xp);
                    ls(pt)=0;
                }
            }
            else break;
        }
        if(pt){
            fz(pt)=yt[i]-yt(pt);
            fm(pt)=xt[i]-xt(pt);
        }
        end:root=Merge(xp,yp);
    }
    printf("%lld",dp[n]);
} 

解法 2

把上面的转移方程 \(b(i)=\min_{j=1}^{i-1}(y(j)-k(i)x(j))\) 换成 \(y(i)=\min_{j=1}^{i-1}(b(j)+x(i)k(j))\) 的形式,则问题等效于每次插入一条直线并且求所有直线在某个 \(x\) 值处的最值。这个问题可以使用 李超线段树 解决,并且码长、常数等方面显然更优。很多类似内容均可以使用这两种方式解决。

P4655 [CEOI2017] Building Bridges 解法 2 代码

点此查看代码
#include <bits/stdc++.h>
using namespace std;
const int maxn=100010;
const int INF=1000000;
const int maxx=1000010;
#define ll long long
int n,i;
ll dp,w[maxn],h[maxn]; 
struct line{
    int k; ll b; bool f;
    inline ll val(int x){return (1LL*x*k)+b;}
}tr[maxx<<2];
#define ls(p) p<<1
#define rs(p) p<<1|1
void Insert(int p,int l,int r,line c){
    if(!tr[p].f){
        tr[p]=c;
        return;
    }
    int m=(l+r)>>1;
    if(c.val(m)<tr[p].val(m)) swap(c,tr[p]);
    if(l==r) return;
    if(c.val(l)<tr[p].val(l)) Insert(ls(p),l,m,c);
    else if(c.val(r)<tr[p].val(r)) Insert(rs(p),m+1,r,c);
}
void Query(int p,int l,int r,int x){
    if(!tr[p].f) return;
    dp=min(dp,tr[p].val(x));
    if(l==r) return; int m=(l+r)>>1;
    if(x<=m) Query(ls(p),l,m,x);
    else Query(rs(p),m+1,r,x);
}
int main(){
    scanf("%d",&n);
    for(i=1;i<=n;++i) scanf("%lld",h+i);
    scanf("%lld",w+1);
    Insert(1,0,INF,{(int)(-2*h[1]),h[1]*h[1]-w[1],1});
    for(i=2;i<=n;++i){
        scanf("%lld",w+i); w[i]+=w[i-1];
        dp=1e18; Query(1,0,INF,h[i]); dp+=w[i-1]+h[i]*h[i];
        Insert(1,0,INF,{(int)(-2*h[i]),dp+h[i]*h[i]-w[i],1});
    }
    printf("%lld",dp);
    return 0;
}
posted @ 2022-10-04 15:58  Fran-Cen  阅读(79)  评论(0编辑  收藏  举报