vijos 1471 教主的游乐场 - 线段树优化dp

传送门

题解:首先这个题可以n遍bfs。

然后发现dp只记录一维状态不够,因为可能有环,不满足dp性质。

解决方案有如下两种:

1.于是找性质,发现非常有意思的一点是每个点可以向前跳到任何一个点。

考虑一条最优解路径,如果先向右跳了一步,然后又向左跳了一步,那么不如直接向右/左跳一步。

也就是最优解至多在第一次跳是向左的。

因此可以先处理一个点只往右跳的答案,这个显然可以用线段树维护出来。

然后再考虑每个点的答案等于只往右跳的答案和先向左跳一步再向右跳这两者取min。

后面一部分维护一个前缀最值即可做到O(n)。

考虑线段树常数巨大,第一部分注意到每次询问虽然强制在线但是区间的左端点递减,

因此可以用单调队列维护,每次在队列里面二分。

这个过程依旧可以优化,注意到队列里面实际上是若干区间,每次询问就是询问一个点所在区间的(最小)值,

而每次左面加一个点相当于把前面的几个区间合并。

本来是要用可并堆维护的,但是由于没有删除操作所以区间只需要维护一个最值即可。

这样用并查集维护可以做到O(nα(n))。


但是如果注意到之所以每次要在单调队列里面二分是因为询问右端点不单调,

或者在一开始dp的时候能够转化模型为:

给定若干区间,每次可以从当前区间跳到另一个左端点小于等于当前区间右端点的区间

不难发现最优解中的右端点必然是严格递增的。因此可以以右端点为阶段划分进行dp。

对区间按照右端点排序使用桶牌显然可以做到O(n)。

然后这样dp显然是可以用单调队列维护的,而且询问的右端点是单调的。

这样复杂度就可以做到O(n)。

但是我太懒了只写了个最笨的线段树。


代码:

#include<iostream>
#include<cstring>
#include<cstdio>
#include<algorithm>
#include<climits>
#define MAXN 100010
#define INF INT_MAX/2
using namespace std;
int f[MAXN],a[MAXN];
struct segment{
	int l,r,v;
	segment *ch[2];
}*rt;
int build(segment* &rt,int l,int r)
{
	rt=new segment;
	rt->l=l,rt->r=r;rt->v=0;
	if(l==r) return 0;
	int mid=(l+r)>>1;
	build(rt->ch[0],l,mid);
	build(rt->ch[1],mid+1,r);
	return 0;
}
int query(segment* &rt,int s,int t)
{
	int l=rt->l,r=rt->r;
	if(s<=l&&r<=t) return rt->v;
	int mid=(l+r)>>1,ans=INF;
	if(s<=mid) ans=min(ans,query(rt->ch[0],s,t));
	if(mid<t) ans=min(ans,query(rt->ch[1],s,t));
	return ans;
}
inline int push_up(segment* &rt)
{
	return rt->v=min(rt->ch[0]->v,rt->ch[1]->v);
}
int update(segment* &rt,int p,int v)
{
	int l=rt->l,r=rt->r;
	if(l==r) return rt->v=v;
	int mid=(l+r)>>1;
	if(p<=mid) update(rt->ch[0],p,v);
	else update(rt->ch[1],p,v);
	return push_up(rt);
}
int main()
{
	int n,m;scanf("%d%d",&n,&m);
	for(int i=1;i<=n;i++) scanf("%d",&a[i]);
	for(int i=1;i<=n;i++)
		if(a[i]+i>n) a[i]=n+1-i;
	build(rt,1,n+1);
	for(int i=n;i;i--)
	{
		if(a[i]) f[i]=query(rt,i+1,i+a[i])+1;
		else f[i]=INF;
		update(rt,i,f[i]);
	}
	for(int i=1;i<=n;i++)
	{
		if(i-1) f[i]=min(f[i],query(rt,1,i-1)+1);
		update(rt,i,f[i]);
	}
	for(int i=1;i<=m;i++)
	{
		int x;scanf("%d",&x);
		if(i^m) printf("%d ",f[x]);
		else printf("%d\n",f[x]);
	}
	return 0;
}


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值