小朋友排队
小朋友排队
$n$ 个小朋友站成一排。
现在要把他们按身高从低到高的顺序排列,但是每次只能交换位置相邻的两个小朋友。
每个小朋友都有一个不高兴的程度。
开始的时候,所有小朋友的不高兴程度都是 $0$。
如果某个小朋友第一次被要求交换,则他的不高兴程度增加 $1$,如果第二次要求他交换,则他的不高兴程度增加 $2$(即不高兴程度为 $3$),依次类推。当要求某个小朋友第 $k$ 次交换时,他的不高兴程度增加 $k$。
请问,要让所有小朋友按从低到高排队,他们的不高兴程度之和最小是多少。
如果有两个小朋友身高一样,则他们谁站在谁前面是没有关系的。
输入格式
输入的第一行包含一个整数 $n$,表示小朋友的个数。
第二行包含 $n$ 个整数 $H_{1},H_{2},…,H_{n}$,分别表示每个小朋友的身高。
输出格式
输出一行,包含一个整数,表示小朋友的不高兴程度和的最小值。
数据范围
$1 \leq n \leq 100000$,
$0 \leq H_{i} \leq 1000000$
输入样例:
3 3 2 1
输出样例:
9
样例解释
首先交换身高为 $3$ 和 $2$ 的小朋友,再交换身高为 $3$ 和 $1$ 的小朋友,再交换身高为 $2$ 和 $1$ 的小朋友,每个小朋友的不高兴程度都是 $3$,总和为 $9$。
解题思路
每次都交换两个相邻的元素,会想到冒泡排序。在冒泡排序中,序列有多少对逆序对,就交换多少次。
每次交换相邻的两个元素,最多会使得整个逆序对的数量减$1$。因为只交换相邻的两个逆序对,其他元素与这两个有关的逆序对是不会发生变化的,因为相对位置没有发生改变,这个交换操作只会改变这两个元素的相对关系。
在冒泡排序中(形成升序序列),只有前一个元素大于后一个元素时才会进行交换,因此每一次交换必然会使得逆序对数量减$1$。
假设序列中逆序对的数量为$k$,那么交换的次数至少为$k$,因为每次交换最多使得逆序对的数量减$1$。同时在冒泡排序中,每一次交换必然会使逆序对数量减$1$,而且冒泡排序正确的(最后一定会使得序列升序),因此只需要交换$k$次,就可以使得逆序对的数量减到$0$。说明存在一种排序方式,可以使得交换的次数恰好为$k$次,如果我们想把这些小朋友拍好序的话,最少需要交换$k$次。
对于某一个小朋友,假设在他前面比他高的人数为$k_{1}$,在他后面比他低的人数为$k_{2}$,则这个小朋友至少会被交换$k_{1} + k_{2}$次(把高的放到这个小朋友后面,低的放到这个小朋友前面)。假设逆序对的数量为$k$,把每一个小朋友的$k_{1} + k_{2}$都加起来,得到的结果就是$2 \cdot k$(每一个逆序对都被算了两次)。因为我们可以取到最小值$k$(前面已证明),取到最小值意味着每一步都没有多余的操作,每一次交换必然会减少一个逆序对。所以每一个小朋友都不会有多余的操作,每一个小朋友交换的次数最小是$k_{1} + k_{2}$。
所以最终要求的是,对于序列中的每一个数,分别统计一下$k_{1}$和$k_{2}$的数量(在这个数前面有多少个数比它大,在这个数后面有多少个数比它小)。对于每一个数来说,$k_{1} + k_{2}$就是它交换的次数。最后对应到这个小朋友的不高兴度为$1 + 2 + ... + k_{1} + k_{2}$,可以用高斯求和得到。求$k_{1}$和$k_{2}$可以用归并排序和树状数组。
如果用树状数组,要求某个数的$k_{1}$,从头开始遍历序列,通过树状数组得到大于这个数的数有多少个,然后在树状数组中给这个数加$1$。要求某个数的$k_{2}$,从后面逆序遍历,通过树状数组得到小于这个数的数有多少个,然后在树状数组中给这个数加$1$。
AC代码如下:
1 #include2 #include 3 #include 4 using namespace std; 5 6 const int N = 1e6 + 10; 7 8 int a[N], tr[N], sum[N]; 9 10 int lowbit(int x) { 11 return x & -x; 12 } 13 14 int query(int x) { 15 int ret = 0; 16 for (int i = x; i; i -= lowbit(i)) { 17 ret += tr[i]; 18 } 19 return ret; 20 } 21 22 void add(int x, int val) { 23 for (int i = x; i < N; i += lowbit(i)) { 24 tr[i] += val; 25 } 26 } 27 28 int main() { 29 int n; 30 scanf("%d", &n); 31 for (int i = 0; i < n; i++) { 32 scanf("%d", a + i); 33 a[i]++; // 树状数组的下标从1开始 34 } 35 36 // 求某个数前面有多少个大于它的数 37 for (int i = 0; i < n; i++) { 38 sum[i] = query(N - 1) - query(a[i]); 39 add(a[i], 1); 40 } 41 42 memset(tr, 0, sizeof(tr)); 43 // 求某个数后面有多少个小于它的数 44 for (int i = n - 1; i >= 0; i--) { 45 sum[i] += query(a[i] - 1); 46 add(a[i], 1); 47 } 48 49 long long ret = 0; 50 for (int i = 0; i < n; i++) { 51 ret += (long long)sum[i] * (1 + sum[i]) >> 1; 52 } 53 printf("%lld", ret); 54 55 return 0; 56 }
如果用归并排序,同样求某个数前面有多少个大于它的数,后面有多少个小于它的数。先递归分别求得左半区间和右半区间内对应的每个数前面有多少个大于它的数,后面有多少个小于它的数。然后左半区间和右左半区间会排好序,所以接着只需要考虑,对于左半区间中每一个数,求在右半区间中有多少个小于它的数。对于右半区间中每一个数,求在左半区间中有多少个大于它的数。
AC代码如下:
1 #include2 #include 3 #include 4 using namespace std; 5 6 typedef pair<int, int> PII; 7 8 const int N = 1e6 + 10; 9 10 PII a[N], tmp[N]; 11 int sum[N]; 12 13 void merge_sort(int left, int right) { 14 if (left >= right) return; 15 16 int mid = left + right >> 1; 17 merge_sort(left, mid), merge_sort(mid + 1, right); 18 19 int i = left, j = mid + 1, k = 0; 20 while (i <= mid && j <= right) { 21 if (a[i].first <= a[j].first) { 22 sum[a[i].second] += j - mid - 1; // 右半边区间小于a[i].first的数在区间[mid + 1, j)中 23 tmp[k++] = a[i++]; 24 } 25 else { 26 sum[a[j].second] += mid - i + 1; // 左半边区间大于a[j].first的数在区间[i, mid]中 27 tmp[k++] = a[j++]; 28 } 29 } 30 31 // 左半区间的数还没有遍历完,说明左边区间剩余没遍历完的数都比右半边的所有数大,即右半区间小于它的数有right - mid个 32 while (i <= mid) { 33 sum[a[i].second] += right - mid; 34 tmp[k++] = a[i++]; 35 } 36 37 // 右半区间的数还没有遍历完,说明右半区间剩余没遍历完的数都比左半边的所有数大,即左半区间没有大于它的数 38 while (j <= right) { 39 tmp[k++] = a[j++]; 40 } 41 memcpy(a + left, tmp, sizeof(PII) * k); 42 } 43 44 int main() { 45 int n; 46 scanf("%d", &n); 47 for (int i = 0; i < n; i++) { 48 int val; 49 scanf("%d", &val); 50 a[i] = {val, i}; 51 } 52 53 merge_sort(0, n - 1); 54 55 long long ret = 0; 56 for (int i = 0; i < n; i++) { 57 ret += (long long)sum[i] * (1 + sum[i]) >> 1; 58 } 59 printf("%lld", ret); 60 61 return 0; 62 }
参考资料
AcWing 1215. 小朋友排队(蓝桥杯C++ AB组辅导课):https://www.acwing.com/video/687/