【洛谷5044】[IOI2018] 会议(笛卡尔树上DP)
- 一个长度为\(n\)的序列,每次询问给定一个区间,要求在区间中选择一个集合点,最小化区间内每个位置与它之间元素的最大值之和。
- \(n,q\le7.5\times10^5\)
笛卡尔树
这种有关区间最大值的问题我们容易想到借助笛卡尔树解决。
实际上很容易想到一种暴力做法,对每次询问在笛卡尔树上做一次\(DP\)。
设\(f_{l,r}\)为\([l,r]\)的答案,假设其中最大值所在的位置为\(x\)。
显然,若\(l\not=r\),我们一定不会选择\(x\)作为集合点,因为此时所有点与集合点之间的最大值都是最大值,肯定不优。
因此我们讨论集合点在哪边,得到转移方程:
但每次都这样跑一遍\(DP\)肯定是不行的,我们需要优化。
离线拆询问
按照上面的转移,一个询问\([L,R]\)可以根据其中最大值所在位置\(X\)拆成\([L,X-1]\)和\([X+1,R]\)两个询问区间。
发现\([L,X-1]\)恰好是其中最大值的子树对应区间的一段后缀,\([X+1,R]\)恰好是其中最大值的子树对应区间的一段前缀。二者非常类似,实际上我们可以只考虑其中一种(以\([X+1,R]\)为例),然后只要把序列翻转再求一遍就能解决\([L,X-1]\)了。
考虑离线,直接把\([X+1,R]\)扔到其中最大值的询问池中。
于是,我们现在的任务是要维护好笛卡尔树上一棵子树(设对应区间为\([l,r]\))所有前缀的答案\(f_{l,l\sim r}\)。
线段树优化\(DP\)
先递归处理左右子树,然后就是考虑如何由\(f_{x+1,x+1\sim r}\)更新得到\(f_{l,x+1\sim r}\)。
回顾先前的转移方程,若\(r\)加上\(1\),\(f_{l,x-1}+(r-x+1)\times a_x\)始终增大\(a_x\),而\(f_{x+1,r}+(x-l+1)\times a_x\)增幅肯定不会超过\(a_x\)(因为新添加上的\(r+1\)与集合点之间的最大值肯定不会超过\(a_x\))。
也就是说,一旦某一刻\(f_{l,x-1}+(r-x+1)\times a_x\)大于\(f_{x+1,r}+(x-l+1)\times a_x\),那么前者就始终大于后者了,所以我们只要通过线段树上二分就能找到划分位置,分两段转移。
这两种转移分别是把一段区间修改为一个等差数列和区间加上一个定值,除此之外还需要单点修改、单点查询、线段树上二分,都属于线段树的基础操作,就不赘述了。
代码:\(O(nlogn)\)
#include<bits/stdc++.h>
#define Tp template<typename Ty>
#define Ts template<typename Ty,typename... Ar>
#define Reg register
#define RI Reg int
#define Con const
#define CI Con int&
#define I inline
#define W while
#define N 750000
#define LN 20
#define LL long long
#define INF (LL)1e18
#define pb emplace_back
using namespace std;
int n,Qt,a[N+5],ql[N+5],qr[N+5];LL s1[N+5],s2[N+5];
namespace FastIO
{
#define FS 100000
#define tc() (FA==FB&&(FB=(FA=FI)+fread(FI,1,FS,stdin),FA==FB)?EOF:*FA++)
#define pc(c) (FC==FE&&(clear(),0),*FC++=c)
int OT;char oc,FI[FS],FO[FS],OS[FS],*FA=FI,*FB=FI,*FC=FO,*FE=FO+FS;
I void clear() {fwrite(FO,1,FC-FO,stdout),FC=FO;}
Tp I void read(Ty& x) {x=0;W(!isdigit(oc=tc()));W(x=(x<<3)+(x<<1)+(oc&15),isdigit(oc=tc()));}
Ts I void read(Ty& x,Ar&... y) {read(x),read(y...);}
Tp I void writeln(Ty x) {W(OS[++OT]=x%10+48,x/=10);W(OT) pc(OS[OT--]);pc('\n');}
}using namespace FastIO;
class SegmentTree
{
private:
#define PT CI l=1,CI r=n+1,CI rt=1
#define LT l,mid,rt<<1
#define RT mid+1,r,rt<<1|1
#define PU(x) (V[x]=V[x<<1|1])//每个节点记录区间最右边的值,用于线段树上二分
#define PD(x,r1,r2) (K[x]&&(Y(x<<1,r1,K[x],B[x]),\
Y(x<<1|1,r2,K[x],B[x]),K[x]=0),F[x]&&(T(x<<1,F[x]),T(x<<1|1,F[x]),F[x]=0))
#define T(x,v) (V[x]+=v,F[x]+=v)
#define Y(x,r,k,b) (V[x]=k*r+b,K[x]=k,B[x]=b,F[x]=0)
LL V[N<<2],K[N<<2],B[N<<2],F[N<<2];
public:
I void Bd(PT)//建树
{
if(K[rt]=B[rt]=F[rt]=0,V[rt]=INF,l==r) return;RI mid=l+r>>1;Bd(LT),Bd(RT);
}
I void U(CI x,Con LL& v,PT)//单点修改
{
if(l==r) return (void)(V[rt]=v);RI mid=l+r>>1;PD(rt,mid,r),x<=mid?U(x,v,LT):U(x,v,RT),PU(rt);
}
I void A(CI L,CI R,Con LL& v,PT)//区间加法
{
if(L<=l&&r<=R) return (void)T(rt,v);RI mid=l+r>>1;PD(rt,mid,r);
L<=mid&&(A(L,R,v,LT),0),R>mid&&(A(L,R,v,RT),0),PU(rt);
}
I void M(CI L,CI R,Con LL& k,Con LL& b,PT)//区间修改为等差数列
{
if(L<=l&&r<=R) return (void)Y(rt,r,k,b);RI mid=l+r>>1;PD(rt,mid,r);
L<=mid&&(M(L,R,k,b,LT),0),R>mid&&(M(L,R,k,b,RT),0),PU(rt);
}
I LL Q(CI x,PT)//单点询问
{
if(l==r) return V[rt];RI mid=l+r>>1;return PD(rt,mid,r),x<=mid?Q(x,LT):Q(x,RT);
}
I int G(CI L,CI R,CI a,Con LL& v,PT)//线段树上二分
{
if(l==r) return l;RI mid=l+r>>1;PD(rt,mid,r);if(R<=mid) return G(L,R,a,v,LT);
if(L>mid) return G(L,R,a,v,RT);return V[rt<<1]-1LL*a*mid<=v?G(L,R,a,v,LT):G(L,R,a,v,RT);
}
}S;
namespace RMQ//求区间最值编号(注意,由于序列翻转后要保证最值位置不变,给两次编号分别乘上系数op=±1)
{
int op=-1,LG[N+5];pair<int,int> Mx[N+5][LN+1];I void Init()
{
RI i,j;for(op*=-1,LG[0]=-1,i=1;i<=n;++i) Mx[i][0]=make_pair(a[i],op*i),LG[i]=LG[i>>1]+1;
for(j=1;(1<<j)<=n;++j) for(i=1;i+(1<<j)-1<=n;++i) Mx[i][j]=max(Mx[i][j-1],Mx[i+(1<<j-1)][j-1]);//预处理
}
I int P(CI l,CI r) {RI k=LG[r-l+1];return op*max(Mx[l][k],Mx[r-(1<<k)+1][k]).second;}//询问
}
struct Q {int p,r;I Q(CI x=0,CI y=0):p(x),r(y){}};
vector<Q> q[N+5];I int Solve(LL *s,CI l=1,CI r=n)//笛卡尔树上DP
{
if(l>r) return -1;RI x,lc,rc,p;LL F;if(l==r) {x=l,S.U(l,a[l]);goto End;}//边界
x=RMQ::P(l,r),lc=Solve(s,l,x-1),rc=Solve(s,x+1,r);//递归
if(!~rc) {S.U(x,S.Q(x-1)+a[x]);goto End;}if(!~lc) {S.U(x,a[x]),S.A(x+1,r,a[x]);goto End;}//如果只有一个儿子
F=S.Q(x-1),p=S.G(x,r+1,a[x],F-(2LL*x-l)*a[x]),S.M(x,p-1,a[x],F-(x-1LL)*a[x]),p<=r&&(S.A(p,r,(x-l+1LL)*a[x]),0);//线段树上二分划分点,然后分别转移
End:for(vector<Q>::iterator it=q[x].begin();it!=q[x].end();++it) s[it->p]=S.Q(it->r);return q[x].clear(),x;//处理询问池中的询问
}
int main()
{
RI i,x;for(read(n,Qt),i=1;i<=n;++i) read(a[i]);for(i=1;i<=Qt;++i) read(ql[i],qr[i]),++ql[i],++qr[i];
RMQ::Init();for(i=1;i<=Qt;++i) (x=RMQ::P(ql[i],qr[i]))^qr[i]&&(q[RMQ::P(x+1,qr[i])].pb(Q(i,qr[i])),0);S.Bd(),Solve(s1);//扔到[x+1,r]中最大值的询问池里
for(reverse(a+1,a+n+1),i=1;i<=Qt;++i) swap(ql[i],qr[i]),ql[i]=n-ql[i]+1,qr[i]=n-qr[i]+1;//翻转序列
RMQ::Init();for(i=1;i<=Qt;++i) (x=RMQ::P(ql[i],qr[i]))^qr[i]&&(q[RMQ::P(x+1,qr[i])].pb(Q(i,qr[i])),0);S.Bd(),Solve(s2);//扔到[x+1,r]中最大值的询问池里
for(i=1;i<=Qt;++i) if(ql[i]==qr[i]) writeln(a[ql[i]]);else x=RMQ::P(ql[i],qr[i]),//特判左右端点相等
writeln(min(s1[i]?s1[i]+(qr[i]-x+1LL)*a[x]:INF,s2[i]?s2[i]+(x-ql[i]+1LL)*a[x]:INF));return clear(),0;//讨论在哪个区间最终转移
}