Processing math: 100%

Codeforces 1175G - Yet Another Partiton Problem(李超线段树)

Codeforces 题面传送门 & 洛谷题面传送门

这是一道李超线段树的毒瘤题。

首先我们可以想到一个非常 trivial 的 DP:dpi,j​ 表示前 i​ 个数划分成 j​ 段的最小代价,那么显然 dpi,j=minl<i{dpl,j1+(il)·imaxt=l+1at},这样暴力 DP 是 n2k 的,一脸过不去。

考虑优化,注意到这里涉及一个 max,注意到在我们 DP 扫一遍 ai 的过程中,ai 的最大值显然是成段分布的,且我们可以通过单调栈求出这些段的左端点和右端点,因此我们考虑将 i​ 前面的部分进行分段,每一段用一个三元组 (L,R,M) 表示,表示 l[L,R] 都有 imaxt=l+1at=M,那么对于 [L,R] 这个区间中的任何一个 l,从 dpl,j1 转移到 dpi,j 的贡献都是 dpl,j1+(il)·M,也就是说上式可以写成 dpi,j=min(L,R,M){Rminl=Ldpl,j1+(il)·M},注意到对于一个固定的 M,我们只需要求出 dpl,j1lM 的最小值 mn,这一段转移到 dpi,j 的贡献就是 iMmn。如果我们令 k=M,x=i,b=mn,那么上式就可以写成 kx+b。想到了什么?没错,斜率优化,李超线段树,我们考虑以原序列中的下标为下标建一棵李超线段树,那么每一段的贡献就是一条直线,将它们插入李超线段树,那么查询 dpi,j 时就查询第 i 位置的最小值即可。

不过注意到我们这个连续段也不是一成不变的,在单调栈维护最大值的段时还会出现弹栈操作,具体来说,当加入一个 ai 时候我们会不断弹出栈顶元素直到栈为空或栈顶元素 >ai,这样带来的副作用就是以这些弹出的这些元素为右端点的连续段全部都会消失,取而代之的是一个大连续段,满足这个连续段中的最大值为 ai,那么我们就需要在李超线段树中删除这些直线,注意到我们是按时间顺序插入这些直线的,因此删除时肯定也会删除新插入的几条直线,因此我们像线段树分治那样开一个栈维护操作序列然后不断弹出栈顶元素并将李超线段树上对应元素改回其以前的版本知道回到操作前的序列为止。还有一个问题,就是这一段的 dpl,j1lM 也会改变,这个看似很好维护,实则比较困难。注意到我们是要对于新的 M,求出 dpl,j1lM 的最小值,如果我们再设 k=l,x=M,b=dpl,j1,那么柿子可以写作 kx+b。想到了什么?没错你没听错,还是李超线段树,我们考虑对每个连续段再建一棵李超线段树,维护这个区间中形如 lx+dpl,j1,那么弹栈过程中这些李超线段树就会合并,因此我们像 CF932F 那样合并这些连续段对应的李超线段树即可查询 dpl,j1lM 的最小值。

时间复杂度大概是 O(nklogn),具体证明大概就考虑每条直线在李超线段树上的深度,显然是不降的,而线段树深度最多 logn,因此这些直线在一轮(求解第二维相同的 DP 值)中下移的次数最多 logn

