【IOI2018】会议【笛卡尔树】【dp】【线段树】

题意:长度为 n n n的序列, q q q次询问,每次给定一个区间,钦定区间中的一个位置 x x x,使得区间所有点 与 x x x之间的最大值(含端点) 之和 最小,输出最小值。

n , q ≤ 7.5 × 1 0 5 n,q\leq7.5\times10^5 n,q7.5×105

神仙题,不愧是IOI

首先有一个 O ( n 2 ) O(n^2) O(n2)的 dp

f ( l , r ) = min ⁡ { f ( l , k − 1 ) + ( r − k + 1 ) h k , ( k − l + 1 ) h k + f ( k + 1 , r ) } f(l,r)=\min\{f(l,k-1)+(r-k+1)h_k,(k-l+1)h_k+f(k+1,r)\} f(l,r)=min{f(l,k1)+(rk+1)hk,(kl+1)hk+f(k+1,r)}

其中 k k k l , r l,r l,r中的最大值的位置(如有多个随便选一个),即讨论钦定的点在最大值的左侧或右侧,然后另一侧的点贡献都是最大值

我觉得我考场上能想到这步就不错了

这个 dp 转移已经 O ( 1 ) O(1) O(1)了,也不好压成一维,所以要么用可持久化之类的东西强行压状态,要么就不记录无用的状态

直接想的话两条路都不好走,但注意到这个过程实际上是最值分治,自然地想到笛卡尔树

哪里自然了啊kora

具体地讲,建出序列的笛卡尔树,然后在树上做上面dp,每个点只记录它代表的区间的dp值,这样就可以 O ( n ) O(n) O(n)处理出来了。

然而就算处理出来了你仍然无法快速计算答案,因为询问区间可能会被拆成很多小段,你需要像平衡树一样沿着树递归下去。而笛卡尔树高是 O ( n ) O(n) O(n)的,仍然可以被卡成狗。

不过思路感觉很对,考虑怎么优化

观察一下这个dp方程式

f ( l , r ) = min ⁡ { f ( l , k − 1 ) + ( r − k + 1 ) h k , f ( k + 1 , r ) + ( k − l + 1 ) h k } f(l,r)=\min\{f(l,k-1)+(r-k+1)h_k,f(k+1,r)+(k-l+1)h_k\} f(l,r)=min{f(l,k1)+(rk+1)hk,f(k+1,r)+(kl+1)hk}

套到树上:当前子树根结点是 k k k,为了计算 f ( l , k − 1 ) f(l,k-1) f(l,k1) f ( k + 1 , r ) f(k+1,r) f(k+1,r),我们需要继续往左右子树递归计算,我们这样子是不行的

但这个 f ( l , k − 1 ) f(l,k-1) f(l,k1) f ( k + 1 , r ) f(k+1,r) f(k+1,r)比较特殊:它们都有一个端点是固定的!

为了叙述方便,下面只讨论 f ( k + 1 , r ) f(k+1,r) f(k+1,r),左边的 f ( l , k − 1 ) f(l,k-1) f(l,k1)是同理的

我们要是知道右子树的区间的所有前缀的dp信息就好了

看上去很扯,但实际上是可行的!

假设我们分别知道 k k k的左右子树的前缀信息,也就是知道 f ( l , l . . . k − 1 ) f(l,l...k-1) f(l,l...k1) f ( k + 1 , k + 1... r ) f(k+1,k+1...r) f(k+1,k+1...r),现在考虑怎么合并 f ( l , l . . . r ) f(l,l...r) f(l,l...r)

显然左边是不用管的

对于右边,我们再把这个方程式搬出来。为了看着顺眼,我把 r r r换成了 i i i

f ( l , i ) = min ⁡ { f ( l , k − 1 ) + ( i − k + 1 ) h k , f ( k + 1 , i ) + ( k − l + 1 ) h k } f(l,i)=\min\{f(l,k-1)+(i-k+1)h_k,f(k+1,i)+(k-l+1)h_k\} f(l,i)=min{f(l,k1)+(ik+1)hk,f(k+1,i)+(kl+1)hk}

注意这个 i i i是在 [ k + 1 , r ] [k+1,r] [k+1,r]内的,冷静分析一下,发现这个东西就是在原来的基础上整体加一个数,然后和一个一次函数取 min ⁡ \min min,因为是个区间,所以可以用线段树维护!

而这个方程是可以找到一个分界点,使得分界点左边取左边的值,右边取右边的值。原因是 i i i每增加 1 1 1,左边的值固定增加 h k h_k hk,而右边增加 f ( k + 1 , i + 1 ) − f ( k + 1 , i ) f(k+1,i+1)-f(k+1,i) f(k+1,i+1)f(k+1,i),区间往右扩张一位增加的代价总不可能大于区间最大值吧……所以左边迟早会超过右边,而且不会被反超。线段树上二分即可。

什么?线段树开不下?

但仔细想想会发现不同的位置是不会冲突的,即每个点 x x x维护它当前处理的左端点到 x x x的dp值,所以开一个线段树就可以了。

线段树合并应该也可以

f ( l , k − 1 ) f(l,k-1) f(l,k1)的话再开一棵线段树,左右倒过来就可以了

