斜率优化(李超线段树)
虽然我现在还不怎么理解
但是
这个和这个
都相当于是斜率优化的
板子
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=1e5+10,M=1e9;
const ll INF=1e18;
int read(){
int x;scanf("%d",&x);return x;
}
int n,cnt,rt;
ll h[N],w[N],sum[N],f[N];
struct{
ll k,b;
}s[N];
struct{
int lc,rc,c;
}t[N*20];
ll calc(int id,ll x){
return s[id].k*x+s[id].b;
}
void update(int &p,ll l,ll r,int id){
if(!p) p=++cnt;
if(!t[p].c) return t[p].c=id,void();
ll mid=(l+r)>>1;
if(calc(id,mid)<calc(t[p].c,mid)) swap(id,t[p].c);
if(calc(id,l)<calc(t[p].c,l)) update(t[p].lc,l,mid,id);
else if(calc(id,r)<calc(t[p].c,r)) update(t[p].rc,mid+1,r,id);
}
ll ask(int p,ll l,ll r,ll x){
if(!p) return INF;
ll res=calc(t[p].c,x);
if(l==r) return res;
ll mid=(l+r)>>1;
if(x<=mid) return min(res,ask(t[p].lc,l,mid,x));
return min(res,ask(t[p].rc,mid+1,r,x));
}
int main(){
n=read();
for(int i=1;i<=n;i++) h[i]=read();
for(int i=1;i<=n;i++){
w[i]=read();
sum[i]=sum[i-1]+w[i];
}
s[1]={-2*h[1],f[1]+h[1]*h[1]-sum[1]};
update(rt,-M,M,1);
for(int i=2;i<=n;i++){
f[i]=ask(rt,-M,M,h[i])+h[i]*h[i]+sum[i-1];
s[i]={-2*h[i],f[i]+h[i]*h[i]-sum[i]};
update(rt,-M,M,i);
}
printf("%lld",f[n]);
return 0;
}
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=2e5+10,M=1e9;
ll read(){
ll x;scanf("%lld",&x);return x;
}
struct{
ll k,b;
}s[N];
ll h[N],f[N];
struct{
int lc,rc,v;
}t[N];
ll calc(int id,ll x){
return s[id].k*x+s[id].b;
}
int cnt,rt;
void update(int &p,ll l,ll r,int id){
if(!p) p=++cnt;
if(!t[p].v) return t[p].v=id,void();
int mid=l+r>>1;
if(calc(id,mid)<calc(t[p].v,mid)) swap(id,t[p].v);
if(calc(id,l)<calc(t[p].v,l)) update(t[p].lc,l,mid,id);
else if(calc(id,r)<calc(t[p].v,r)) update(t[p].rc,mid+1,r,id);
}
ll ask(int p,ll l,ll r,ll x){
if(!p) return 1e18;
ll ans=calc(t[p].v,x);
if(l==r) return ans;
ll mid=l+r>>1;
if(x<=mid) return min(ans,ask(t[p].lc,l,mid,x));
return min(ans,ask(t[p].rc,mid+1,r,x));
}
int main(){
int n=read();
ll c=read();
for(int i=1;i<=n;i++) h[i]=read();
s[1]={-2*h[1],h[1]*h[1]+f[1]};
update(rt,-M,M,1);
for(int i=2;i<=n;i++){
f[i]=ask(rt,-M,M,h[i])+h[i]*h[i]+c;
s[i]={-2*h[i],h[i]*h[i]+f[i]};
update(rt,-M,M,i);
}
printf("%lld",f[n]);
return 0;
}
代码也是基本一样,第一篇是自己推+抄了李超,第二篇用的第一篇模板,然后自己改了改dp转移
(2025/10/9)遇到了一道斜率优化题
首先有一个朴素的\(n^4\)的做法
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=2010;
const ll INF=1e18;
int read(){
int x;scanf("%d",&x);return x;
}
ll r[N],f[10][N];
ll calc(int l,int r){
ll res=0;
for(int i=l;i<=r;i++)
res+=::r[i]*(i-l);
return res;
}
ll calc2(int l,int r,int p){
ll res=0;
for(int i=l;i<=r;i++)
res+=::r[i]*(i-p);
return res;
}
int main(){
int n=read(),k=read();
for(int i=1;i<=n;i++) r[i]=read(),r[i+n]=r[i];
ll ans=INF;
for(int t=1;t<=n;t++){//断环成链
for(int i=0;i<=k;i++)
for(int j=t;j<=t+n-1;j++)
f[i][j]=INF;
for(int j=t;j<=t+n-1;j++) f[1][j]=calc(t,j);
for(int i=2;i<=k;i++){
for(int j=t+i-1;j<=t+n-1;j++){
//f[i][j]表示在j位置开了第i扇门的最小花费
for(int p=t+i-2;p<j;p++){
f[i][j]=min(f[i][j],f[i-1][p]+calc(p+1,j));
}
}
}
for(int j=t+k-1;j<=t+n-1;j++){
if(f[k][j]<INF){
ll res=f[k][j];
if(j<t+n-1) res+=calc2(j+1,t+n-1,t);
ans=min(ans,res);
}
}
}
// ll ans=INF;
// for(int i=k;i<2*n;i++) ans=min(ans,f[k][i]);
printf("%lld",ans);
return 0;
}
这个可以拿下30分的高分,并且可以通过这道题的弱化版
这时候我们注意到calc里的内容可以进行前缀和优化,那么我们就可以优化掉一个n,即有了\(n^3\)的做法
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=2010;
const ll INF=1e18;
int read(){
int x;scanf("%d",&x);return x;
}
ll r[N],f[10][N];
ll sum1[N],sum2[N];
ll calc(int l,int r){
// ll res=0;
// for(int i=l;i<=r;i++)
// res+=::r[i]*(i-l);//res+=r[i]*i-r[i]*l=sum2[i]-sum1[i]*l;
// return res;
return (sum2[r]-sum2[l-1])-l*(sum1[r]-sum1[l-1]);
}
ll calc2(int l,int r,int p){
// ll res=0;
// for(int i=l;i<=r;i++)
// res+=::r[i]*(i-p);
// return res;
return (sum2[r]-sum2[l-1])-p*(sum1[r]-sum1[l-1]);
}
int main(){
int n=read(),k=read();
for(int i=1;i<=n;i++) r[i]=read(),r[i+n]=r[i];
for(int i=1;i<=2*n;i++){
sum1[i]=sum1[i-1]+r[i];
sum2[i]=sum2[i-1]+i*r[i];
}
ll ans=INF;
for(int t=1;t<=n;t++){//断环成链
for(int i=0;i<=k;i++)
for(int j=t;j<=t+n-1;j++)
f[i][j]=INF;
for(int j=t;j<=t+n-1;j++) f[1][j]=calc(t,j);
for(int i=2;i<=k;i++){
for(int j=t+i-1;j<=t+n-1;j++){
//f[i][j]表示在j位置开了第i扇门的最小花费
for(int p=t+i-2;p<j;p++){
f[i][j]=min(f[i][j],f[i-1][p]+calc(p+1,j));
}
}
}
for(int j=t+k-1;j<=t+n-1;j++){
if(f[k][j]<INF){
ll res=f[k][j];
if(j<t+n-1) res+=calc2(j+1,t+n-1,t);
ans=min(ans,res);
}
}
}
// ll ans=INF;
// for(int i=k;i<2*n;i++) ans=min(ans,f[k][i]);
printf("%lld",ans);
return 0;
}
这时候就有了60分的高分
那么,我们还能怎样优化呢?
注意到,我们可以把转移式展开!
我们有转移方程:
\[f[i][j] = \min_{p} \{ f[i-1][p] + \text{calc}(p+1, j) \}
\]
其中:
\[\text{calc}(L, R) = \sum_{x=L}^R (x-L) \cdot r[x] = \sum_{x=L}^R x\cdot r[x] - L \cdot \sum_{x=L}^R r[x]
\]
把 \(\text{calc}(p+1, j)\) 展开:
\[\text{calc}(p+1, j) = \text{sum2}[j] - \text{sum2}[p] - (p+1) \cdot [\text{sum1}[j] - \text{sum1}[p]]
\]
代入转移方程:
\[f[i][j] = \min_p \{ f[i-1][p] + \text{sum2}[j] - \text{sum2}[p] - (p+1)\cdot\text{sum1}[j] + (p+1)\cdot\text{sum1}[p] \}
\]
提取与 \(p\) 无关的项:
\[f[i][j] = \text{sum2}[j] + \min_p \{ f[i-1][p] - \text{sum2}[p] + (p+1)\cdot\text{sum1}[p] - (p+1)\cdot\text{sum1}[j] \}
\]
设:
\[B_p = f[i-1][p] - \text{sum2}[p] + (p+1)\cdot\text{sum1}[p]
\]
\[K_p = -(p+1)
\]
\[X_j = \text{sum1}[j]
\]
则:
\[f[i][j] = \text{sum2}[j] + \min_p \{ B_p + K_p \cdot X_j \}
\]
这就是标准的斜率优化形式:\(\min\{K_p \cdot X + B_p\}\)。
此时,只需翻出来上面的板子,进行一一对应即可通过本题
//T了五六发,感觉被资本做局了
//结果是M开的太大了小一点直接快了1倍
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=2010;
const ll INF=1e18;
int read(){
int x;scanf("%d",&x);return x;
}
struct{
ll k,b;
}s[N];
struct{
int l,r,c;
}t[N*20];
ll calc(int id,ll x){
return s[id].k*x+s[id].b;
}
int cnt,rt;
ll M;
void update(int &p,ll l,ll r,int id){
if(!p) p=++cnt;
if(!t[p].c) return t[p].c=id,void();
ll mid=(l+r)>>1;
if(calc(id,mid)<calc(t[p].c,mid)) swap(id,t[p].c);
if(calc(id,l)<calc(t[p].c,l)) update(t[p].l,l,mid,id);
else if(calc(id,r)<calc(t[p].c,r)) update(t[p].r,mid+1,r,id);
}
ll ask(int p,ll l,ll r,ll x){
if(!p) return LONG_MAX;
ll ans=calc(t[p].c,x);
if(l==r) return ans;
ll mid=l+r>>1;
if(x<=mid) return min(ans,ask(t[p].l,l,mid,x));
return min(ans,ask(t[p].r,mid+1,r,x));
}
void clear(){
for(int i=1;i<=cnt;i++)
t[i].l=t[i].r=t[i].c=0;
cnt=rt=0;
}
ll r[N],f[10][N];
ll sum1[N],sum2[N];
//ll calc(int l,int r){
//// ll res=0;
//// for(int i=l;i<=r;i++)
//// res+=::r[i]*(i-l);//res+=r[i]*i-r[i]*l=sum2[i]-sum1[i]*l;
//// return res;
// return (sum2[r]-sum2[l-1])-l*(sum1[r]-sum1[l-1]);
//}
ll calc2(int l,int r,int p){
// ll res=0;
// for(int i=l;i<=r;i++)
// res+=::r[i]*(i-p);
// return res;
return (sum2[r]-sum2[l-1])-p*(sum1[r]-sum1[l-1]);
}
int main(){
int n=read(),k=read();
for(int i=1;i<=n;i++) r[i]=read(),r[i+n]=r[i];
for(int i=1;i<=2*n;i++){
sum1[i]=sum1[i-1]+r[i];
sum2[i]=sum2[i-1]+i*r[i];
M=max(M,sum1[i]);
}
ll ans=INF;
for(int t=1;t<=n;t++){//断环成链
for(int i=0;i<=k;i++)
for(int j=t;j<=t+n-1;j++)
f[i][j]=INF;
for(int j=t;j<=t+n-1;j++)
f[1][j]=(sum2[j]-sum2[t-1])-t*(sum1[j]-sum1[t-1]);/// f[1][j]=calc(t,j);
for(int i=2;i<=k;i++){
clear();
for(int p=t+i-2;p<=t+n-1;p++){
ll b=f[i-1][p]-sum2[p]+(p+1)*sum1[p];
ll k=-(p+1);
s[p]={k,b};
update(rt,-M,M,p);
}
for(int j=t+i-1;j<=t+n-1;j++){
//f[i][j]表示在j位置开了第i扇门的最小花费
// for(int p=t+i-2;p<j;p++){
// f[i][j]=min(f[i][j],f[i-1][p]+calc(p+1,j));
// }
ll val=ask(rt,-M,M,sum1[j])+sum2[j];
f[i][j]=min(f[i][j],val);
}
}
for(int j=t+k-1;j<=t+n-1;j++){
if(f[k][j]<INF){
ll res=f[k][j];
if(j<t+n-1) res+=calc2(j+1,t+n-1,t);
ans=min(ans,res);
}
}
}
// ll ans=INF;
// for(int i=k;i<2*n;i++) ans=min(ans,f[k][i]);
printf("%lld",ans);
return 0;
}
更为简洁的写法:
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=2010;
const ll INF=1e18;
int read(){
int x;scanf("%d",&x);return x;
}
struct{
ll k,b;
}s[N];
struct{
int l,r,c;
}t[N*20];
ll calc(int id,ll x){
return s[id].k*x+s[id].b;
}
int cnt,rt;
void update(int &p,ll l,ll r,int id){
if(!p) p=++cnt;
if(!t[p].c) return t[p].c=id,void();
ll mid=(l+r)>>1;
if(calc(id,mid)<calc(t[p].c,mid)) swap(id,t[p].c);
if(calc(id,l)<calc(t[p].c,l)) update(t[p].l,l,mid,id);
else if(calc(id,r)<calc(t[p].c,r)) update(t[p].r,mid+1,r,id);
}
ll ask(int p,ll l,ll r,ll x){
if(!p) return LONG_MAX;
ll ans=calc(t[p].c,x);
if(l==r) return ans;
ll mid=l+r>>1;
if(x<=mid) return min(ans,ask(t[p].l,l,mid,x));
return min(ans,ask(t[p].r,mid+1,r,x));
}
void clear(){
for(int i=1;i<=cnt;i++)
t[i].l=t[i].r=t[i].c=0;
cnt=rt=0;
}
//前面这套的都是李超树板子,一个字没改,看来这板子确实十分万能
ll r[N],f[10][N];
ll sum1[N],sum2[N];
ll calc2(int l,int r,int p){
return (sum2[r]-sum2[l-1])-p*(sum1[r]-sum1[l-1]);
}
int main(){
int n=read(),k=read();
for(int i=1;i<=n;i++) r[i]=read(),r[i+n]=r[i];
ll M=0;
for(int i=1;i<=2*n;i++){
sum1[i]=sum1[i-1]+r[i];
sum2[i]=sum2[i-1]+i*r[i];
M=max(M,sum1[i]);//求出最大的范围(千万不要太大,会直接影响到李超树的运行次数)
}
ll ans=INF;
for(int t=1;t<=n;t++){//断环成链 枚举第t个点为起点
for(int j=t;j<=t+n-1;j++)
f[1][j]=(sum2[j]-sum2[t-1])-t*(sum1[j]-sum1[t-1]);//算出一扇门到达所有点的花费
for(int i=2;i<=k;i++){
clear();//注意清空
for(int p=t+i-2;p<=t+n-1;p++){
//经过拆解dp转移式可以得到的kb,具体过程可见博客\
不过其实写在这里也不是不行 \
f[i][j]=min(f[i-1][p]+calc(p+1,j));\
=min(f[i-1][p]+sum2[j]-sum2[p]-(p+1)*(sum1[j]-sum1[p]);\
=sum2[j]+min(f[i-1][p]-sum2[p]+(p+1)*sum1[p]-(p+1)*sum1[j]);
ll b=f[i-1][p]-sum2[p]+(p+1)*sum1[p];
ll k=-(p+1);
//你看,我的b是不是就是min的前半部分,x是不是就是sum1[j],k是不是就是x的系数
s[p]={k,b};
update(rt,-M,M,p);
//把这些直线都存在线段树里
}
for(int j=t+i-1;j<=t+n-1;j++)
//f[i][j]表示在j位置开了第i扇门的最小花费
f[i][j]=ask(rt,-M,M,sum1[j])+sum2[j];
//ask相当于y,加上的是一个与p无关的常数,上文也提到它了
}
for(int j=t+k-1;j<=t+n-1;j++){
if(f[k][j]<INF){
ll res=f[k][j];
if(j<t+n-1) res+=calc2(j+1,t+n-1,t);
ans=min(ans,res);
}
}//这一段暴力里面也会有,和前面的预处理一样,因为复杂度本来就是n^2,所以也没什么可优化的了
}
printf("%lld",ans);
return 0;
}
不过话又说回来了,这道题其实还可以决策单调性或是分治,而且本来今天(2025/10/9)的专题就是分治,所以还要对其他做法进行进一步的了解.

浙公网安备 33010602011771号