【题解】P8251 [NOI Online 2022 提高组] 丹钓战(官方数据)
题目传送门
题面
给你 n 个数对 (a,b) 和一个栈 S 。
向 S 中添加数对 \((a_i,b_i)\) 时,先不断弹出栈顶直至栈空或者栈顶的 \((a_j,b_j)\) 满足 \(a_i \neq a_j ,b_i<b_j\) 。
现给你 q 组 \(l,r\) ,问你加入 \(l\sim r\) 的数对时,有多少数对是成功的。
一个数对是成功的,当且仅当加入这个数对后栈中只有一个元素。
思路
先来考虑,如果一个点是成功的,那么什么会让它变得不成功呢?
首先,在它后面加入的元素时不会恶心到它的,但是前面的会。然而,多一个数就多一个条件,所以元素增加可能让它挂掉,然鹅减少并不会。
总的来说,l 越小,一个成功的数对就越容易挂掉。
那么,我们考虑维护一个 \(lst_i\) ,表示第 i 个数对在 l 缩小至 \(lst_i\) -1 时会挂掉。
它怎么求呢?
因为右端点无所谓,我们索性把它固定在 n 的位置,不管它了,然后从 n 到 1 不断缩小 l ,更新 \(lst_i\) 。
我们在缩小 l 的时候,可以记录下 l 在当前位置时成功的数对,对于 l-1 对应的数对,我们来对它们进行更新。
假设它们的在 k 的位置,那么显然, \(l\sim k-1\) 的所有数对它都能弹掉,所以我们只需要考虑它能不能弹掉新来的,如果弹不掉就趋势。
很显然,如果它连新来的都弹不掉,也就不用想前面的了,所以 \(lst_k\) 也就一定是 l+1 了( l 时趋势意味着 l+1 是最后一个可行的位置)。
事实上,记录成功的数对应当用栈,因为如果新来的被某一个搞掉了,就肯定不能影响到后面的,而栈是可以做到这一点的。
于是,我们不断判断栈顶的可行性,不断弹栈直至栈顶满足条件即可。
最后,别忘了清空栈,并把剩下的这些数对的 \(lst\) 赋值为 1 。
这样就完成了对 \(lst\) 的处理!
然后,怎么利用呢?
\(lst_i\) 的定义换句话说就是使 i 成功的最小的 l 。
所以,我们实际要求的就是 \(lst_{l \sim r}\) 中满足 \(lst_i < l\) 的数的个数。
我们考虑将操作离线,然后把左右端点分别标记到一个数组上,然后从 \(1 \sim n\) 遍历一遍 \(lst\) 数组,然后不断插入树状数组中。
值得注意的是,这里使用值作为下标,因为我们要查的是值的个数。
中途,如果遇到左端点,那么先给对应的询问的答案减去一个 \(sum(now)\) (sum表示查询值域为 1~now 的数的个数),而且需要注意,这次查询应该在插入之前,不然可能会把 now 减掉。而为什么 sum 里面是 now 呢,这个也是显然的,因为遇到的是左端点,所以查询的值域理应在 \(1 \sim now\) 。
而如果遇到右端点呢?
我们发现,需要记录它对应的左端点,这需要我们提前处理,这里设它是 \(x\)。
而后,我们将对应的询问的答案加上一个 \(sum(x)\) ,而它肯定不会多,因为 \(1\sim x-1\) 所对应的 \(sum(x)\) 已经提前被做掉了。
之后,便可以快乐输出啦!
十年OI一场空,不删调试语句见祖宗QwQ
代码
//吾日八省吾身:
//输入多而不快读乎?
//题目标注而不freopen乎?
//乘除并列先乘后除乎?
//不手撕样例直接写代码乎?
//不仔细读题直接关页面乎?
//1e9而不开long long乎?
//Ctrl+V而不改名称乎?(papaw->papan IMPLIES tg1=->2=)
//相信评测神机乎?
#include<iostream>
#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<string>
#include<cmath>
#include<iomanip>
#include<cctype>
#include<vector>
#include<stack>
#include<queue>
#include<map>
#include<set>
#include<algorithm>
#include<utility>
#include<deque>
#include<ctime>
#include<sstream>
#include<list>
#include<bitset>
using namespace std;
typedef long long ll;
const ll MAXN(500233);
ll tree[MAXN],n,q,lst[MAXN];
ll stk[MAXN],cnt;
ll ans[MAXN];
struct twt{
ll a,b;
}tt[MAXN];
vector<ll> ls[MAXN];
vector<ll> rs[MAXN];
ll getl[MAXN];//只和l有关,我们需要知道l是多少
ll lowbit(ll x){return x&(-x);}
void add(ll x,ll num){
while(x<=n){
tree[x]+=num;
x+=lowbit(x);
}
return;
}
ll sum(ll x){
ll suu=0;
while(x>=1){
suu+=tree[x];
x-=lowbit(x);
}
return suu;
}
void R(ll &x){
x=0;ll f=1;char c='c';
while(c>'9'||c<'0'){
f=f*(c=='-'?-1:1);
c=getchar();
}
while(c<='9'&&c>='0'){
x=x*10+c-'0';
c=getchar();
}
return;
}
int main(){
R(n);R(q);
for(int i=1;i<=n;++i) R(tt[i].a);
for(int i=1;i<=n;++i) R(tt[i].b);
for(int i=n;i>=1;--i){
while(((tt[i].a!=tt[stk[cnt]].a)&&(tt[i].b>tt[stk[cnt]].b))&&cnt){
lst[stk[cnt]]=i+1;//i的时候挂了说明i+1是最后还行的了
cnt--;
}
stk[++cnt]=i;
}
while(cnt){lst[stk[cnt]]=1;cnt--;}//如果这样还能成功,就意味着l可以取1
for(int i=1;i<=q;++i){
ll l,r;
R(l);R(r);
ls[l].push_back(i);
rs[r].push_back(i);
getl[i]=l;//i号询问的左端点
}
for(int i=1;i<=n;++i){
for(auto j=ls[i].begin();j!=ls[i].end();++j) ans[*j]-=sum(i);//现在的sum_i是1~i-1范围内≤i的数的个数
add(lst[i],1);
for(auto j=rs[i].begin();j!=rs[i].end();++j) ans[*j]+=sum(getl[*j]);//查询1~i范围内≤i对应的左端点的下标的数的个数
}
for(int i=1;i<=q;++i) printf("%lld\n",ans[i]);
return 0;
}