逆序对 线段树&树状数组 (重制版)
逆序对的定义:长度为n的数组a,求满足i<j时a[i]>a[j]条件的数对个数。
第一次接触这种问题的人可能是更先想到的是n^2去暴力数前面有几个比他大的数。
1 int main() 2 { 3 int n; 4 while(~scanf("%d", &n), n) { 5 ans = 0; 6 for(int i = 1; i <= n; i++) 7 scanf("%d", &a[i]); 8 for(int i = 1; i <= n; i+=2) 9 for(int j = 1; j < i; j+=2) 10 if(a[j] > a[i]) ans++; 11 printf("%d", ans); 12 } 13 return 0; 14 }
n^2算法就是数一下前面有多少个数比现在这个数大 这样全部跑完只后就是逆序数了。
其中重点是 前面有多少个数比现在这个数大
但是每次从1 fo r一遍到i的位置太浪费时间了
所以我们可以用线段树来优化这个数数过程
1 #include<bits/stdc++.h> 2 using namespace std; 3 #define lson l,m,rt<<1 4 #define rson m+1,r,rt<<1|1 5 #define LL long long 6 const int N=100005; 7 int sum[N<<2], a[N]; 8 LL ans; 9 void Update(int c, int l, int r,int rt) { 10 if(l == r) { 11 sum[rt]++; 12 return; 13 } 14 int m = l+r >> 1; 15 if(c <= m) Update(c,lson); 16 else Update(c,rson); 17 sum[rt]=sum[rt<<1]+sum[rt<<1|1]; 18 } 19 LL Query(int L, int R, int l, int r, int rt) { 20 if(L <= l && r <= R) 21 return sum[rt]; 22 int m = l+r >> 1; 23 LL cnt = 0; 24 if(L <= m) cnt+=Query(L,R,lson); 25 if(m < R) cnt += Query(L,R,rson); 26 return cnt; 27 } 28 int main() { 29 int n; 30 while(~scanf("%d", &n), n){ 31 ans = 0; 32 memset(sum, 0, sizeof(sum)); 33 for(int i = 1; i <= n; i++){ 34 scanf("%d", &a[i]); 35 } 36 for(int i = 1; i <= n; i++) { 37 ans += Query(a[i],n,1,n,1); 38 Update(a[i],1,n,1); 39 } 40 printf("%d\n", ans); 41 } 42 return 0; 43 }
线段树算法的在求逆序对的关键就是将出现过的数对应的位置标记一下(+1)
假设 i=k时, 查询一下区间 [a[k], n] 的区间和, 这个和就是(j < k && a[j] > a[k]) 的数目
然后在a[k] 的位置 +1
重复这个过程就能求出解了
是不是很疑惑为什么?
当查询区间的时候, 如果在后面的区间内查询到次数不为0时, 说明有几个比他大数在他前面出现过,
重点就是标记。
这里还可以用树状数组来代替线段树。树状数组的特点就是好写,并且速度比线段树快一点。
1 #include<algorithm> 2 #include<cstdio> 3 #include<cstring> 4 using namespace std; 5 #define Fopen freopen("_in.txt","r",stdin); freopen("_out.txt","w",stdout); 6 #define LL long long 7 #define ULL unsigned LL 8 #define fi first 9 #define se second 10 #define pb push_back 11 #define lson l,m,rt<<1 12 #define rson m+1,r,rt<<1|1 13 #define max3(a,b,c) max(a,max(b,c)) 14 #define min3(a,b,c) min(a,min(b,c))amespace std; 15 const int N = 500000+5; 16 int tree[N], cnt = 0, A[N]; 17 pair<int, int> P[N]; 18 int lowbit(int x){ 19 return x&(-x); 20 } 21 void Add(int x) { 22 while(x <= cnt) { 23 tree[x]++; 24 x += lowbit(x); 25 } 26 } 27 int Query(int x) { 28 int ret = 0; 29 while(x) { 30 ret += tree[x]; 31 x -= lowbit(x); 32 } 33 return ret; 34 } 35 int main(){ 36 int n; 37 while(~scanf("%d", &n), n) { 38 cnt = 0; 39 for(int i = 1; i <= n; i++) { 40 scanf("%d", &A[i]); 41 } 42 memset(tree, 0, sizeof(tree)); 43 LL ans = 0; 44 for(int i = 1; i <= n; i++) { 45 ans += Query(cnt) - Query(A[i]); 46 Add(A[i]); 47 } 48 printf("%I64d\n", ans); 49 } 50 return 0; 51 }
注意的是 线段树与树状数组求逆序对的时候 数值不能太大 比如在 a[i] <= 1e9的时候 就不能直接用树状数组和逆序数去求了,因为开不了那么大的空间。
但在这个时候,如果n不是很大可以先对数据进行离散化再进行使用树状数组或者线段树处理数据。
题意就是求逆序对, 但是这个a[i]的范围太大,所以我们不能直接进行求解,但是这个地方只于数的相对大小有关,对于数的差值无关,所以我们可以先对所有数离散化,再进行上述的操作。
1 #include<algorithm> 2 #include<cstdio> 3 #include<cstring> 4 using namespace std; 5 #define Fopen freopen("_in.txt","r",stdin); freopen("_out.txt","w",stdout); 6 #define LL long long 7 #define ULL unsigned LL 8 #define fi first 9 #define se second 10 #define pb push_back 11 #define lson l,m,rt<<1 12 #define rson m+1,r,rt<<1|1 13 #define max3(a,b,c) max(a,max(b,c)) 14 #define min3(a,b,c) min(a,min(b,c))amespace std; 15 const int N = 500000+5; 16 int tree[N], cnt = 0, A[N]; 17 pair<int, int> P[N]; 18 int lowbit(int x){ 19 return x&(-x); 20 } 21 void Add(int x) { 22 while(x <= cnt) { 23 tree[x]++; 24 x += lowbit(x); 25 } 26 } 27 int Query(int x) { 28 int ret = 0; 29 while(x) { 30 ret += tree[x]; 31 x -= lowbit(x); 32 } 33 return ret; 34 } 35 int main(){ 36 int n; 37 while(~scanf("%d", &n), n) { 38 cnt = 0; 39 for(int i = 1; i <= n; i++) { 40 scanf("%d", &A[i]); 41 P[i].fi = A[i]; 42 P[i].se = i; 43 } 44 sort(P+1, P+1+n); 45 P[0].fi = -1; 46 for(int i = 1; i <= n; i++) { 47 if(P[i-1].fi == P[i].fi) 48 A[P[i].se] = cnt; 49 else cnt++, A[P[i].se] = cnt; 50 } 51 memset(tree, 0, sizeof(tree)); 52 LL ans = 0; 53 for(int i = 1; i <= n; i++) { 54 ans += Query(cnt) - Query(A[i]); 55 Add(A[i]); 56 } 57 printf("%I64d\n", ans); 58 } 59 return 0; 60 }
Emmm, 上面的都是去年写的,最近突然发现写逆序对不需要那么麻烦, 所以我就再新加一个做法,本来是想删掉的,后来想想各有优点,并且,上面那个博客是最第一个写的这么详细的博客,本来是当时自己太捞了,写给自己看的,2333。
重温一下逆序对的定义, 就是对于每一个数都求出在他前面有多少个数比他大,然后对每一个数的这个东西求和,最后的值就是逆序对数了。
第二种做法就是:讲元素从大到小sort,如果相同的话,位置大的排前面,然后按照这个顺序查询前面的位置有多少个位置的数出现过,然后加到答案里, 然后再标记一下这个位置,以便下次询问。
由于我们是向前询问多少个数出现过了, 并且逆序对的定义是不包括相等的数的,所以我们要前处理位置再后面的数,再处理在前面的数。
原理就是,先出现的数必然大,所以在你前面标记过位置的数就一定大于当前的数。
还是上面那个题目,这个时候我们就不需要离散化了。
1 #include<algorithm> 2 #include<iostream> 3 #include<cstdio> 4 #include<cstring> 5 using namespace std; 6 #define Fopen freopen("_in.txt","r",stdin); freopen("_out.txt","w",stdout); 7 #define LL long long 8 #define ULL unsigned LL 9 #define fi first 10 #define se second 11 #define pb push_back 12 #define lson l,m,rt<<1 13 #define rson m+1,r,rt<<1|1 14 #define max3(a,b,c) max(a,max(b,c)) 15 #define min3(a,b,c) min(a,min(b,c))amespace std; 16 const int N = 500000+5; 17 int tree[N], n; 18 struct Node{ 19 int a; 20 int id; 21 }A[N]; 22 bool cmp(Node x1, Node x2){ 23 if(x1.a == x2.a) return x1.id > x2.id; 24 return x1.a > x2.a; 25 } 26 int lowbit(int x){ 27 return x&(-x); 28 } 29 void Add(int x) { 30 while(x <= n) { 31 tree[x]++; 32 x += lowbit(x); 33 } 34 } 35 int Query(int x) { 36 int ret = 0; 37 while(x) { 38 ret += tree[x]; 39 x -= lowbit(x); 40 } 41 return ret; 42 } 43 int main(){ 44 while(~scanf("%d", &n), n) { 45 for(int i = 1; i <= n; i++) { 46 scanf("%d", &A[i].a); 47 A[i].id = i; 48 } 49 memset(tree, 0, sizeof(tree)); 50 LL ans = 0; 51 sort(A+1, A+1+n, cmp); 52 for(int i = 1; i <= n; i++) { 53 ans += Query(A[i].id); 54 Add(A[i].id); 55 } 56 printf("%I64d\n", ans); 57 } 58 return 0; 59 }
结论是2个方法都有优点吧, 如果相同元素的个数多的话,离散化或者排序之后n的个数很小,那么前面那种或许会更优, 如果n不大,a[i]的值很大,我们就可以用第二种写法去解决。
当然有些题目只能第二种写法求解如:
二维树状数组的逆序数。 往里拐一下。 传送门。