P11390 题解
这道题还挺有意思,需要一步一步分析下来,最后才能发现正解算法。那我们可以来看点部分分想想正解是怎么来的。
子任务一
一个暴力就可以解决的问题。
具体的,我们枚举每一个区间,开一个
for(int l=1;l<=n;l++){
fill(cnt+1,cnt+n+1,0);
cnt1=cnt2=cnt3=cnt4=0;
for(int r=l,x;r<=n;r++){
x=cnt[a[r]];
if(x==0)cnt1++;
else if(x==1)cnt1--,cnt2++;
else if(x==2)cnt2--,cnt3++;
else if(x==3)cnt3--,cnt4++;
else if(x==4)cnt4--;
cnt[a[r]]++;
if(k>=1&&!cnt1)continue;
if(k>=2&&!cnt2)continue;
if(k>=3&&!cnt3)continue;
if(k>=4&&!cnt4)continue;
ans++;
}
}
子任务二
我们可以凭借敏锐的直觉感受到合法区间的长度应该是
那这部分可以使用一个双指针来解决。
//这部分的代码着实有点抽象了,有种复沓的美
len=k*(1+k)/2;
for(int i=1,x;i<=len;i++){
x=cnt[a[i]];
if(x==0)cnt1++;
else if(x==1)cnt1--,cnt2++;
else if(x==2)cnt2--,cnt3++;
else if(x==3)cnt3--,cnt4++;
else if(x==4)cnt4--;
cnt[a[i]]++;
}
if(k==1&&cnt1)ans++;
else if(k==2&&cnt1&&cnt2)ans++;
else if(k==3&&cnt1&&cnt2&&cnt3)ans++;
else if(k==4&&cnt1&&cnt2&&cnt3&&cnt4)ans++;
for(int r=len+1,x;r<=n;r++){
x=cnt[a[r-len]];
if(x==1)cnt1--;
else if(x==2)cnt2--,cnt1++;
else if(x==3)cnt3--,cnt2++;
else if(x==4)cnt4--,cnt3++;
else if(x==5)cnt4++;
cnt[a[r-len]]--;
x=cnt[a[r]];
if(x==0)cnt1++;
else if(x==1)cnt1--,cnt2++;
else if(x==2)cnt2--,cnt3++;
else if(x==3)cnt3--,cnt4++;
else if(x==4)cnt4--;
cnt[a[r]]++;
if(k==1&&cnt1)ans++;
else if(k==2&&cnt1&&cnt2)ans++;
else if(k==3&&cnt1&&cnt2&&cnt3)ans++;
else if(k==4&&cnt1&&cnt2&&cnt3&&cnt4)ans++;
}
write(ans),Nxt;
子任务三
这是最核心的一个子任务,因为它直接提示了我们正解的做法。
我们考虑
那这就变成了一个区间加的问题了,具体的,我们记
这显然可以使用一个线段树进行优化。
/*线段树代码*/
struct Node{
int mi,cnt;
Node friend operator +(Node a,Node b){
Node res;
res.mi=min(a.mi,b.mi);
res.cnt=0;
if(res.mi==a.mi)res.cnt+=a.cnt;
if(res.mi==b.mi)res.cnt+=b.cnt;
return res;
}
Node operator +(const int &a)const{
return {mi+a,cnt};
}
};
struct Segment_tree{
Node c[N<<2];
int tag[N<<2];
#define ls p<<1
#define rs p<<1|1
#define mid (l+r>>1)
void pushup(int p){c[p]=c[ls]+c[rs];}
void Tag(int p,int v){
c[p]=c[p]+v;
tag[p]+=v;
}
void pushdown(int p){
if(!tag[p])return ;
Tag(ls,tag[p]);
Tag(rs,tag[p]);
tag[p]=0;
}
void build(int p,int l,int r){
if(l==r)return void(c[p]={0,1});
build(ls,l,mid),build(rs,mid+1,r);
pushup(p);
}
void change(int p,int l,int r,int L,int R,int v){
if(L<=l&&r<=R)return Tag(p,v);
pushdown(p);
if(mid>=L)change(ls,l,mid,L,R,v);
if(mid<R)change(rs,mid+1,r,L,R,v);
pushup(p);
}
int query(){
if(c[1].mi==0)return n-c[1].cnt;
return n;
}
}
/*主函数*/
for(int i=1;i<=n;i++){
pre[i]=las[a[i]];
las[a[i]]=i;
}
Set.build(1,1,n);
for(int i=1;i<=n;i++){
Set.change(1,1,n,pre[i]+1,i,1);
if(pre[i])Set.change(1,1,n,pre[pre[i]]+1,pre[i],-1);
ans+=Set.query();
//查询 1~n 中有多少个不为 0 的节点,和查询 1~i 中有多少个不为 0 的节点是等价的,因为 i+1~n 显然为 0
}
write(ans);
4.正解
我们回忆一下子任务三的求解过程,可以分为两步。第一步,区间修改;第二步,求有多少个不为
看着着实有点眼熟。
这不是求矩形面积并的过程吗?
我们将右端点视作矩形的宽,左端点视作矩形的长,就可以很明显的发现这个结论。
我们沿着这个思路想下去,会发现这样一件事:最后的答案其实就是求区间中拥有恰好出现一次,两次,三次,四次的合法区间各自的矩形面积并求交的答案。
但是求交着实不太好求,所以我们做一点容斥,把交搞成多个并相减的形式即可。
/*线段树的代码和上面是一样的,这里就不放了*/
for(int st=1,tp;st<(1<<k);st++){
Set.build(1,1,n);
tp=(cnt[st]&1)?1:-1;//容斥原理,奇加偶减,cnt[st] 表示 st 在二进制下有几个 1
for(int i=1,now;i<=n;i++){
now=i;
for(int j=0;j<k;j++){
if(st&(1<<j)){
Set.change(1,1,n,pre[now]+1,now,1);
if(pre[pre[now]]<pre[now])Set.change(1,1,n,pre[pre[now]]+1,pre[now],-1);
}
now=pre[now];
if(!now)break;
}
ans+=tp*(Set.query());
}
}