P9342 Bitaro's Travel 题解

发布时间 2023-08-10 19:05:17作者: Mo默Sh笙

模拟赛做到的题,赛后看了 Y2hlbnlpa2Fp 的题解,感觉没讲清楚,这里做下补充,提供自己的理解。


基本思路:

对每个 \(A_i\) 的答案进行预处理,对于每个询问,只需要找到第一个到达的景点即可。

那么如何预处理每个点的答案呢?有一条很重要的性质:最多转向 \(\log{X}\)

要证明这个结论,先放上一张图:

设第 \(k\) 段路径长度为 \(L\),从图中可以看出,\(L_{k}\geq 2\times L_{k-1}\),因为 \(1 \leq L_k\leq X\),可以推得 \(k\leq \log{X}\)

证明了这个结论,我们就可以在 \(\mathcal{O}(n^2\log{n})\) 的时间暴力走一遍预处理出答案,运用ST表倍增可以优化到 \(\mathcal{O}(n\log^2{n})\)

对于每个询问,二分找到第一个到达的景点,直接输出对应的答案即可。

整体复杂度 \(\mathcal{O}(n\log^2{n}+q\log{N})\)

注释很详细,写在代码里,注释掉的代码是对 Y2hlbnlpa2Fp 题解的改进。


\(Code\)

#include<bits/stdc++.h>//MoSheng
using namespace std;
#define ll long long
#define ul unsigned long long
#define db double
#define il inline
#define re register
#define INF 0x3f3f3f3f
#define LINF 0x3f3f3f3f3f3f3f3f//ll范围下LINF,够大 
#define int ll//要开longlong 
#define F(i,a,b) for(int (i)=(a);(i)<=(b);(i)++)
#define DF(i,a,b) for(int (i)=(a);(i)>=(b);(i)--)
#define G(i,u) for(int (i)=head[u];(i);(i)=nxt[(i)])
inline ll read(){ll x=0,f=1;char ch=getchar();while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();}while(ch>='0'&&ch<='9'){x=(x<<3)+(x<<1)+ch-48;ch=getchar();}return x*f;}
const int N=200010;
int n;
int pos[N],ans[N];
int stl[N][25],str[N][25];
signed main()
{
	n=read();
	F(i,1,n) pos[i]=read();
	pos[0]=-LINF;//边界处理I
	pos[n+1]=LINF;//一定要开7f^4或3f^8,1e9范围下3f^4太小 
//	if(n==1)//特判?其实没必要,因为有边界处理 
//	{
//		int q=read();
//		F(i,1,q)
//		{
//			int loc=read();
//			printf("%lld\n",abs(loc-pos[1]));
//		}
//		return 0;
//	}
	F(i,1,n-1) stl[i][0]=pos[i]-(pos[i+1]-pos[i]);//该点向右移动的距离在左边的坐标映射,便于与左节点距离比较 
	stl[n][0]=-LINF;//边界处理II
	F(i,2,n) str[i][0]=pos[i]+(pos[i]-pos[i-1]);//该点向左移动的距离在右边的坐标映射,便于与右节点距离比较 
	str[1][0]=LINF;
	F(k,1,20)
	{
		F(i,1,n)
			if(i+(1<<(k-1))<=n)
				stl[i][k]=min(stl[i][k-1],stl[i+(1<<(k-1))][k-1]);//过程中最远的一次能满足就一定满足全过程,所以取极值 
//			else//不满足则说明扩展到底了,不用继续拓展。但其实这步不需要,因为处理时不满足时会continue 
//				stl[i][k]=stl[i][k-1];
		F(i,1,n)//DF(i,n,1)//正反皆可 
			if(i-(1<<(k-1))>=1)
				str[i][k]=max(str[i][k-1],str[i-(1<<(k-1))][k-1]);
//			else
//				str[i][k]=str[i][k-1];
	}
	F(i,1,n)
	{
		int l=i,r=i,dir=0,now=i;//l和r分别表示左右两边走到的最远点 
		if(pos[i]-pos[i-1]<=pos[i+1]-pos[i]) dir=0;//判断开始时移动方向,0为左,1为右  
		else dir=1; 
		while(1<l||r<n)//还有没走的点 
		{
			if(dir==0)//先向左再向右转 
			{
				now=l;
				now--;//转完向至少走一步,提前走掉 
				DF(k,20,0)//倍增经典倒序,向左走到不能走 
				{
					if(now-(1<<k)<1) continue;//无法往左移动1<<k次就跳过,注意不能带= 
					if(str[now][k]<=pos[r+1]) now-=(1<<k);//向左走的距离更近就走,注意这里now不需要-1,因为我们已经提前走过一步了 
				}
//				now--;//这一步与now的-1一样,其实就是上一次转向后至少要走的那一步,我们在转向后立刻处理就不需要(不能加)这一步 
				ans[i]+=pos[r]-pos[now];
				l=now;
				dir=1;//记得转向 
			}
			else// if(dir==1)//先向右再向左转 
			{
				now=r;
				now++;//转完向至少走一步,提前走掉 
				DF(k,20,0)//倍增经典倒序,向右走到不能走 
				{
					if(now+(1<<k)>n) continue;//无法往右移动1<<k次就跳过,注意不能带=
					if(stl[now][k]>pos[l-1]) now+=(1<<k);//向右走的距离更近就走,与dir==0的情况同理,now不需要+1 
				} 
//				now++;//与dir==0的情况同理,这步不加 
				ans[i]+=pos[now]-pos[l];
				r=now;
				dir=0;//记得转向 
			}
		}
	}
	int q=read();
	F(i,1,q)
	{
		int loc=read();
		int R=upper_bound(pos+1,pos+n+1,loc)-pos;//找右边最近点,lower_bound和upper_bound都可 
		int L=R-1;//左边最近点 
		if(loc-pos[L]<=pos[R]-loc) printf("%lld\n",loc-pos[L]+ans[L]);
		else printf("%lld\n",pos[R]-loc+ans[R]);
	}
	return 0;
}