单调队列和斜率优化 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

适用条件

形如 dpi=u(i)+minj[li,ri]v(j) (其中若 v(j)dpj 有关,则有 ri<i)。

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

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

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

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

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

例题1:CF372C Watching Fireworks is Fun

题意

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

节日中有 m 个烟花要放,给定放的地点 ai,时间 ti,如果你当时在区域 x,那么你可以获得 bi|aix| 的开心值。

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

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

n1.5×105,m300

解法

dpi,j 为在烟花 i 燃放时处于位置 j 的最大快乐值。

考虑 dpi,j 能够从哪些 dpi1,k 值转移而来。显然需要 |ki|(titi1)d。故而转移有 dpi,j=bi|aij|+maxk[i(titi1)d,i+(titi1)d]dpi1,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 个木匠要么不粉刷,要么粉刷包含木板 Si 的,长度不超过 Li 的连续的一段木板,每粉刷一块可以得到 Pi 的报酬。

不同工匠的 Si 不同。

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

N16000,M100

解法

考虑维护 dpi,j 表示考虑第 i 位工匠,和第 1j 块木板时的最大收益。

考虑 dpi,j 可以从哪些 dp 值转移而来。

  • i 位工匠可以不粉刷木板,此时 dpi,j=max(dpi,j,dpi1,j)
  • j 块木板可以不粉刷,此时 dpi,j=max(dpi,j,dpi,j1)
  • i 位工匠粉刷包括第 Sij 的木板,此时 dpi,j=max(dpi,j,maxk[jLi,Si1](dpi1,k+(jk)Pi))=max(dpi,j,maxk[jLi,Si1](dpi1,kkPi)+jPi)

此时可以直接预处理 [SiLi,Si1] 的决策集合,对于每一个 dpi,j(j[Si,Si+Li1]) 作转移即可。

注意需要对所有工匠按 Si 升序排序,可能有某位工匠在中间新粉刷木板的情况。

代码

点此查看代码
#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 的前提下,让“每段中所有数的最大值”之和最小。

试计算这个最小值。

N105,M1011,1ai106

解法

dpi 表示对 a1ai 进行划分,则转移有 dpi=minprejpreiM(dpj+maxk=j+1iak),其中 prei=j=1iaj,pre0=0。考虑由 maxk=j+1iak 递推到 maxk=j+1i+1ak

考虑将 dpj+maxk=j+1iak 拆开,判断两个决策之间的优劣。下面只比较 j j1 决策而非其他决策,其中 j<i,prej1preiM

首先显然有 dp 单调不降,故而有 dpj1dpj。如果 maxk=jiakmaxk=j+1iak(也就是 max(maxk=j+1iak,aj)=maxk=j+1iak,即为 maxk=j+1iakaj),则 dpj1+maxk=jiakdpj+maxk=j+1iak,则 j1 一定优于 j。故而在 每次查找 i 的可能最优决策前,先把 j(maxk=j+1iakaj,prej1preiM)j 移出单调队列。如果之前已经将 j(maxk=j+1i1akaj,prej1prei1M)j 移出队列,则这次需要把 j(aiaj,prej1preiM)j 移出队列。

综上,可以维护一个 aj 递减的单调队列 q,在将 i 入队前移出所有非法 j,在队列中保存可能的最优解,再将 i 入队。同时,j,qji,maxk=qj+1iak 即为 aqj+1。(可以用反证法证明此结论。)

至于找出 minprejpreiM(dpj+maxk=j+1iak)(即 min(dpqj+aqj+1)),可以同时维护一个 std::multiset,以 dpqj+aqj+1 为键值,维护队列中所有元素对应的 min(dpj+maxk=j+1iak)。(同时可以使用优先队列 + 懒惰删除)

注意若令 ci=argmaxj=0ipreiprejM,则 dpi 初值为 dpci+aq0(因为 ci 始终为一个合法且可能的最优决策,处理队列为空的情况,此时显然 aq0=maxk=ci+1iak,如此则需要在将 i 入队前将 ci 移出队列)。

代码

点此查看代码
#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 种物品有 ci 个,每个的体积为 vi,价值为 wi。求可以得到的最大价值。1n100,nci105,0W4104,

解法

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

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

dpi,j 为考虑完第 1i 种物品后,体积之和为 j 的物品价值之和最多是多少。

转移方程如下:

dpi,j=maxk=0ci(dpi1,jkvi+kwi)

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

dpi,j+kvi=maxp=kcik(dpi1,j+pvipwi)+kwi

其中 j[0,vi)

代码

点此查看代码
//二进制
#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 是凸函数当且仅当其二阶差分恒非负(形式化即为:i,j (ij,i,i+jD),f(i)f(ij)f(i+j)f(i)),其是凹函数当且仅当其二阶差分恒非正(形式化即为:i,j (ij,i,i+jD),f(i)f(ij)f(i+j)f(i))。

斜率优化 dp

适用条件

形如 b(i)=minj[Li,Ri](y(j)k(i)x(j)) 的式子(其中条件和单调队列优化 dp 相似)。

做法

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

对于任意两点 (x(a),y(a)),(x(b),y(b)),此时若有 x(a)x(b),且令 K(a,b)=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)<y(a)y(b)x(a)x(b)

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

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

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

例题1:玩具装箱

题意

n 个玩具,第 i 个玩具价值为 ci。要求将这 n 个玩具排成一排,分成若干段。对于一段 [l,r],它的代价为 (rl+i=lrciL)2。其中 L 是一个常量,求分段的最小代价。1n5×104,1L,ci107

解法

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

dpi=minj=1i1(dpj+(ij1+k=j+1ickL)2)=minj=1i1(dpj+(ij1+preiprejL)2)=minj=1i1(dpj+(i+preiL1)22(i+preiL1)(j+prej)+(j+prej)2)

其中 prei=j=1icj

此时令 dpj+(j+prej)2y(j)dpi(i+preiL1)2b(i)2(i+preiL1)k(i)j+prejx(j),则方程转为 b(i)=minj=1i1(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 个玩具价值为 ci。要求将这 n 个玩具排成一排,分成若干段。对于一段 [l,r],它的代价为 (rl+i=lrciL)2。其中 L 是一个常量,求分段的最小代价。1n5×104;1L105;105ci105

### 解法 1

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

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

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)=minj=1i1(y(j)k(i)x(j)) 换成 y(i)=minj=1i1(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 @   Fran-Cen  阅读(80)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 没有源码,如何修改代码逻辑?
· PowerShell开发游戏 · 打蜜蜂
· 在鹅厂做java开发是什么体验
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战
点击右上角即可分享
微信分享提示