本文已参与「新人创作礼」活动,一起开启掘金创作之路。
样例输入:
1
10 5
3 7 9 2 6 4 5 8 10 1
1 10
2 6
7 9
4 9
3 3
样例输出:
2
1
0
1
0
题意:给定任何一个长度为𝑁的数组𝐴 = (𝑎1, 𝑎2, . . . , 𝑎𝑛),令𝐵(𝐴)表示对𝐴进行一次bubble sort循环之后得到的数组。令𝑛𝑢𝑚(𝐴)表示从𝐴到𝐵(𝐴)最少需要移动元素(数组区间循环移位)的次数。给定一个1 − 𝑛的排列𝑃以及𝑞组1 ≤ 𝑙 ≤ 𝑟 ≤ 𝑛,求𝑛𝑢𝑚(𝑃 [𝑙, 𝑟])。
分析:现在假如我们有t个数a1,a2,……,at,我们对区间[1,t]进行一次循环左移,就相当于先用a1和a2换位,再用a1和a3换位……,再用a1和at换位,这个操作非常像我们冒泡排序的过程,通过分析这个我们就不难发现假如我们要询问num(P[l,r]),那么就先找到一个区间[l,x]进行一次循环左移,其中x是l后面第一个大于a[l]的数的前一个,前提是x不能等于l,也就是说区间[l,x]的长度至少是1,那么我们就可以定义一个不同于数学中的极值点,第i个点为极大值点的充分必要条件就是a[i]>a[i+1]。数学中的极大值点定义还要求a[i]>a[i-1],这里不作要求,那么通过模拟不难发现𝑛𝑢𝑚(𝑃 [𝑙, 𝑟])就是[l,r]中递增极值点的个数。不妨假设递增极值点个数为n,那么这n个递增极值点就是我们的循环左移区间的左边界,这个模拟不难得到,那我们怎么求解一个区间中递增极大值点的个数呢?就是先用一个数组记录每一个位置后面出现的第一个极大值点,不妨记为f[i]吧。一开始写的是用单调栈记录每一个数后面第一个大于他的数的位置,然后倒着进行判断,如果第一个大于他的数的位置是当前数的后一个,那么这种情况就对应着区间长度为1的情况,我们就直接令f[i]=f[i+1],否则f[i]就是i后面第一个大于等于i的数的位置,然后直接暴力跳,但是这样会超时,所以我们就需要对这种方式进行优化,这个时候我们就会想到倍增,也就是说设f[i][j]记录第i个位置的数后面第2^j个递增极大值点所在的位置,那么我们用倍增判断[l,r]内有多少个递增极大值点的复杂度就变为logn,这样复杂度就够了,需要注意的一个点是我们在倍增过程中的退出条件是f[l][i]<=r,有可能从l出发一次跳的比r大,这种状态对应的是两种情况,一种是区间[l,r]是递增的,那么这种情况下本身就不需要考虑,但是还有一种情况我们不得不考虑,就是[l,r]中存在一个极大值点,从这个极大值点找到下一个大于当前极大值点的位置是大于等于r的,如果我们不考虑这种情况我们就会少算答案。所以那这种情况怎么办呢?我们f[i][0]记录的是i后面能够跳到的第一个递增极大值点,这是不包含第i个位置的,而刚才这种情况我们是需要考虑到i这个位置的,所以我们需要新开一个ne[i]表示i后面(包含i)的第一个极大值点出现的位置,这样就可以O(1)判断倍增后的区间[l,r)中是否还有极大值点了。
细节见代码:
#include<cstdio>
#include<iostream>
#include<algorithm>
#include<cstring>
#include<map>
#include<queue>
#include<vector>
#include<cmath>
using namespace std;
const int N=1e5+10;
int st[N],tt,a[N];
int f[N][21];//f[i][j]记录第i个位置的数后面第2^j个递增极大值点所在的位置
int mp[N],ne[N];//ne[i]记录第i个数及之后的数中出现的第一个极大值点的位置
//注意:此处的极大值与数学中的极大值略有不同,此处的第i个位置为极大值点当且仅当a[i]>a[i+1],不要求a[i]>a[i-1]
int main()
{
int T;
cin>>T;
while(T--)
{
int n,q;
scanf("%d%d",&n,&q);
tt=0;
for(int i=1;i<=n;i++)
{
scanf("%d",&a[i]);
mp[a[i]]=i;
}
for(int i=n;i>=1;i--)//单调栈求每个数后面第一个比他大的数的位置
{
while(tt&&a[i]>st[tt]) tt--;
if(!tt) f[i][0]=n+1;
else f[i][0]=mp[st[tt]];
st[++tt]=a[i];
}
ne[n]=n+1;//后面没有极大值就默认极大值出现的位置是n+1
for(int i=n-1;i>=1;i--)
if(f[i][0]==i+1)
{
f[i][0]=f[i+1][0];
ne[i]=ne[i+1];
}
else ne[i]=i;
for(int i=n-1;i>=1;i--)//倍增预处理
for(int j=1;j<=20;j++)
f[i][j]=f[f[i][j-1]][j-1];
while(q--)
{
int ans=0,l,r;
scanf("%d%d",&l,&r);
for(int i=20;i>=0;i--)
{
if(f[l][i]&&f[l][i]<=r)
{
l=f[l][i];
ans+=1<<i;
}
}
if(ne[l]<r) ans++;//如果区间[l,r-1]之间还有一个极大值,那么就还需要一次转移
printf("%d\n",ans);
}
}
return 0;
}