ABC 365 F-Takahashi on Grid

原题链接:F - Takahashi on Grid

题意:n次输入,每次输入l,r。第i次输入代表,第i列上第l行到第r行是联通的。q次询问,每次给出二个点的坐标,问最短需要走几步?

参考:ABC365 A~G

思路:倍增,二分。看到题目之后,可以想到一个很直接很暴力的方法,那么就是按照题意去模拟每次询问的路线,输出最小值。如果这样写肯定会超时,但是可以思考一下这样硬走并且步数最小,那么我们路线的策略应该是能往右边走就往右边走,如果不能走,那么就去上下走,寻找能够去右边的路。如果我们一直向右边走就可以走到目标点的列,那么这二个点的最短路径就是它们x和y的差值绝对值之和。如果需要找到右边的路,那么这个点一定是右边的最上面的点或者最下面的点,然后从最上面或者最下面点继续走,就是重复上面的步骤了。

抽象一下操作,第一步从当前点走到不能继续往右边走的点,第二步上下走,寻找能走到右边一列的点,然后去右边的一列。因为第二步结束之后,我们的位置一定是这一列的最上面或者最下面,那么明显的优化思路就是,预处理出从每列最上面点和最下面的点,重复一次上面的步骤能够走到的点。如果目标点在当前点一次操作之后的位置,那么就可以直接从当前点到执行上面的操作之后的点,因为预处理了,所以可以直接去操作之后的点。称这种操作为跳跃。如果跳跃一下,会超过目标点,那么就直接将答案加上从当前点到目标点的哈夫曼距离。

上述思考过后,还有三个难点的问题没有解决。

第一个难点,是怎么预处理出一次跳跃的点,我们可以使用st表来维护区间的最大值和最小值,如果我们是从一列的最上方的点开始走那么,我们操作结束之后,我们肯定是停留在某一列的最上方的点,这一列一定是最上方的点,低于操作值的点。如果我们是从一列的最下方的点开始走那么,我们操作结束之后,我们肯定是停留在某一列的最下方的点,这一列一定是最下方的点,高于操作值的点。因为这个性质,所以二分就可以了。

第二个难点,是这样操作仍然会超时,因为给出的l和r完全可以卡掉第一步操作,一直执行第二步数,那么就相当于暴力走了,对于这个问题可以想到的是使用倍增算法,开辟数组st[i][j],代表从i点跳2^j次后到达的点。但是因为是一列的上下点,不好维护,所以可以对每一个编号处理。

第三个难点,是如何计算跳跃过程中走的步数呢?也可以开一个倍增数组w来记录一下就可以了。

//冷静,冷静,冷静
//调不出来就重构
#pragma GCC optimize(2)
#pragma GCC optimize("O3")
#include<bits/stdc++.h>
#define endl '\n'
using namespace std;
typedef long long ll;
typedef long double ld;
typedef pair<ll,ll> pii;
const int N=1e6+10,mod=1000000007,M=25;
ll n,st[N][M],w[N][M],a[N][2]; 
//对点进行编号 
ll up(ll x)//上面的点 
{
	return 2*x-1;
}
ll down(ll x)//下面的点 
{
	return 2*x;
}
//用编号还原出点的实际位置 
ll get_x(ll x)
{
	return (x+1)>>1;
}
ll get_y(ll x)
{
	return a[get_x(x)][(x+1)&1];
}
struct RMQ//st表板子 
{
	ll st1[N][M],st2[N][M];
	void init()
	{
		for(int i=1;i<=n;i++)
		{
			st1[i][0]=a[i][0];
			st2[i][0]=a[i][1];
		}
		for(int j=1;j<M;j++)
		{
			for(int i=1;i+(1ll<<j)-1<=n;i++)
			{
				st1[i][j]=min(st1[i][j-1],st1[i+(1<<j-1)][j-1]);
				st2[i][j]=max(st2[i][j-1],st2[i+(1<<j-1)][j-1]);
			}
		}
	}
	ll up_min(ll l,ll r)
	{
		ll k=log2(r-l+1);
		return min(st1[l][k],st1[r-(1ll<<k)+1][k]);
	}
	ll down_max(ll l,ll r)
	{
		ll k=log2(r-l+1);
		return max(st2[l][k],st2[r-(1ll<<k)+1][k]);
	}
}rmq;
ll calc(ll x,ll y,ll a,ll b)
{
	return abs(x-a)+abs(y-b);
}
ll jump(ll x,ll y)
{
	ll res1=n+1,res2=n+1;
	ll l=x+1,r=n;
	while(l+1<r)
	{
		ll mid=l+r>>1;
		if(rmq.up_min(x+1,mid)<y)
		{
			r=mid;
			res1=mid;
		}
		else l=mid;
	}
	if(rmq.up_min(x+1,l)<y)
	{
		res1=l;
	}
	l=x+1,r=n;
	while(l+1<r)
	{
		ll mid=l+r>>1;
		if(rmq.down_max(x+1,mid)>y)
		{
			r=mid;
			res2=mid;
		}
		else l=mid;
	}
	if(rmq.down_max(x+1,l)>y)
	{
		res2=l;
	}
	 
	if(res1==n+1&&res2==n+1)return n*2+1;//如果可以一直走,那么就返回一个不可能的位置
	if(res1<res2)return up(res1);//注意,二分求出来的是列号,所以最后需要转化为点的编号 
	else return down(res2);
}
ll solve(ll x,ll y,ll a,ll b)
{
	if(a<x)//强制a>b 
	{
		swap(a,x);
		swap(b,y);
	}
	ll t=jump(x,y);
	if(get_x(t)>=a)return calc(x,y,a,b);//如果一次也不要跳,就直接输出 
	ll ans=0;
	ans=ans+calc(x,y,get_x(t),get_y(t));//先跳到某一列的最上方或者最下方,这样就可以倍增的求 
	for(int i=M-1;i>=0;i--)
	{
		ll nt=st[t][i];
		if(get_x(nt)<=a)//注意nt其实是点的编号,所以比较的时候需要转化一下 
		{
			ans=ans+w[t][i];
			t=nt;
		}
	}
	ans=ans+calc(get_x(t),get_y(t),a,b);//最后只是来到了a列,还要移动到b行 
	return ans;
}
int main()
{
	ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
	cin>>n;
	for(int i=1;i<=n;i++)
	{
		cin>>a[i][1]>>a[i][0];//1代表这一列最下面的,0代表最上面的 
	}
	rmq.init();//st表 
	for(int i=1;i<=n;i++)//从下面的点跳 
	{
		ll t=jump(i,a[i][0]);//t代表能跳到的点转换成的编号 
		st[up(i)][0]=t;
		w[up(i)][0]=calc(i,a[i][0],get_x(t),get_y(t));//这二个点是可以直接计算的 
	}
	for(int i=1;i<=n;i++)//从上面的点跳 
	{
		ll t=jump(i,a[i][1]);
		st[down(i)][0]=t;
		w[down(i)][0]=calc(i,a[i][1],get_x(t),get_y(t));
	}
	st[n*2+1][0]=n*2+1;
	for(int j=1;j<M;j++)//倍增 
	{
		for(int i=1;i<=n*2+1;i++)
		{
			st[i][j]=st[st[i][j-1]][j-1];
			w[i][j]=w[i][j-1]+w[st[i][j-1]][j-1]; 
		}
	}
	ll q;cin>>q;
	while(q--)
	{
		ll x,y,a,b;cin>>x>>y>>a>>b;
		cout<<solve(x,y,a,b)<<endl;
	}
    return 0;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值