先放题面:
A. 签到题
题目描述
你是城市的顶级信息分析师,负责监管全城的信息高速公路——一条长达 n 个信息节点的“数据高速公路”。 每个信息节点有一个编号 a[i],表示信息的优先级: 编号越小,信息越重要; 编号越大,信息越“迟缓”。 在信息高速公路中,如果一个重要信息被比他迟缓信息挡在后面,就会造成信息拥堵,混乱程度+1。 你的任务是分析高速公路的各段区间,计算混乱程度。
输入
系统给你一条长度为 n 的信息序列 a[1..n],表示一条从前往后的序列。 你会收到 m 个任务,每个任务给出一个区间 [l, r],你需要快速计算该区间内的混乱程度。 混乱程度越高,说明这一段信息高速公路越拥堵,优化的需求越迫切。
输出
输出 m 行,每行一个整数表示这次询问的答案。
m,n<=1e5,a[i]<=1e9
解析:
读题后易得:本题求的是区间逆序对个数并且没有强制在线。
那么我们就开始从暴力慢慢优化吧:
最暴力的算法
对于每次询问,从l
遍历到r
对于每个数在for
一遍求逆序对。复杂度 O(m*n*n)
这要是过了那么说明出数据的人很认真的在降低难度捏( ̄▽ ̄)*
考虑优化1
树状数组统计逆序对个数。
树状数组求的是前缀和,如果a[i]
数组是 1~n 的排列的话,如果开一个树状数组t[i]
,每次对于一个新数a[i]
,用树状数组的算法,每次统计,Add(a[i],1)
,那么我们查询的时候query(x)
就是查询到小于等于x
的所有数字出现总次数,对于每个r
我们都预处理 1~r 间的查询值。区间右边界查询的答案减去左边界的查询答案就得到了这个数对逆序对的贡献。
这样的话复杂度可以降到 O(n*m)
inline ll Lowbit(ll x) {return x&(-x);
}
inline void Add(ll x,ll k) {for(; x<=cnt; x+=Lowbit(x)) {t[x]+=k;}return ;
}
inline ll query(ll x) {ll ans=0;for(; x>0; x-=Lowbit(x)) {ans+=t[x];}return ans;
}
考虑优化2
区间询问不强制在线(离线)————很容易想到莫队。
离线算法就是所有询问不会在输入的时候强制要求给答案,允许集中处理询问。而莫队就是一种优雅的指针使用方法,针对区间问题的对策卡。
首先分块给每个块打上标记0(n-1)或者1n,将每个区间的左端点所属块记录:
给区间排序sort+cmp:第一步先按排区间左端点所在块的大小排;块相同进入第二步,按照右端点的大小排。
但是这里有一个小优化———— 奇偶块优化 。对于编号为奇数的块,右端点升序排列,编号为偶数的块右端点按照降序排。具体为什么后面会说。
inline bool cmp(Qruey_ a,Qruey_ b) {ll sa=a.l/sQrt, sb=b.l/sQrt;if(sa!=sb)return sa<sb;return (sa%2)?(a.r>b.r):(a.r<b.r);
}
定义指针l
,r
,分别代表当前计算的区间左端点和右端点。每次遍历区间都是从上一个区间开始向新的区间一个个扩展,直到相等并且在扩展的同时更新答案。最后当区间相等时答案就已经统计好了。那么说到奇偶块优化,每次指针跳完当前块后,右指针就会处于最右端,而如果下一个块也是升序右端点那么右指针r
就会先跳回最左端然后再向右跳到最右端。但是如果下一个块的右端点按照降序排列那么每次r
跳完之后就可以直接往回走,或者向右走一小段再往回走。大大地降低了常数。
既然我们要模拟跳指针的过程我们就需要 四个函数 分别是扩张左右指针和收缩左右指针,最难的地方就是小函数内各种操作的顺序。
这里我们的树状数组存储的就不再是1~x内所有数的前缀和了,而是只会把在我们当前扩展区间内的数统计出现次数
(注:lt是左指针rt是右指针,ll是long long,a数组是离散化后的排列,query和add是树状数组的函数)
壹:扩张左指针
inline void Add_l(ll &g) {--lt;ll x=a[lt];g+=query(x-1);Add(x,1);return ;
}
左指针扩张,计算顺序如何呢?
由于我们加入了一个新的数,我们需要先将指针向左扩展一位,此时的左指针就指向了新加入的数,然后我们需要统计这个数字的加入对整个区间逆序对的贡献————这个数在最左端,那么贡献就是它右边所有小于它的数,不难发现,它右边所有在区间的数其实都是已经在树状数组内的,那么查询树状数组内小于x的数字的出现次数。然后将这个新加入的数字统计到树状数组内。
贰:收缩左指针
我们要将左指针右移,这个时候需要减去它的贡献但是不能先移动指针,因为指针指向的当前数就是我们需要删除的那个数。直接统计删去后对整个区间的影响。需要先将查询区间内小于当前数的出现次数,统计答案减去查询值,最后在移动指针。
注意我们的指针指向的元素都是我们当前要操作的那个元素,这样会方便一点,不然的话会比较麻烦
inline void Del_l(ll &g) {ll x=a[lt];Add(x,-1);g-=query(x-1);++lt;return ;
}
#include<bits/stdc++.h>
#define MAXN 100005
#define ll long long
using namespace std;ll a[MAXN],b[MAXN];
ll t[MAXN*2];
ll n,m,cnt;
ll sQrt;ll Ans[MAXN];struct Qruey_ {ll l,r,id;
} q[MAXN];ll lt=1,rt=0;
ll sum=0;inline void read(ll &x) {char c=getchar();bool f=0;x=0;while(!isdigit(c)) {f|=(c=='-');c=getchar();}while(isdigit(c)) {x=(x<<3)+(x<<1)+(c^48);c=getchar();}x=f?(-x):x;return ;
}
//////////////////////////////////////树
inline ll Lowbit(ll x) {return x&(-x);
}
inline void Add(ll x,ll k) {for(; x<=cnt; x+=Lowbit(x)) {t[x]+=k;}return ;
}
inline ll query(ll x) {ll ans=0;for(; x>0; x-=Lowbit(x)) {ans+=t[x];}return ans;
}inline bool cmp(Qruey_ a,Qruey_ b) {ll sa=a.l/sQrt, sb=b.l/sQrt;if(sa!=sb)return sa<sb;return (sa%2)?(a.r>b.r):(a.r<b.r);
}////////////////////////////////////////莫队
inline void Add_l(ll &g) {--lt;ll x=a[lt];g+=query(x-1);Add(x,1);return ;
}
inline void Del_l(ll &g) {ll x=a[lt];Add(x,-1);g-=query(x-1);++lt;return ;
}inline void Add_r(ll &g) {++rt;ll x=a[rt];g+=query(cnt)-query(x);Add(x,1);return ;
}
inline void Del_r(ll &g) {ll x=a[rt];Add(x,-1);g-=query(cnt)-query(x);--rt;return ;
}int main() {read(n),read(m);for(ll i=1; i<=n; ++i) {read(a[i]);b[i]=a[i];}sort(b+1,b+1+n);cnt=unique(b+1,b+1+n)-(b+1);for(ll i=1; i<=n; ++i) {a[i]=lower_bound(b+1,b+1+cnt,a[i])-b;}for(ll i=1; i<=m; ++i) {read(q[i].l),read(q[i].r);q[i].id=i;}sQrt=(ll)sqrt(n)+1;sort(q+1,q+1+m,cmp);memset(t,0,sizeof(t));for(ll i=1; i<=m; ++i) {ll L=q[i].l, R=q[i].r;while(lt>L) {Add_l(sum);}while(lt<L) {Del_l(sum);}while(rt<R) {Add_r(sum);}while(rt>R) {Del_r(sum);}Ans[q[i].id]=sum;}for(ll i=1; i<=m; ++i) {printf("%lld\n",Ans[i]);}return 0;
}