const int MAXN=2e4;
const int MAXV=2e4;
const int MAXK=100;
const int MAXP=MAXN*30;
const ll INF=0x3f3f3f3f3f3f3f3fll;
int n,k,a[MAXN+5];ll dp[MAXN+5][MAXK+5];
struct line{
ll k,b;
line(ll _k=0,ll _b=INF):k(_k),b(_b){}
ll get(int x){return 1ll*k*x+b;}
} lns[MAXN*2+5];
int lcnt=0;
struct node{int ch[2],mx;} s[MAXP+5];
int rt[MAXN+5],R=0,ocnt=0,ncnt=0;
struct chg{int k,on,ori;} op[MAXP+5];
void deal(int k,int id,int o){
// printf("deal %d %d %d\n",k,id,o);
if(o) op[++ocnt]={k,o,s[k].mx};
s[k].mx=id;
}
void insert(int &k,int l,int r,int v,int is){
if(!k) return k=++ncnt,deal(k,v,is),void();int mid=l+r>>1;
ll l1=lns[s[k].mx].get(l),r1=lns[s[k].mx].get(r),m1=lns[s[k].mx].get(mid);
ll l2=lns[v].get(l),r2=lns[v].get(r),m2=lns[v].get(mid);
if(l1<=l2&&r1<=r2) return;
if(l2<=l1&&r2<=r1) return deal(k,v,is),void();
if(m2<=m1){
if(l2<=l1) insert(s[k].ch[1],mid+1,r,s[k].mx,is),deal(k,v,is);
else insert(s[k].ch[0],l,mid,s[k].mx,is),deal(k,v,is);
} else {
if(l2<=l1) insert(s[k].ch[0],l,mid,v,is);
else insert(s[k].ch[1],mid+1,r,v,is);
}
}
int merge(int x,int y,int l,int r){
if(!x||!y) return x+y;insert(x,l,r,s[y].mx,0);
int mid=l+r>>1;//printf("%d %d\n",s[x].mx,s[y].mx);
s[x].ch[0]=merge(s[x].ch[0],s[y].ch[0],l,mid);
s[x].ch[1]=merge(s[x].ch[1],s[y].ch[1],mid+1,r);
return x;
}
ll query(int k,int l,int r,int p){
if(!k) return INF;int mid=l+r>>1;
if(l==r) return lns[s[k].mx].get(p);
return min((p<=mid)?query(s[k].ch[0],l,mid,p):query(s[k].ch[1],mid+1,r,p),
lns[s[k].mx].get(p));
}
int stk[MAXN+5],tp=0;
void clear(){
memset(rt,0,sizeof(rt));
for(int i=1;i<=ncnt;i++) s[i].ch[0]=s[i].ch[1]=s[i].mx=0;
ncnt=lcnt=ocnt=tp=R=0;
}
int main(){
scanf("%d%d",&n,&k);
for(int i=1;i<=n;i++) scanf("%d",&a[i]);
memset(dp,63,sizeof(dp));dp[0][0]=0;
for(int j=1;j<=k;j++){
clear();
for(int i=1;i<=n;i++){
lns[++lcnt]=line(-(i-1),dp[i-1][j-1]);
insert(rt[i],1,MAXV,lcnt,0);
// printf("lns[%d]={%lld,%lld}\n",lcnt,lns[lcnt].k,lns[lcnt].b);
while(tp>0&&a[stk[tp]]<a[i]){
while(ocnt>0&&op[ocnt].on==stk[tp]) s[op[ocnt].k].mx=op[ocnt].ori,ocnt--;
rt[i]=merge(rt[i],rt[stk[tp]],1,MAXV);tp--;
} lns[++lcnt]=line(a[i],query(rt[i],1,MAXV,a[i]));
// printf("lns[%d]={%lld,%lld}\n",lcnt,lns[lcnt].k,lns[lcnt].b);
insert(R,1,n,lcnt,i);dp[i][j]=query(R,1,n,i);
stk[++tp]=i;
// printf("%d %d %lld\n",i,j,dp[i][j]);
}
} printf("%lld\n",dp[n][k]);
return 0;
}
posted @   tzc_wk  阅读(95)  评论(0编辑  收藏  举报
编辑推荐:
· 理解Rust引用及其生命周期标识(下)
· 从二进制到误差:逐行拆解C语言浮点运算中的4008175468544之谜
· .NET制作智能桌面机器人:结合BotSharp智能体框架开发语音交互
· 软件产品开发中常见的10个问题及处理方法
· .NET 原生驾驭 AI 新基建实战系列:向量数据库的应用与畅想
阅读排行:
· C# 13 中的新增功能实操
· Ollama本地部署大模型总结
· 2025成都.NET开发者Connect圆满结束
· langchain0.3教程:从0到1打造一个智能聊天机器人
· 用一种新的分类方法梳理设计模式的脉络
点击右上角即可分享
微信分享提示