单调队列和斜率优化 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\) 的物品价值之和最多是多少。
转移方程如下:
此时如果令 \(p=j\text{ mod }v\),只考虑 \(\{p+0v,p+1v,p+2v,\cdots\}\),则这个方程中的 \([j-c_iv_i,j]\) 可以看成是上述集合内的一段连续段。此时对于 \(k\cdot w_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\) 个物品的最小代价,则转移方程有
其中 \(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\)。
转移方程同上。但是新加入的决策的对应坐标横坐标不单调。
考虑将现有的凸壳按照新的 \(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;
}
本文来自博客园,作者:Fran-Cen,转载请注明原文链接:https://www.cnblogs.com/Fran-CENSORED-Cwoi/p/16753906.html