小朋友排队
一、 题目分析
表面上看,这是一道排序题,但实际上,这道题目不仅仅要求简单的排序,因为题目要求的是小朋友从低到高排序后,他们的不高兴程度之和的最小值,也就是求逆序对数的题目。
例如:样例输入 (3, 2, 1) 中,有 3 个逆序对—— (3, 2) , (3, 1) , (2, 1) ,使用使小朋友不高兴之和最小的排序方法后,即首先交换身高为 3 和 2 的小朋友,再交换身高为 3 和 1的小朋友,再交换身高为 2 和 1 的小朋友,每一个逆序对的两个元素都被要求交换了一次,而与 3 有关的逆序对为 (3, 2) 和 (3, 1) ,与 2 有关的逆序对为 (3, 2) 和 (2, 1) ,与 1 有关的逆序对为 (3, 1) 和 (2, 1) ,显然可以看出每个小朋友都被要求交换的两次。
综上所述,题目的关键是求出小朋友的身高在所有逆序对中出现的次数,也就是包含这个元素的逆序对的个数,更直接的说法是对每一个元素,求出在它前面大于它的元素的个数和在它后面小于它的元素的个数之和。最后计算出每个元素的不高兴程度并相加即可。
为了求小朋友的身高在所有逆序对中出现的次数,我们很容易想到使用暴力法解决,但是,看清楚数据规模和约定:对于 100% 的数据,1<=n<=100000,0<=Hi<=1000000,这种方法肯定会超出时间限制:1.0s ,所以我们要采用另一种数据结构——树状数组来解决这道题。
二、 树状数组介绍
设数组 A[n] , n=1 , 2 , 3 … ,树 C ,令这棵树的结点编号为 C1 , C2 , ... Cn ,每个结点的值为这棵树的值的总和,则:
C1 = A[1]
C2 = A[1] + A[2]
C3 = A[3]
C4 = A[1] + A[2] + A[3] + A[4]
C5 = A[5]
C6 = A[5] + A[6]
C7 = A[7]
C8 = A[1] + A[2] + A[3] + A[4] + A[5] + A[6] + A[7] + A[8]
...
这里有一个有趣的性质:
设节点编号为 i ,那么这个节点管辖的区间为 2^k (其中 k 为 i 二进制末尾 0 的个数)个元素。因为这个区间最后一个元素必然为Ai。
所以:Ci = A(n-2^k+1) + ... + Ai
算这个2^k有一个快捷的办法,定义一个函数如下即可:
1 /********************************************************************************************************* 2 ** 函数功能 :计算编号为 i 的节点管辖的区间(该区间中元素的个数) 3 ** 函数说明 :设节点编号为 i,那么这个节点管辖的区间为 2^k(其中 k 为 i 二进制末尾0的个数)个元素。 4 ** :i&(i^(i-1))=i&(i) 5 ** 入口参数 :i:节点编号 6 ** 出口参数 :编号为 i 的节点管辖的区间(该区间中元素的个数) 7 *********************************************************************************************************/ 8 int GovernNodeNum(int i) 9 { 10 return i&(-i); 11 }
下面介绍两个算法:
A、求前 i 个元素的和的步骤如下:
- 令 sum=0 ;
- 若 i<=0 ,结束并返回 sum 值,否则 sum=sum+Cn ;
- 令 i=i-GovernNodeNum (n) ,返回第二步。
程序设计如下:
1 /********************************************************************************************************* 2 ** 函数功能 :计算树状数组节点 1 ~ i 的值的和 3 ** 函数说明 :无 4 ** 入口参数 :i :求和节点区间的上限 5 ** :tree :数状数组 6 ** 出口参数 :树状数组节点 1 ~ i 的值的和 7 *********************************************************************************************************/ 8 int BinaryIndexedTreeSum(int i,int *tree) 9 { 10 int sum=0; 11 while(i>0) 12 { 13 sum+=tree[i]; 14 i-=GovernNodeNum(i); 15 } 16 17 return sum; 18 }
可以看出,这个算法就是将这一个个区间的和全部加起来,为什么是效率是 log(i) 的呢?以下给出证明:
i=i-lowbit(n) 这一步实际上等价于将 i 的二进制的最前面的一个 1 删去。而 i 的二进制里最多有 log(i) 个 1 ,所以查询效率是 log(i) 。
B、修改一个节点(给某个结点i加上 val):
修改一个节点,必须修改其所有祖先,最坏情况下为修改第一个元素,最多有 log(n) 个祖先。修改步骤如下(给某个结点i加上val):
- 若 i>n ,结束,否则跳到第二步 ;
- Ci = Ci + val ,i=i+GovernNodeNum (i) 返回第一步。
程序设计如下:
1 /********************************************************************************************************* 2 ** 函数功能 :在数组数组 tree 的节点 i 中加上 num 3 ** 函数说明 :数组数组的节点从 1 开始 4 ** 入口参数 :i :节点编号 5 ** :num :需要加上的数值 6 ** :tree :树状数组 7 ** 出口参数 :无 8 *********************************************************************************************************/ 9 void BinaryIndexedTreeAdd(int i,int num,int *tree) 10 { 11 while(i<=MAX_Height) 12 { 13 tree[i]+=num; 14 i+=GovernNodeNum(i); 15 } 16 }
i=i+lowbit(i) 这个过程实际上也只是在 i 的二进制最后面的添加 0 的过程。
三、 算法设计:
树状数组的特点是快速地求前 n 个元素的和。接着还是以样例输入为例来讲解。
树状数组实际上是由两部分组成:数据数组+统计数组,我们这里只看数据数组。
由于树状数组是从 1 开始的,而题目中小朋友的身高可以为 0 ,所以我们将每个小朋友的身高加上 1 然后作为树状数组节点的下标,使该节点的值加 1 。
第一次读入 3 ,此时读入的数据量为 1 个,则:
C1 |
C2 |
C3 |
C4 |
C5 |
C6 |
C7 |
C8 |
0 |
0 |
0 |
1 |
0 |
0 |
0 |
0 |
可以看到 sum(C1,C4)=1 (可以由树状数组的统计数组得到),这个是小于等于 3 的数字的个数,也就是说当输入第一个数字 3 的时候没有比它小的数字存在,这时我们可以用输入数字总数 - sum(C1,C4)=0 ,也就是说大于 3 的数字的个数为 0 。
第二次读入2,此时读入的数据量为2个,则:
C1 |
C2 |
C3 |
C4 |
C5 |
C6 |
C7 |
C8 |
0 |
0 |
1 |
1 |
0 |
0 |
0 |
0 |
可以看到 sum(C1,C3)=1 ,依然不存在比它小的数,但此时输入的数据总量为2,而 2-1=1 ,即一个大于或等于 2 的数,这个数就是3。
第三次读入1,此时读入的数据量为3,则:
C1 |
C2 |
C3 |
C4 |
C5 |
C6 |
C7 |
C8 |
0 |
1 |
1 |
1 |
0 |
0 |
0 |
0 |
可以看到 sum(C1,C2)=1,依然不存在比它小的数,但此时输入的数据总量为 3 ,而3-1=2 ,即存在两个大于或等于 1 的数,这个两个数就是 2 和 3。
这里有一个问题:如果重复的数字出现怎么办?实际上我们可以这样解决:
出现该问题的会是求每个数前面较大数的那部分,因为用到了数的总个数,如果出现一样的数,就会导致相减后的结果偏大,而且正好是大了重复量 - 1 个,那么我们就可以算出重复量,然后将这一部分减去就行。关键是怎么算重复量,实际也很简单,通过树状数组,我们可以求得 sum(1,a) 和 sum(1,a+1) ,其中输入的数字为 a ,前者算出的小于 a 的数的个数,后者算出的是小于等于 a 的数的个数,两个一减就是等于 a 的个数了。
至此,我们已经计算出了每元素前面的比该元素大的个数,现在我们再反过来,先插入1 ,再插入 2 ,再插入 3 ,但这次我们不再用总数减去 sum 了,而直接求 sum ,求出来的就是每元素后面比该元素小的个数,然后将上面统计得到的每个元素对应的两个统计值相加,即可得到每个元素在完成排序后,被要求交换的次数。
最后,我们要做的是根据每个元素被要求交换的次数求其对应的不高兴程度并累加求和。这里,我们可以先打个表,就是将被移动 n 次后的不高兴值全部算出来,然后直接用就可以了,我们将其存到 degree_sad[] 数组中,而且 degree_sad[2]=3,所以总不高兴值就是9。
四、 程序设计
1 #include<iostream> 2 #include<memory.h> 3 4 using namespace std; 5 6 //宏声明 7 #define MAX_n 100001 //定义小朋友个数的最大值 8 #define MAX_Height 1000001 //定义小朋友身高的最大值 9 10 //结构体声明 11 typedef struct Child //小朋友结构体 12 { 13 int height; //身高 14 int times; //被要求交换的次数 15 }child; 16 17 //全局变量声明 18 child c[MAX_n]; //用于记录小朋友的身高和被要求交换次数的结构体数组 19 int tree[MAX_Height]; //用于记录小朋友身高的树状数组 20 21 //函数声明 22 int GovernNodeNum(int i); //计算编号为 i 的节点管辖的区间(该区间中元素的个数) 23 void BinaryIndexedTreeAdd(int i,int num,int *tree); //在数组数组 tree 的节点 i 中加上 num 24 int BinaryIndexedTreeSum(int i,int *tree); //计算树状数组节点 1 ~ i 的值的和 25 26 int main() 27 { 28 int n; //用于记录输入的小朋友的个数 29 cin>>n; //输入小朋友的个数 30 31 int i; 32 memset(c,0,sizeof(c)); //清零小朋友结构体数组 33 memset(tree,0,sizeof(tree)); //清零树状数组 34 for(i=0;i<n;i++) 35 { 36 cin>>c[i].height; //输入小朋友的身高 37 BinaryIndexedTreeAdd(c[i].height+1,1,tree); //把身高添加到树状数组中 38 //计算树状数组中比刚输入的小朋友身高高的小朋友的个数 39 c[i].times=i-BinaryIndexedTreeSum(c[i].height,tree); 40 c[i].times-=(BinaryIndexedTreeSum(c[i].height+1,tree)-BinaryIndexedTreeSum(c[i].height,tree)-1); 41 } 42 43 memset(tree,0,sizeof(tree)); //清零树状数组 44 for(i=n-1;i>=0;i--) 45 { 46 BinaryIndexedTreeAdd(c[i].height+1,1,tree); //把身高倒序添加到树状数组中 47 c[i].times+=BinaryIndexedTreeSum(c[i].height,tree); //计算树状数组中比刚输入的小朋友身高矮的小朋友的个数 48 } 49 50 //初始化小朋友被交换次数与不高兴程度对应的数组 51 long long degree_sad[MAX_n]; 52 degree_sad[0]=0; 53 for(i=1;i<MAX_n;i++) 54 degree_sad[i]=degree_sad[i-1]+i; 55 56 //计算小朋友不高兴程度的和 57 long long sad_sum=0; 58 for(i=0;i<n;i++) 59 sad_sum+=degree_sad[c[i].times]; 60 61 cout<<sad_sum; //输出小朋友不高兴程度的和 62 63 return 0; 64 } 65 66 /********************************************************************************************************* 67 ** 函数功能 :计算编号为 i 的节点管辖的区间(该区间中元素的个数) 68 ** 函数说明 :设节点编号为 i,那么这个节点管辖的区间为 2^k(其中 k 为 i 二进制末尾0的个数)个元素。 69 ** :i&(i^(i-1))=i&(i) 70 ** 入口参数 :i:节点编号 71 ** 出口参数 :编号为 i 的节点管辖的区间(该区间中元素的个数) 72 *********************************************************************************************************/ 73 int GovernNodeNum(int i) 74 { 75 return i&(-i); 76 } 77 78 /********************************************************************************************************* 79 ** 函数功能 :在数组数组 tree 的节点 i 中加上 num 80 ** 函数说明 :数组数组的节点从 1 开始 81 ** 入口参数 :i :节点编号 82 ** :num :需要加上的数值 83 ** :tree :树状数组 84 ** 出口参数 :无 85 *********************************************************************************************************/ 86 void BinaryIndexedTreeAdd(int i,int num,int *tree) 87 { 88 while(i<=MAX_Height) 89 { 90 tree[i]+=num; 91 i+=GovernNodeNum(i); 92 } 93 } 94 95 /********************************************************************************************************* 96 ** 函数功能 :计算树状数组节点 1 ~ i 的值的和 97 ** 函数说明 :无 98 ** 入口参数 :i :求和节点区间的上限 99 ** :tree :数状数组 100 ** 出口参数 :树状数组节点 1 ~ i 的值的和 101 *********************************************************************************************************/ 102 int BinaryIndexedTreeSum(int i,int *tree) 103 { 104 int sum=0; 105 while(i>0) 106 { 107 sum+=tree[i]; 108 i-=GovernNodeNum(i); 109 } 110 111 return sum; 112 }