参考:算法学习(二)——树状数组求逆序数 、线段树或树状数组求逆序数(附例题)
应用树状数组 || 线段树求逆序数是一种很巧妙的技巧,这个技巧的关键在于如何把原来单纯的求区间和操作转换为 求小于等于a的数的总数 再转换为 求序列里大于a的数的总数,学习这个技巧源于一道题目 poj 3067 Japan (一道需要YY后运用这个技巧求解的题目),此外这个技巧也让我联想到 树状数组区间加/单点求值的技巧(基于区间加法的思维),话不多说,开始正题。
一、什么是逆序数?
在一个排列中,如果一对数的前后位置与大小顺序相反,即前面的数大于后面的数,那么它们就称为一个逆序。一个排列中逆序的总数就称为这个排列的逆序数。一个排列中所有逆序总数叫做这个排列的逆序数。也就是说,对于n个不同的元素,先规定各元素之间有一个标准次序(例如n个 不同的自然数,可规定从小到大为标准次序),于是在这n个元素的任一排列中,当某两个元素的先后次序与标准次序不同时,就说有1个逆序。一个排列中所有逆序总数叫做这个排列的逆序数。
举个栗子:如2431中,21,43,41,31是逆序,逆序数是4。
二、如何用树状数组求逆序数?
1、定义: a[ i ] 储存原始序列(事实上只需要一个变量 a 就ok了), i 即 出现的顺序;c[ k ] 树状数组 k 为 a[ i ]。
2、步骤:按顺序输入储存原始序列 a[ i ] , 输入的同时维护 c[ i ] , a[i] 出现 c[ a[i] ] 则加一, add(a[ i ], 1);所以用数组数组求(1, a[i]) 的区间和的意义就变成了统计小于等于 a[ i ] 的数的个数, 若当前序列总数为 N, 则 N - sum( a ) 就是长度为N的序列中比 a 大的数字的总数了。
3、举个栗子:2431 ans = 0 + 0 + 1 + 3 = 4; 分别是(21, 43, 41, 31)。
i | a[ i ] | k | c[ 1 ] ~ c[ 4 ] | i - sum( k ) |
1 | 2 | 2 | 0 1 0 0 | 1 - 1 = 0 |
2 | 4 | 4 | 0 1 0 1 | 2 - 2 = 0 |
3 | 3 | 3 | 0 1 1 1 | 3 - 2 = 1 |
4 | 1 | 1 | 1 1 1 1 | 4 - 1 = 3 |
4、贴个代码:
1 ///树状数组求逆序数 2 #include <cstdio> 3 #include <cstring> 4 #include <iostream> 5 #include <algorithm> 6 using namespace std; 7 8 const int MAXN = 1001; 9 int c[MAXN]; 10 int N; 11 12 int lowbit(int x) ///实现树状数组需要的基本函数 13 { 14 return x&(-x); 15 } 16 17 void add(int i, int value) 18 { 19 while(i <= N) 20 { 21 c[i]+=value; 22 i+=lowbit(i); 23 } 24 } 25 26 int sum(int i) 27 { 28 int res = 0; 29 while(i > 0) 30 { 31 res+=c[i]; 32 i-=lowbit(i); 33 } 34 return res; 35 } 36 int main() 37 { 38 while(~scanf("%d", &N)) 39 { 40 int ans = 0; 41 memset(c, 0, sizeof(c)); 42 for(int i = 1; i <= N; i++) 43 { 44 int a; 45 scanf("%d", &a); 46 add(a, 1); 47 ans+=i-sum(a); 48 } 49 printf("%d\n", ans); 50 } 51 return 0; 52 }
三、如何用线段树求逆序数
与树状数组原理,只不过实现的区间求和的方式不同罢了
直接贴代码:
#include <iostream> #include <cstdio> #include <cstring> #define L(a) a<<1 #define R(a) (a<<1)|1 const int maxn = 51000; int ans[maxn]; struct node{ int num,l,r; }tree[maxn<<2]; int n; void Build(int m,int l, int r){ tree[m].l=l; tree[m].r=r; if(tree[m].l==tree[m].r){ tree[m].num=0; return ; } int mid = (tree[m].l+tree[m].r)>>1; Build(L(m),l,mid); Build(R(m),mid+1,r); //并不要回溯, 建立空树 } void Insert(int m,int l,int r,int x){ if(tree[m].l==l&&tree[m].r==r){ tree[m].num+=x; return ; } int mid = (tree[m].l+tree[m].r)>>1; if(r<=mid) Insert(L(m),l,r,x); else if(l>mid) Insert(R(m),l,r,x); else{ Insert(L(m),l,mid,x); Insert(R(m),mid+1,r,x); } tree[m].num=tree[L(m)].num+tree[R(m)].num; } int Query(int m,int l,int r){ if(tree[m].l==l&&tree[m].r==r) return tree[m].num; int mid = (tree[m].l+tree[m].r)>>1; if(r<=mid) return Query(L(m),l,r); if(l>mid) return Query(R(m),l,r); return Query(L(m),l,mid)+Query(R(m),mid+1,r); } int main(){ int a,n,i,t; int k=0; scanf("%d",&n); memset(tree,0,sizeof(tree)); Build(1,1,n); for(int i=1;i<=n;i++) { scanf("%d",&ans[i]); } for(int i=1;i<=n;i++){ Insert(1,ans[i],ans[i],1);// 每个位置插入1 k+=(i - Query(1,1,ans[i])); } printf("%d\n",k); return 0; }
四、几道题目
1、HDU 1394
思路:树状数组+暴力(每次a[ i ] 掉到最后的时候 减去比 a[ i ] 小的数, 加上比 a[ i ] 大的数)
Ac Code:
1 ///HDU 1394 树状数组 2 3 #include <cstdio> 4 #include <cstring> 5 #include <iostream> 6 #include <algorithm> 7 #define INF 0x3f3f3f3f 8 using namespace std; 9 10 const int MAXN = 5005; 11 12 int N; 13 int c[MAXN], a[MAXN]; 14 15 int lowbit(int x) 16 { 17 return x&(-x); 18 } 19 20 void add(int i, int value) 21 { 22 while(i <= N) 23 { 24 c[i]+=value; 25 i+=lowbit(i); 26 } 27 } 28 29 int sum(int i) 30 { 31 int res = 0; 32 while(i > 0) 33 { 34 res+=c[i]; 35 i-=lowbit(i); 36 } 37 //printf("%d\n" ,res); 38 return res; 39 } 40 41 int main() 42 { 43 int cnt = 0; 44 while(~scanf("%d", &N)) 45 { 46 cnt = 0; 47 memset(c, 0, sizeof(c)); 48 memset(a, 0, sizeof(a)); 49 for(int i = 1; i <= N; i++) 50 { 51 scanf("%d", &a[i]); 52 a[i]++; 53 add(a[i], 1); 54 cnt+=i-sum(a[i]); 55 } 56 57 int ans = cnt; 58 //printf("%d\n", ans); 59 for(int i = 1; i <= N; i++) 60 { 61 a[i]--; 62 cnt = cnt-a[i]*2+N-1; 63 ans = min(ans, cnt); 64 //printf("%d\n", ans); 65 } 66 printf("%d\n", ans); 67 } 68 return 0; 69 }