【[SHOI2009]会场预约】

时间:2023-03-09 01:35:16
【[SHOI2009]会场预约】

同样是从试炼场点进来的,这是一道非常需要耐心的题

不过明明就是我太菜了,真正的大佬都是一眼秒吧

首先我们有一种比较常规的暴力思路,就是用线段树来维护区间连续子段数,而拒绝掉所有与当前区间相冲突的预约我们可以通过二分来做,来查找从最开始到这个区间的区间首第一个与区间首相同的位置,和区间尾到最后最靠后的一个与区间尾相同的位置

为什么可以二分呢,我们知道我们要找的子段是连续的,所以具有单调性,于是可以二分

由于我们在维护区间连续子段数和进行二分的时候需要做一个单点查询,所以时间复杂度是\(O(nlog^{2}n)\)

代码如下

#include<iostream>
#include<cstdio>
#include<cstring>
#define re register
#define maxn 200001
using namespace std;
int l[maxn<<2],r[maxn<<2],sum[maxn<<2],d[maxn<<2],tag[maxn<<2];
//sum[i]表示i对应区间内连续子段数
int n,m,tot;
char p[maxn];
int x[maxn],y[maxn];
void build(int x,int y,int i)
{
l[i]=x;
r[i]=y;
tag[i]=-1;
if(x==y) return;
int mid=x+y>>1;
build(x,mid,i<<1);
build(mid+1,y,i<<1|1);
}
inline void pushdown(int i)
{
if(tag[i]!=-1)
{
tag[i<<1]=tag[i];
tag[i<<1|1]=tag[i];
d[i<<1|1]=tag[i];
d[i<<1]=tag[i];
if(tag[i]) sum[i<<1|1]=sum[i<<1]=1;
else sum[i<<1|1]=sum[i<<1]=0;
tag[i]=-1;
}
}
int ask(int x,int i)
{
if(l[i]==r[i]&&l[i]==x) return d[i];
pushdown(i);
int mid=l[i]+r[i]>>1;
if(x<=mid) return ask(x,i<<1);
return ask(x,i<<1|1);
}//单点查询
void change(int x,int y,int i,int v)
{
if(x<=l[i]&&y>=r[i])
{
if(v) sum[i]=1;
else sum[i]=0;
tag[i]=v;
d[i]=v;
return;
}
pushdown(i);
int mid=l[i]+r[i]>>1;
if(y<=mid) change(x,y,i<<1,v);
else if(x>mid) change(x,y,i<<1|1,v);
else change(x,y,i<<1|1,v),change(x,y,i<<1,v);
int q1=ask(mid,1),q2=ask(mid+1,1);
if(q1==q2&&q1) sum[i]=sum[i<<1]+sum[i<<1|1]-1;
//在进行updata操作时我们要进行两次单点查询
//如果当前区间左儿子的最右点和右儿子的最左点相同且均不为0
//则说明有一个子段被分开了,sum[i]应该等于sum[i<<1]+sum[i<<1|1]-1;
else sum[i]=sum[i<<1|1]+sum[i<<1];
}
int query(int x,int y,int i)
{
if(x<=l[i]&&y>=r[i]) return sum[i];
pushdown(i);
int mid=l[i]+r[i]>>1;
if(y<=mid) return query(x,y,i<<1);
if(x>mid) return query(x,y,i<<1|1);
int q1=ask(mid,1),q2=ask(mid+1,1);
if(q1==q2&&q1) return query(x,y,i<<1)+query(x,y,i<<1|1)-1;
return query(x,y,i<<1)+query(x,y,i<<1|1);
d[i]=d[i<<1];
}
int main()
{
ios::sync_with_stdio(false);
cin>>n;
int xx,yy;
int maxx=0,minn=99999999;
for(re int i=1;i<=n;i++)
{
cin>>p[i];
if(p[i]=='A') cin>>x[i]>>y[i],maxx=max(maxx,y[i]),minn=min(minn,x[i]);
}
build(1,maxx,1);
for(re int i=1;i<=n;i++)
{
if(p[i]=='B') cout<<m-tot<<endl;
if(p[i]=='A')
{
int now=query(x[i],y[i],1);
cout<<now<<endl;
tot+=now;//tot为总撤销个数,方便进行B操作
int now1=ask(x[i],1),now2=ask(y[i],1);
int ans1,ans2;
int ll=minn,rr=x[i];
if(now1)//如果当前区间的最左点为0,那么就不需要进行二分了
{
while(ll<=rr)
{
int mid=ll+rr>>1;
if(ask(mid,1)==now1) ans1=mid,rr=mid-1;
else ll=mid+1;
}
}//查找从最开始到这个区间的区间首第一个与区间首相同的位置
if(now2)
{
ll=y[i],rr=maxx;
while(ll<=rr)
{
int mid=ll+rr>>1;
if(ask(mid,1)==now2) ans2=mid,ll=mid+1;
else rr=mid-1;
}
}//区间尾到最后最靠后的一个与区间尾相同的位置
if(now1&&x[i]!=ans1) change(ans1,x[i],1,0);
if(now2&&y[i]!=ans2) change(y[i],ans2,1,0);
change(x[i],y[i],1,++m);
}
}
return 0;
}

当然,这样写理论上的复杂度应该是\(O(nlog^{2}n)\),但是常数太大了,所以吸氧也不能拯救这份代码,这种写法就只有60

