《论为什么现场的我想不出 O ( n log n ) O(n\log n) O(nlogn) 做法只能用莫队拿 65 分这件事》
Step 1
关于丹钓站单调栈,我们有一个很好的性质。我们先按照题目的规定将
n
n
n 个二元组模拟一遍入栈过程。在过程中对每一个元素
(
a
i
,
b
i
)
(a_i,b_i)
(ai,bi),记录
p
r
e
i
pre_i
prei 表示这个元素进栈时栈顶元素的下标,若栈空则记为 -1。
你试几个数据后就会发现,得到所有 p r e i pre_i prei 之后,问题瞬间变简单了。假设答案询问的区间是 [ l , r ] [l,r] [l,r],那么对于区间中的每个二元组 ( a i , b i ) (a_i,b_i) (ai,bi),如果 p r e i < l pre_i<l prei<l,则二元组 ( a i , b i ) (a_i,b_i) (ai,bi) 是“成功的”,反之则不是“成功的”。这个性质其实是比较显然的。
Step 2
现在问题被我们转化为:
有 n n n 个 p r e i pre_i prei,每次询问区间 [ l , r ] [l,r] [l,r] 中 p r e i < l pre_i<l prei<l 的个数。
我们希望去用线段树或 BIT 处理这样的区间问题。
然后,我们发现这个 l l l 很讨厌,它不是固定的。这样我们自然而然地想到把它变得有序,就会很好处理。那就先离线下来,将所有询问按 l l l 升序排序。同时也将所有下标 i i i 按 p r e i pre_i prei 升序排序。
接下来,我们维护线段树或 BIT,这里用 c i c_i ci 表示元素 i i i 当前是否满足 p r e i < l pre_i<l prei<l,是则为 1,否则为 0。
每次对于一个询问 [ l , r ] [l,r] [l,r],我们将所有 p r e i < l pre_i<l prei<l 且 c i = 0 c_i=0 ci=0 的元素令 c i ← 1 c_i\gets 1 ci←1,然后查询 ∑ i = l r c i \sum\limits_{i=l}^rc_i i=l∑rci 作为询问答案。
最后按照询问编号输出。
Code
#include <bits/stdc++.h>
#define ll long long
#define pb push_back
#define pii pair<int,int>
#define mp make_pair
#define F first
#define S second
#define debug(x) cerr<<#x<<"="<<x<<endl
using namespace std;
inline ll read()//快读快写
{
ll x=0,f=0;
char ch=getchar();
while(!isdigit(ch))
f|=ch=='-',ch=getchar();
while(isdigit(ch))
x=(x<<1)+(x<<3)+(ch^48),ch=getchar();
return f?-x:x;
}
template<typename T>inline void write(T x)
{
if(x<0)x=-x,putchar('-');
if(x>9)write(x/10);
putchar(x%10^48);
}
int n,m;//n:元素个数,m:询问个数
pii a[500005],pre[500005];
int st[500005],tp,ans[500005],sum[1000005];
struct Query//定义询问结构体
{
int l,r,id;
bool operator<(Query &B)
{
return l!=B.l? l<B.l : r<B.r;
}
}q[500005];
void modify(int l,int x)//线段树单点修改
{
l+=n;
sum[l]+=x;
while(l>1)
{
l>>=1;
sum[l]=sum[l<<1]+sum[l<<1|1];
}
}
int query(int l,int r)//线段树区间查询
{
int res=0;
for(l+=n,r+=n;l<r;l>>=1,r>>=1)
{
if (l&1) res+=sum[l++];
if (r&1) res+=sum[--r];
}
return res;
}
int main()
{
//freopen("stack.in","r",stdin);
//freopen("stack.out","w",stdout);
n=read(),m=read();
for(int i=0;i<n;i++)
a[i].F=read();
for(int i=0;i<n;i++)
a[i].S=read();
for(int i=0;i<m;i++)
q[i].l=read()-1,q[i].r=read()-1,q[i].id=i;
sort(q,q+m);//对询问按左端点排序
for(int i=0;i<n;i++)//模拟入栈过程
{
while(tp&&!(a[st[tp]].F!=a[i].F&&a[st[tp]].S>a[i].S))
--tp;
pre[i].F=(tp==0?-1:st[tp]);
pre[i].S=i;
st[++tp]=i;
}
sort(pre,pre+n);//对下标按pre排序
int cur=0;//维护当前第一个pre>=l的下标
for(int i=0;i<m;i++)
{
while(cur<n&&pre[cur].F<q[i].l)
{
modify(pre[cur].S,1);
cur++;
}
ans[q[i].id]=query(q[i].l,q[i].r+1);//我的线段树区间[l,r)左闭右开
}
for(int i=0;i<m;i++)
write(ans[i]),putchar('\n');
return 0;
}
时间复杂度: O ( n log n + m log m ) O(n\log n + m\log m) O(nlogn+mlogm)
空间复杂度: O ( n + m ) O(n+m) O(n+m)
Summary
我个人认为这是一道很不错的题目 ,就是谐音梗俗了点 。它很好的把单调栈的性质、区间操作和离线排序的技巧结合在一起。对我这种蒟蒻还是有些思维难度,希望我下次不会被这样的题难住了罢。
希望题解对你有帮助!