然后把询问离线,每组询问挂在区间最大值的点上面,建笛卡尔树的时候顺便处理一下就可以了。

复杂度 O ( n log ⁡ n ) O(n\log n) O(nlogn)

#include <iostream>
#include <cstring>
#include <cctype>
#include <cstdio>
#include <vector>
#define MAXN 750005
using namespace std;
inline int read()
{
	int ans=0;
	char c=getchar();
	while (!isdigit(c)) c=getchar();
	while (isdigit(c)) ans=(ans<<3)+(ans<<1)+(c^48),c=getchar();
	return ans;
}
typedef long long ll;
int n;
struct SegmentTree
{
	#define lc p<<1
	#define rc p<<1|1
	struct node
	{
		int l,r,tag;
		ll k,b,lv,rv;
	}t[MAXN<<2];
	void build(int p,int l,int r)
	{
		t[p].l=l,t[p].r=r;
		if (l==r) return;
		int mid=(l+r)>>1;
		build(lc,l,mid),build(rc,mid+1,r);
	}
	inline void pushcov(int p,ll k,ll b)
	{
		t[p].k=k,t[p].b=b;
		t[p].lv=k*t[p].l+b,t[p].rv=k*t[p].r+b;
		t[p].tag=1;
	}
	inline void pushadd(int p,ll k,ll b)
	{
		t[p].k+=k,t[p].b+=b;
		t[p].lv+=k*t[p].l+b,t[p].rv+=k*t[p].r+b;
		if (!t[p].tag) t[p].tag=2;		
	}
	inline void pushdown(int p)
	{
		if (t[p].tag)
		{
			if (t[p].tag==1) pushcov(lc,t[p].k,t[p].b),pushcov(rc,t[p].k,t[p].b);
			if (t[p].tag==2) pushadd(lc,t[p].k,t[p].b),pushadd(rc,t[p].k,t[p].b);
			t[p].tag=t[p].k=t[p].b=0;
		}
	}
	inline void update(int p){t[p].lv=t[lc].lv,t[p].rv=t[rc].rv;} 
	ll query(int p,int k)
	{
		if (t[p].l==t[p].r) return t[p].lv;
		pushdown(p);
		if (k<=t[lc].r) return query(lc,k);
		return query(rc,k);
	}
	void search(int p,int l,int r,ll k1,ll b1,ll b2)
	{
		if (l<=t[p].l&&t[p].r<=r)
		{
			if (k1*t[p].l+b1<=t[p].lv+b2&&k1*t[p].r+b1<=t[p].rv+b2) return pushcov(p,k1,b1);
			if (t[p].lv+b2<=k1*t[p].l+b1&&t[p].rv+b2<=k1*t[p].r+b1) return pushadd(p,0,b2);
		}
		if (r<t[p].l||t[p].r<l) return;
		pushdown(p);
		search(lc,l,r,k1,b1,b2),search(rc,l,r,k1,b1,b2);
		update(p);
	}
}lt,rt;
int h[MAXN];
inline int Max(const int& x,const int& y){return h[x]>h[y]? x:y;}
int st[20][MAXN],LOG[MAXN];
inline int rmq(int l,int r)
{
	int t=LOG[r-l+1];
	return Max(st[t][l],st[t][r-(1<<t)+1]);
}
int ql[MAXN],qr[MAXN];
ll ans[MAXN];
vector<int> lis[MAXN];
void solve(int l,int r)
{
	if (l>r) return;
	int k=rmq(l,r);
	solve(l,k-1),solve(k+1,r);
	for (int i=0;i<(int)lis[k].size();i++)
	{
		int L=ql[lis[k][i]],R=qr[lis[k][i]];
		ans[lis[k][i]]=h[k]*(R-L+1ll);
		if (L<k) ans[lis[k][i]]=min(ans[lis[k][i]],rt.query(1,L)+(R-k+1ll)*h[k]);
		if (k<R) ans[lis[k][i]]=min(ans[lis[k][i]],(k-L+1ll)*h[k]+lt.query(1,R));
	}
	ll tl=rt.query(1,l),tr=lt.query(1,r);
	lt.search(1,k,r,h[k],tl+h[k]*(1ll-k),(k-l+1ll)*h[k]);
	rt.search(1,l,k,-h[k],tr+h[k]*(k+1ll),(r-k+1ll)*h[k]);
}
int main()
{
	n=read();
	int q=read();
	for (int i=1;i<=n;i++) h[i]=read();
	lt.build(1,1,n);rt.build(1,1,n);
	for (int i=1;i<=n;i++) st[0][i]=i;
	for (int j=1;j<20;j++)
		for (int i=1;i+(1<<(j-1))<=n;i++)
			st[j][i]=Max(st[j-1][i],st[j-1][i+(1<<(j-1))]);
	LOG[0]=-1;
	for (int i=1;i<=n;i++) LOG[i]=LOG[i>>1]+1;
	for (int i=1;i<=q;i++) ql[i]=read()+1,qr[i]=read()+1,lis[rmq(ql[i],qr[i])].push_back(i);
	solve(1,n);
	for (int i=1;i<=q;i++) printf("%lld\n",ans[i]);
	return 0;
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值