但是我们可以考虑对这份代码进行优化,尝试去掉一个\(log\)

首先我们在维护sum的时候,只用到了区间最左和最右位置的状态,所以我们完全可以再开两个数组\(lc\),\(rc\),分别维护区间最左和最右的位置的状态

而对于我们二分查找做撤销操作,我们可以对每个区间维护一下撤销这个区间时应该撤销的实际区间是哪一个,于是我们可以再开两个数组\(ll\),\(rr\)来进行维护每个区间做撤销操作时实际应该对哪个区间进行操作

这样一来我们通过来我们通过两次时间换空间,使我们的代码的时间复杂度降到了\(O(nlongn)\),而空间上尽管多开了几个数组,但内存仍绰绰有余

于是就是代码了

#include<iostream>
#include<cstring>
#include<cstdio>
#define re register
#define maxn 200001
using namespace std;
struct node
{
int a,b,c;
};
int l[maxn<<2],r[maxn<<2],ll[maxn<<2],rr[maxn<<2];
int rc[maxn<<2],lc[maxn<<2],sum[maxn<<2],tag[maxn<<2];
int n,m,tot;
char p[maxn];
int x[maxn],y[maxn];
void build(int x,int y,int i)
{
ll[i]=l[i]=x;
rr[i]=r[i]=y;
tag[i]=-1;
if(x==y) return;
int mid=x+y>>1;
build(x,mid,i<<1);
build(mid+1,y,i<<1|1);
}
inline void pushdown(int i)
{
if(tag[i]==-1) return;
if(tag[i])
{
sum[i<<1|1]=sum[i<<1]=1;
tag[i<<1|1]=tag[i<<1]=tag[i];
ll[i<<1]=ll[i];
ll[i<<1|1]=ll[i];
//左右两个儿子向左扩展的位置应该与i向左扩展的位置一致
rr[i<<1|1]=rr[i];
rr[i<<1]=rr[i];
//左右两个儿子向右扩展的位置应该与i向右扩展的位置一致
rc[i<<1]=rc[i<<1|1]=lc[i<<1]=lc[i<<1|1]=tag[i];
//更新状态
}
if(!tag[i])//撤销操作的下传标记
{
sum[i<<1|1]=sum[i<<1]=0;
tag[i<<1|1]=tag[i<<1]=0;
ll[i<<1]=l[i<<1];
ll[i<<1|1]=l[i<<1|1];
rr[i<<1]=r[i<<1];
rr[i<<1|1]=r[i<<1|1];
//由于又恢复到初始状态,不需要进行扩展,所以直接修改成区间的实际值就好
rc[i<<1]=rc[i<<1|1]=lc[i<<1]=lc[i<<1|1]=0;
}
tag[i]=-1;
}
void change(int x,int y,int i,int v)
{
if(x<=l[i]&&y>=r[i])
{
if(v)
{
sum[i]=1;
ll[i]=x;
rr[i]=y;
}else
{
sum[i]=0;
ll[i]=l[i];
rr[i]=r[i];
}
rc[i]=lc[i]=v;
tag[i]=v;
return;
}
pushdown(i);
int mid=l[i]+r[i]>>1;
if(y<=mid) change(x,y,i<<1,v);
else if(x>mid) change(x,y,i<<1|1,v);
else change(x,y,i<<1,v),change(x,y,i<<1|1,v);
if(rc[i<<1]==lc[i<<1|1]&&rc[i<<1]) sum[i]=sum[i<<1]+sum[i<<1|1]-1;//同上一份代码
else sum[i]=sum[i<<1|1]+sum[i<<1];
rc[i]=rc[i<<1|1];
lc[i]=lc[i<<1];
//最右边的状态来自于右儿子,最左边的状态来自于左儿子
ll[i]=ll[i<<1];
rr[i]=rr[i<<1|1];
//更新扩展的区间
}
node query(int x,int y,int i)
{
if(x<=l[i]&&y>=r[i]) return (node){sum[i],ll[i],rr[i]};
pushdown(i);
int mid=l[i]+r[i]>>1;
if(y<=mid) return query(x,y,i<<1);
if(x>mid) return query(x,y,i<<1|1);
node q1=query(x,y,i<<1),q2=query(x,y,i<<1|1);
if(rc[i<<1]==lc[i<<1|1]&&rc[i<<1]) return (node){q1.a+q2.a-1,min(q1.b,q2.b),max(q1.c,q2.c)};
return (node){q1.a+q2.a,min(q1.b,q2.b),max(q1.c,q2.c)};
}
int main()
{
ios::sync_with_stdio(false);
cin>>n;
int xx,yy;
int maxx=0,minn=99999999;
for(re int i=0;i<n;++i)
{
cin>>p[i];
if(p[i]=='A') cin>>x[i]>>y[i],maxx=max(maxx,y[i]),minn=min(minn,x[i]);
}
build(minn,maxx,1);
for(re int i=0;i<n;++i)
{
if(p[i]=='B') cout<<m-tot<<endl;
if(p[i]=='A')
{
node now=query(x[i],y[i],1);
cout<<now.a<<endl;
tot+=now.a;
if(now.a) change(now.b,now.c,1,0);
change(x[i],y[i],1,++m);
}
}
return 0;
}