题解 [POI2010]ZAB-Frog

很厉害的题。倍增和单调队列。

这是 zpl 新手向算法第二弹,第一弹可以看小挖的核燃料填充 我会尽量讲的比较细致。

同第一弹,尽量配合代码食用。


这道题的题目描述写的不是特别清楚,这里我写了一个简要题意。

\(n\) 个点,升序给出每个点到起点的距离。有编号为 \(n\) 只青蛙编号分别为\(1 \to n\) 个点上,第 \(i\) 只青蛙在第 \(i\) 个点上。每次它们会跳到距离自己第 \(k\) 远的点上。

如果有相同距离的点,就跳到下标更小的点上。求跳 \(m\)次之后,第 \(i\) 只的青蛙在哪个点上。


因为 \(m\le 10^{18}\) 所以暴力跳肯定是寄的。

首先看到这个跳点的操作可以想到倍增,想到一开始做的倍增典题的描述的跳跃多数都是直接跳到第 \(i+k\) 个点上。所以如果我们可以提前预处理出每一个点会跳到哪里,这道题是板子了。

直接预处理第 \(k\) 远是不容易的,但是 \(x\) 的第 \(k\) 远的点一定是在 \(x\) 的两侧,且是一个长度为 \(k\) 的区间。想到滑动窗口所维护的单调队列是前 \(k\) 小,所以考虑使用单调队列解决这个问题。

这里有一个小性质,因为青蛙的一开始的位置和每个点到起点的距离都是升序的,所以 \(i\) 的第 \(k\) 远一定在 \(i-1\) 的第 \(k\) 远的右边,也就是说队尾直接每次加一就可以了。所以直接维护一个单调队列,设当前点为 \(i\),队首为 \(x\) 队尾为 \(y\),则若 \(a_{y+1}-a_i<a_i-a_{x}\)\(a_x\) 肯定不是第 \(k\) 小,弹出。而 \(y\) 要向后扩展到 \(y+1\)

可能理解起来比较复杂,建议模拟一边样例或者看代码。

然后是倍增的部分,这个还是比较板子的,考虑像快速幂一样二进制分解 \(m\)。但有一个小问题,我们可不可以像求 LCA 那样来做这道题呢?其实是一样的,我们求 LCA 的时候之所以维护一个 \(f_{i,j}\) 表示从 \(i\) 开始跳 \(2^j\) 步可以跳到哪里是因为我们不知道最多能跳多少步,而在这题中我们已经明确了跳 \(m\) 步所以跟 LCA 看起来可能不一样。

代码可能跟别的题解长得几乎一样,但因为方法一样所以代码都大同小异。我删除了调试部分。

const int N=1e6+10;
int k,n,m;
int tmp[N];
int a[N],nxt[N],pos[N];	
//a题目给的距离原点的距离,nxt是第i个点可以跳到哪里(也就是第k远),pos是存最终位置的
signed main()
{
	n=read();
	k=read();
	m=read();
	for(int i=1;i<=n;i++) a[i]=read();
	int head=1,tail=k+1;//对于第一个点,他的第k远肯定是第k+1个点
	for(int i=1;i<=n;i++)
	{
		while(tail+1<=n and a[tail+1]-a[i]<a[i]-a[head]) head++,tail++;//如果移动之后当前窗口依旧合法且下一个元素距离当前点的距离小于队首距离当前点的距离则队首不合法,并且下一个元素合法
		if(a[tail]-a[i]>a[i]-a[head]) nxt[i]=tail;//答案肯定是队首或者队尾,因为两边才是第k小,中间维护的是前k小。所以判断一下距离大小即可
		else nxt[i]=head;
	}
	for(int i=1;i<=n;i++) pos[i]=i;
	while(m)//倍增
	{
		if(m&1)
		{
			for(int i=1;i<=n;i++) pos[i]=nxt[pos[i]];//跳到他能跳到的点
		}
		m>>=1;
		for(int i=1;i<=n;i++) tmp[i]=nxt[i];
		for(int i=1;i<=n;i++) nxt[i]=tmp[tmp[i]];//改变nxt的值(因为原数组位置有改变)。
	}
	for(int i=1;i<=n;i++) cout<<pos[i]<<" ";
	return 0;
}
posted @ 2022-09-30 17:00  zplqwq  阅读(74)  评论(0编辑  收藏  举报