动态逆序对
P3157 [CQOI2011] 动态逆序对
一道相当不错的数据结构综合。题目要求我们实现一个带修的逆序对查询的功能。先从静态逆序对开始思考。对于静态逆序对,显然我们可以使用树状数组
树状数组求逆序对
在值域上开一个树状数组,正序扫整个序列,每次对于当前序列中的值求一次后缀和(也就是\(sum[n]-sum[i]\))计入贡献,这同时也是这个点本身的逆序对数,然后在树状数组上单点修改当前序列中的值,对其在树状数组上的权值增加1。如果值域相当大,考虑如下处理:为原序列创建一组映射,把原序列和映射作为整体按照原序列降序排序,对于排完序后的映射序列按照上文提到的方式求一次逆序对就是答案,这实际上也就是二维偏序问题。过程类似下图:
带修逆序对
静态逆序对相当容易地解决了,则思考关于动态逆序对的解决办法。通过以上对求逆序对过程的分析,我们不难发现每个点对于逆序对总数的贡献就是它前面比它大的数的数量和它后面比它小的数的数量,那么只要我们预处理出每一个位置上的数的这两部分贡献,删除时把删除位置得到贡献消去就行。不难想到,求每个数前面比它大的数的数量就是上文提到的静态逆序对求法,直接跑一遍就行。而求一个数后面比它小的数,相当于把序列反转求正序对,所以我们倒序把所有点加入树状数组,每次求贡献时把后缀和替换为前缀和就行
但是我们考虑到,直接这样消去贡献会导致一部分点被多次删除,也就是这个点已经被删除了,但是它后面删除的某个点又统计了一次它的贡献并消去。那么怎么处理这个情况呢?
我们想到,如果我们把所有删除的点放到一个数据结构里维护,每次删除一个点时,我们检查当前位置之前有多少个已删除的点值比当前值大,当前位置后面有多少个已删除的点比当前值小,然后加回到答案中。换而言之,我们需要求\(x\in{[l,r]},x>k\)和\(x\in[l,r],x<k\)的\(x\)的个数,发现这实际上是一个变种的查询区间某个值的排名的问题,所以可以用主席树去做。但是如果单纯的硬套一个主席树容易导致复杂度爆表,因为我们需要在主席树上求前缀和,所以考虑给这个主席树套上一个树状数组,令其变成树套树,然后就可以\(log(n)\)求前缀和了。具体实现上,我们先正常写一个求区间内值为k的数的排名的函数,这个函数承担了求\(x\in[l,r],x<k\)的\(x\)的作用,然后把整个函数的逻辑反转(二分判定符号方向改变,并交换所有步骤访问的子树方向)就得到了求\(x\in{[l,r]},x>k\)的函数。对于每次删除点\(x\)的操作,消去自身贡献之后在把上述两个函数的值加回去就行
总体思路
1.接收原序列,记录每个值在原序列中的位置
2.预处理\(al\)记录原序列中每个位置的数前面比它大的数的数量,同时统计原序列的逆序对数,记为\(ans\)
3.预处理\(ar\)记录原序列中每个位置后面比它小的数的数量
4.每次删除操作之前输出一次\(ans\)
5.接收要删除的数\(x\),查询它在原序列中的位置\(p\),随后依次进行以下操作:
消去当前数贡献:\(ans=ans-al_{p}-ar_{p}\)
恢复重复统计的贡献(令\(qnxt\)为对于值k统计区间内大于k的数的个数,\(qpre\)统计区间内小于k的数的个数):\(ans=ans+qnxt([1,p-1],x)+qpre([p+1,n],x)\)
代码如下
点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define N 200010
#define lowbit(x) x&(-x)
#define ls(p) t[p].l
#define rs(p) t[p].r
#define val(p) t[p].val
/*初始化*/
int n,m;
int a[N],b[N];//b为a的映射,用以求解逆序对
int del;//每次删除的数
//主席树
struct segTree{
int l,r,val;
}t[N*100];
int rt[N],tot;
//树状数组
int tmp1[N],cnt1;
int tmp2[N],cnt2;
int c[N];//临时树状数组,用于统计al和ar,作用相当于一个桶
int al[N];//记录每个数前面比它大的数的个数
int ar[N];//记录每个数后面比它小的数的个数
//对于每个数,它对逆序对总数的贡献就是al[i]+ar[i]
int ans;//逆序对数
/*主席树模块*/
void push_up(int p){
val(p)=val(ls(p))+val(rs(p));
}
//单点修改
void modify(int &p,int l,int r,int x,int k){
if(!p) p=++tot;//缺节点就新建一个
if(l==r){
val(p)+=k;
return;
}
int mid=(l+r)>>1;
if(x<=mid) modify(ls(p),l,mid,x,k);
else modify(rs(p),mid+1,r,x,k);
push_up(p);
return;
}
/*树状数组模块*/
//插入
void insert(int x,int k){
for(int i=x;i<=n;i+=lowbit(i)){
modify(rt[i],1,n,a[x],k);
}
}
//查询区间内值大于k的数的个数
int qnxt(int x,int y,int k){
int cnt1=0,cnt2=0;
int res=0;
for(int i=x-1;i;i-=lowbit(i)) tmp1[++cnt1]=rt[i];
for(int i=y;i;i-=lowbit(i)) tmp2[++cnt2]=rt[i];
int l=1,r=n;
while(l<r){
int mid=(l+r)>>1;
if(k<=mid){
for(int i=1;i<=cnt1;++i) res-=val(rs(tmp1[i]));
for(int i=1;i<=cnt2;++i) res+=val(rs(tmp2[i]));
for(int i=1;i<=cnt1;++i) tmp1[i]=ls(tmp1[i]);
for(int i=1;i<=cnt2;++i) tmp2[i]=ls(tmp2[i]);
r=mid;
}
else{
for(int i=1;i<=cnt1;++i) tmp1[i]=rs(tmp1[i]);
for(int i=1;i<=cnt2;++i) tmp2[i]=rs(tmp2[i]);
l=mid+1;
}
}
return res;
}
//查询区间内小于k的数的个数
int qpre(int x,int y,int k){
int cnt1=0,cnt2=0;
int res=0;
for(int i=x-1;i;i-=lowbit(i)) tmp1[++cnt1]=rt[i];
for(int i=y;i;i-=lowbit(i)) tmp2[++cnt2]=rt[i];
int l=1,r=n;
while(l<r){
int mid=(l+r)>>1;
if(k>mid){
for(int i=1;i<=cnt1;++i) res-=val(ls(tmp1[i]));
for(int i=1;i<=cnt2;++i) res+=val(ls(tmp2[i]));
for(int i=1;i<=cnt1;++i) tmp1[i]=rs(tmp1[i]);
for(int i=1;i<=cnt2;++i) tmp2[i]=rs(tmp2[i]);
l=mid+1;
}
else{
for(int i=1;i<=cnt1;++i) tmp1[i]=ls(tmp1[i]);
for(int i=1;i<=cnt2;++i) tmp2[i]=ls(tmp2[i]);
r=mid;
}
}
return res;
}
/*预处理模块*/
//查询前缀和
void add(int x,int k){
while(x<=n){
c[x]+=k;
x+=lowbit(x);}
return;
}
//求和
int ask(int x){
int sum=0;
while(x!=0){
sum+=c[x];
x-=lowbit(x);
}
return sum;
}
signed main(){
ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
cin>>n>>m;
for(int i=1;i<=n;i++){
cin>>a[i];
b[a[i]]=i;//记录映射
}
//处理al和ar
/*处理al时,正序把所有数放进树状数组。此时树状数组统计值域上对于
每个数出现的次数。对于al,al[i]实际上等于树状数组上n到a[i]的数的
个数,也就是前缀和,也等效于a[i]的逆序对数*/
for(int i=1;i<=n;i++){
al[i]=ask(n)-ask(a[i]);
ans+=al[i];//记录初始逆序对数
add(a[i],1);//更新树状数组
}
/*处理ar时同理,但是改为倒序把所有数放进树状数组,查询时差a[i]-1
到数组开头的前缀和*///记得要清空
memset(c,0,sizeof(c));
for(int i=n;i>=1;i--){
ar[i]=ask(a[i]-1);
add(a[i],1);
}
//处理询问
for(int i=1;i<=m;i++){
cout<<ans<<"\n";
cin>>del;
del=b[del];
ans-=(al[del]+ar[del]);//消去删除部分的贡献
//重新计算被重复删除的部分的贡献
ans+=(qnxt(1,del-1,a[del])+qpre(del+1,n,a[del]));
//新删除的节点计入树状数组
insert(del,1);
}
return 0;
}