hdu 1394 Minimum Inversion Number
线段树 or 树状数组 or 归并排序
一个关于逆序数的问题。输入n,下面给出n个数字,分别是0到n-1,顺序不定。问你逆序数是多少?
逆序数就是逆序对的个数,所谓逆序对,就是一个序列中任取两个数字,若i<j,a[i]>a[j],则称a[i]和a[j]是一个逆序对,好像3,1,4,2,逆序对有(3,1),(3,2),(4,2),所以逆序数为3
但题目还没完,它还需要移动,每次将序列的第一个元素放在序列的最后,得到的新序列再求一次逆序数,可知一个长度为n的序列这样的移动可以做n-1次,可以得到n-1个新的序列,(第n次的话就会变回原始的序列)。然后问你,原序列和n-1个新序列中最小的逆序数是多少,然后输出。
1.这题的特殊性方便了我们去寻找最小值。现在假设我们知道了原序列的逆序数是first,那么每次移动后怎么算出新的逆序数呢?因为每次都只是移动头元素,假设头元素为x,那么可以知道由x产生的逆序对的个数为x,因为有x个数小于它(0,1,2……x-1),如果将它放到了末尾,那么这x个逆序对将会消失。同样地,整个序列中有(n-1-x)个元素大于x,那么x移到了末尾,将产生(n-1-x)个新的逆序对(这些逆序对分别为(x+1,x),(x+2,x),(x+3,x)……(n-1,x))。因此可以递推地解决这个问题。如果知道了当前序列逆序数为sum,那么移动头元素后的逆序数将会是sum-x+(n-1-x)
2.那么接下来的问题就是怎么求原序列的逆序数,得到它就可以知道所有其他序列的逆序数。用线段数解决
先以区间[0,9]为根节点建立val都为0的线段树,
再看看怎样求下面序列的逆序数:
1 3 6 9 0 8 5 7 4 2
在线段树中插入1, 插入之前先询问区间[1,9]已插入的节点数(如果存在,必与1构成逆序) v1=0
在线段树中插入3, 插入之前先询问区间[3,9]已插入的节点数(如果存在,必与3构成逆序) v2=0
在线段树中插入6, 插入之前先询问区间[6,9]已插入的节点数(如果存在,必与6构成逆序) v3=0
在线段树中插入9, 插入之前先询问区间[9,9]已插入的节点数(如果存在,必与9构成逆序) v4=0
在线段树中插入0, 插入之前先询问区间[0,9]已插入的节点数(如果存在,必与0构成逆序) v5=4
在线段树中插入8, 插入之前先询问区间[8,9]已插入的节点数(如果存在,必与8构成逆序) v6=1
在线段树中插入5, 插入之前先询问区间[5,9]已插入的节点数(如果存在,必与5构成逆序) v7=3
在线段树中插入7, 插入之前先询问区间[7,9]已插入的节点数(如果存在,必与7构成逆序) v8=2
在线段树中插入4, 插入之前先询问区间[4,9]已插入的节点数(如果存在,必与4构成逆序) v9=5
在线段树中插入2, 插入之前先询问区间[2,9]已插入的节点数(如果存在,必与2构成逆序) v10=7
累加v1+……+v10 =22,这就是1 3 6 9 0 8 5 7 4 2的逆序数了.
其实就是统计一个区间内已经插入了多少个元素,也可以说成是区间求和(一个区间能达到的最大和就是该区间长度,现在的和就是已经插入在内的元素个数)
下面是代码,只要看了上面的分析,代码是通熟易懂的
#include <cstdio> #include <cstring> //#define max(a,b) a>b?a:b #define N 5000 struct node { int l,r,c; }t[4*N+10]; int x[N],n; void build(int a ,int b ,int rt) { t[rt].l=a; t[rt].r=b; t[rt].c=0; if(a==b) return ; int mid=(a+b)>>1; build(a,mid,rt<<1); build(mid+1,b,rt<<1|1); } int query(int a, int b ,int rt) { if(t[rt].l==a && t[rt].r==b) return t[rt].c; int mid=(t[rt].l+t[rt].r)>>1; if(a>mid) return query(a, b, rt<<1|1); //只访问右子树 else if(b<=mid) return query(a, b, rt<<1); //只访问左子树 else return query(a, mid, rt<<1)+query(mid+1, b, rt<<1|1);; //左右均访问 } void updata(int val , int rt) { t[rt].c++; if(t[rt].l==t[rt].r) return ; int mid=(t[rt].l+t[rt].r)>>1; if(val>mid) updata(val, rt<<1|1); else updata(val, rt<<1); } int min(int a ,int b) { return a<b?a:b; } int main() { while(scanf("%d",&n)!=EOF) { build(0,n-1,1); int first=0; for(int i=1; i<=n; i++) { scanf("%d",&x[i]); first+=query(x[i],n-1,1); updata(x[i],1); } // printf("first=%d\n",first); int ans,sum; ans=sum=first; for(int i=1; i<=n-1; i++) { sum=sum-x[i]+(n-1-x[i]); ans=min(ans,sum); } printf("%d\n",ans); } return 0; }
用树状数组解决,和线段树是完全一样的思想,就是统计,一开始c数组先清空为0,没读入一个数字,相当于相对应的a[i]增1,所以将增1沿路径修改c数组的值。
对于每个读入的元素x,就看[x+1,n-1]已经读入了多少个,就是这个数字产生的逆序对
#include <cstdio> #include <cstring> #define N 5010 int lowbit(int n) { return n&(-n); } void add(int *c ,int p ,int n,int k) { while(p <= n) { c[p] += k; p += lowbit(p); } } int sum(int *c ,int p) { int ans=0; while(p) { ans += c[p]; p -= lowbit(p); } return ans; } int main() { int n,m,first; int c[N],a[N]; while(scanf("%d",&n)!=EOF) { memset(c,0,sizeof(c)); first=0; for(int i=0; i<n; i++) { scanf("%d",&m); a[i]=m; //接着找出[m+1,n-1]里面已经有多少个元素 //其实就是c[n-1]-c[m] //但c数组是从下标1开始的,所以c[n]-c[m+1] int s1,s2; add(c,m+1,n,1); //m+1这个位置(下标从1开始)的元素加1 s1=sum(c,m+1); s2=sum(c,n); first += (s2-s1); } //printf("%d\n",first); int ans=first , pre=first; for(int i=0; i<n-1; i++) { int x=a[i],s; s = pre-x+n-1-x; //少了x对,多了n-1-x对 if(s<ans) ans=s; pre=s; } printf("%d\n",ans); } return 0; }
用归并排序来求解逆序对问题
在合并的过程中是将两个相邻并且有序的序列合并成一个有序序列,如以下两个有序序列
Seq1:3 4 5
Seq2:2 6 8 9
合并成一个有序序:
Seq:2 3 4 5 6 8 9
对于序列seq1中的某个数a[i],序列seq2中的某个数a[j],如果a[i]<a[j],没有逆序数,如果a[i]>a[j],那么逆序数为seq1中a[i]后边元素的个数(包括a[i]),即len1-i+1,
这样累加每次递归过程的逆序数,在完成整个递归过程之后,最后的累加和就是逆序的总数
#include <cstdio> #include <cstring> #define min(a,b) a<b?a:b #define N 5050 int first,n; void merge(int *c ,int *b , int left ,int right) { int mid=(left+right)>>1 , i=left , j=mid+1 , k=left; while(i<=mid && j<=right) { if(c[i]>c[j]) //前部分大,要计数 { b[k++]=c[j++]; first+=(mid-i+1); } else b[k++]=c[i++]; } if(i<=mid) while(i<=mid) { /*first+=(mid-i+1);*/ b[k++]=c[i++]; } else while(j<=right) b[k++]=c[j++]; } void mergesort(int *a , int *b , int left , int right) {//把a数组的元素排好序保存在b数组中 if(left==right) { b[left]=a[left]; return ;} int mid=(left+right)>>1 , c[N]; mergesort(a,c,left,mid); //将a数组前部分排好临时保存在c数组中 mergesort(a,c,mid+1,right); //将a数组后部分排好临时保存在c数组中 merge(c,b,left,right); //将前后部分都有序的c数组合并在b数组中 } int main() { int a[N],x[N]; while(scanf("%d",&n)!=EOF) { for(int i=1; i<=n; i++) scanf("%d",&x[i]); first=0; mergesort(x,a,1,n); //将x数组的元素排好序保存在a数组中,要保存x数组不变 // printf("%d\n",first); int ans,sum; ans=sum=first; for(int i=1; i<=n-1; i++) { sum=sum-x[i]+(n-1-x[i]); ans=min(ans,sum); } printf("%d\n",ans); } return 0; }