蓝桥杯 历届试题 小朋友排队(归并,树状数组求逆序对数)

11 篇文章 0 订阅
5 篇文章 0 订阅
这篇博客主要介绍了如何使用归并排序和树状数组来解决蓝桥杯中关于小朋友排队问题,即计算逆序对数。通过归并排序分析每个小朋友需要交换的位置,以及利用树状数组动态维护逆序对。同时,针对身高相等的情况进行了处理。
摘要由CSDN通过智能技术生成

历届试题 小朋友排队  
时间限制:1.0s   内存限制:256.0MB
    
问题描述
  n 个小朋友站成一排。现在要把他们按身高从低到高的顺序排列,但是每次只能交换位置相邻的两个小朋友。

  每个小朋友都有一个不高兴的程度。开始的时候,所有小朋友的不高兴程度都是0。

  如果某个小朋友第一次被要求交换,则他的不高兴程度增加1,如果第二次要求他交换,则他的不高兴程度增加2(即不高兴程度为3),依次类推。当要求某个小朋友第k次交换时,他的不高兴程度增加k。

  请问,要让所有小朋友按从低到高排队,他们的不高兴程度之和最小是多少。

  如果有两个小朋友身高一样,则他们谁站在谁前面是没有关系的。
输入格式
  输入的第一行包含一个整数n,表示小朋友的个数。
  第二行包含 n 个整数 H1 H2 … Hn,分别表示每个小朋友的身高。
输出格式
  输出一行,包含一个整数,表示小朋友的不高兴程度和的最小值。
样例输入
3
3 2 1
样例输出
9
样例说明
  首先交换身高为3和2的小朋友,再交换身高为3和1的小朋友,再交换身高为2和1的小朋友,每个小朋友的不高兴程度都是3,总和为9。
数据规模和约定
  对于10%的数据, 1<=n<=10;
  对于30%的数据, 1<=n<=1000;
  对于50%的数据, 1<=n<=10000;
  对于100%的数据,1<=n<=100000,0<=Hi<=1000000。


方法一:归并排序

参考博客:http://www.mamicode.com/info-detail-556673.html   感谢!

问题分析:

1.对于n个小朋友中的任何一个小朋友k,分析可以得到:

   a.排在k前面比k高的小朋友都会交换到k后面去(这种小朋友个数记为higher),

   b.排在k后面比k低的小朋友都会交换到k前面去(这种小朋友个数记为lower),

 所以最后排好序的时候,k至少交换higher+lower次。

2.先假设n个小朋友身高均不相等,我们通过归并排序可以求出每个小朋友的higher值,设小朋友k在第k位上,是第w矮的,则low = w-1 - (k-1-higher)。(w-1表示比k矮的总人数,k-1-higher表示排在k前面比k矮的人数)

3.归并排序求小朋友k的high值,初始时high[k]=0,在对身高数组a[]中a[l]~a[mid]和a[mid+1]~a[r]合并操作的时候,若对a[l]~a[mid]中的一个数a[j],若mid+1<=k<=r && a[k]<a[j],则higher[k] += mid-j+1(因为两个序列都是排好序的,所以a[j]~a[mid]都比a[k]大且排在a[k]前边,所以累加)

4.现在处理n个小朋友中,有身高相等的情况,转化为身高全不相等即可:

 a.对小朋友的身高按从小到大的顺序从1~n进行重新编号,对于身高相等的小朋友,按从左到右的顺序从1~n依次编号(这样做不会影响结果).

 b.重新编号方法:先将所有小朋友的身高数组(a[])赋值到另一数组(b[])并进行排序(从小到大),对每一个a[i],去b[]里面二分查找其相对位置k(下标),并令b[k]--。其中二分查找返回的是最左边的a[i]==b[k]的下标k,b[k]--的目的是保证再次查到b[k]的时候查到的是后一个(这样是不会影响b[k]前面的数的  ,至于为什么看看二分代码就明白了)。


#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
const int maxn = 100000 + 100;
int n;
int a[maxn], b[maxn];
struct node {
	int id;
	int higher;
	int lower;
}num[maxn];
int binary(int l, int r, int key) {  //返回满足条件的最靠左的下标 
	int mid;
	while(l < r) {
		mid = (l + r) / 2;
		if(key == b[mid]) {
			if(b[mid - 1] < key) {
				return mid;
			}
			else {
				r = mid - 1;
			}
		}
		else if(key > b[mid]) {
			l = mid + 1;
		}
		else {
			r = mid - 1;
		}
	}
	return l;
}
void merge(int l, int r) {
	if(l == r) return;
	int mid = (l + r) / 2;
	merge(l, mid);
	merge(mid + 1, r);
	int i, j, k;
	for(i = l, j = mid + 1, k = 0; i <= mid && j <= r; ) {
		if(a[i] < a[j]) {
			b[k++] = a[i++];
		}
		else if(a[i] > a[j]){
			b[k++] = a[j];
			num[a[j]].higher += mid - i + 1;
			j++;
		}
	}
	while(i <= mid) {
		b[k++] = a[i++];
	}
	while(j <= r) {
		b[k++] = a[j++];
	}
	for(i = l, j = 0; i <= r; i++) {
		a[i] = b[j++];
	}
}
int main() {
	scanf("%d", &n);
	int i, j;
	for(i = 1; i <= n; i++) {
		num[i].higher = 0;
	}
	for(i = 1; i <= n; i++) {
		scanf("%d", a + i);
		b[i] = a[i];
	}
	sort(b + 1, b + n + 1);
	for(i = 1; i <= n; i++) {   //重新编号 
		a[i] = binary(1, n, a[i]);
		b[a[i]]--;
		num[a[i]].id = i;
	}
	merge(1, n);
	long long ans = 0, t;
	int pos, w;
	for(i = 1; i <= n; i++) {
		pos = num[i].id;  //第i低的小朋友在原队的位置 
		w = i;
		num[i].lower = w - 1 - (pos - 1 - num[i].higher);
		t = num[i].higher + num[i].lower;
		ans += t * (t + 1) / 2;
	}
	printf("%I64d\n", ans);
	return 0;
}



方法二:树状数组求逆序对数

参考博客:http://blog.csdn.net/wr132/article/details/43856905   感谢!!

方法是从上面的博客里学到的,但是个人感觉上述博客虽然方法和代码是正确的,但是在细节方面还是有某些地方没讲太清晰。当然这只是我的一己拙见,我试着把求逆序对数的过程修改得更清楚一点,希望没有画蛇添足吧。


首先必须要明确树状数组的用法和意义,不然是没办法做的,关于树状数组的入门,推荐这个博客,讲得很清晰,感谢! http://www.cnblogs.com/zichi/p/4806998.html

首先声明,为了避免混乱,所有的数组下标都是从1开始,因此在这里我们取树状数组sum的下标从1开始,而小朋友身高却是从零开始的,所以将身高+1 作为树状数组的下标,而1即作为增加值(下文中的身高均指原始身高+1之后的身高)。

STEP ONE:  求每个小朋友前面能和他组成逆序对的小朋友个数

第一次输入3,输入数据量i = 1,得到以下数

sum[1]        sum[2]        sum[3]        sum[4]       sum[5]        sum[6]         sum[7]         sum[8]
  0                    0                 0                  1                  0                  0                  0                   0
使用树状数组的getsum(3)操作可以得到身高小于4的人数,在这里getsum(3) = 0,因为我们是按照从前到后的顺序输入的,所以这里的getsum(3) = 0即代表 第一个小朋友前面身高小于他的身高的小朋友的数量。而我们想得到的是这个小朋友前面身高大于他的身高的小朋友的数量,所以应该用此时插入的小朋友总数 i 减去getnum(3)再减去1(这个小朋友本身),即i - getnum(3) - 1 = 0; 这就表示第一个小朋友前面比他高的人数为0,这是显而易见的,但计算的过程需要理解。我们记录下结果b[1] = 0,表示第1个小朋友前面的可以和他形成逆序对的小朋友个数。
第二次输入2,输入数据量i = 2,得到以下数组:
sum[1]        sum[2]        sum[3]        sum[4]       sum[5]        sum[6]         sum[7]         sum[8]
  0                    0                 1                  1                  0                  0                  0                   0
与第一次类似,使用树状数组的getsum(2)操作 可以得到身高小于3的人数,getsum(2) = 0,代表第二个小朋友前面身高小于他的身高的小朋友数量为0,而 i - getnum(2) - 1 = 1表示第二个小朋友前面比他高的人数为1,也就是第一次输入的小朋友。 记录下结果b[2] = 1。
第三次输入1,输入数据量i = 3,得到以下数组:

sum[1]        sum[2]        sum[3]        sum[4]       sum[5]        sum[6]         sum[7]         sum[8]
  0                    1                 1                  1                  0                  0                  0                   0
与前两次类似, 使用树状数组的getsum(1)操作 可以得到身高小于2的人数,getsum(1) = 0,代表第三个小朋友前面身高小于他的身高的小朋友数量为0,而 i - getnum(1) - 1 = 2表示第二个小朋友前面比他高的人数为2,也就是前两次输入的小朋友。 记录下结果b[3] = 2。

到这里我们就求出了每个小朋友前面能和他组成逆序对的小朋友个数,下面我们就要求每个小朋友后面的能和他组成逆序对的小朋友个数。

STEP TWO:   求每个小朋友后面的能和他组成逆序对的小朋友个数

这次我们将sum数组置零,然后从后往前插入,即先插入1,然后2,然后3。

这样我们每插入完一次,getsum(num[i])得到便是排在第i个小朋友后面并且比第i个小朋友矮的小朋友个数,这就是我们想求的排在第i个小朋友后面的能和他组成逆序对的小朋友个数。所以直接利用这个结果更新b[i]即可。

最终我们得到b[1] = 2,b[2] = 2, b[3] = 2。

这样我们就求出了每个小朋友的逆序对数,也就是他们最少需要交换几次,后面就直接按要求求和即可。注意要用long long。

但是上面的例子是小朋友身高都不同的情况,如果有多个小朋友身高相同,则需要多一步处理。

身高相同的处理:

在step one中,我们想利用i - getnum(num[i]) - 1 求得排在第i个小朋友前面并且比他高的个数,但是仔细一想,getnum(num[i])求得的仅仅是比他矮的人的个数,所以如果还有x个人和i小朋友身高相等,那么i - getnum(num[i]) - 1求得的便是排在第i个小朋友前面并且比他高的个数+x,所以我们应该在最后减去这个x(如果没人和他身高相等的话x即为0)。那么现在的问题就是这个x如何求呢?我们注意到getsum(num[i])求得的是比i矮的人的个数也就是身高小于等于num[i]的个数(注意此时i的身高应为num[i] + 1),那么getsum(num[i] + 1)求得的是身高小于等于num[i] + 1的个数也就是比i矮或者和i身高相等的个数,这样的话 x 就等于getsum(num[i] + 1) - getsum(num[i]) - 1。

而在step tow中,即使存在身高相同的小朋友,我们求得的getsum(num[i])依然是排在第i个小朋友后面并且比第i个小朋友矮的小朋友个数,所以不用处理。


解释就到这里,具体见代码,祝参加蓝桥杯的同学都能取得好成绩!

#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
const int maxn = 100000 + 100;
const int maxh = 1000000 + 100;
int num[maxn], sum[maxh], b[maxh], n;
int lowbit(int x) {
	return x & (- x);
}
void update(int index, int x) {
	int i;
	for(i = index; i <= maxh; i += lowbit(i)) {
		sum[i] += x;
	}
}

int getsum(int index) {
	int i;
	int res = 0;
	for(i = index; i > 0; i -= lowbit(i)) {
		res += sum[i];
	}
	return res;
}
int main() {
	scanf("%d", &n);
	memset(sum, 0, sizeof(sum));
	int i, j;
	for(i = 1; i <= n; i++) {
		scanf("%d", num + i);
		update(num[i] + 1, 1);
		b[i] = i - getsum(num[i]) - 1;
		b[i] -= getsum(num[i] + 1) - getsum(num[i]) - 1;  //身高相同的处理 
	}
	memset(sum, 0, sizeof(sum));
	for(i = n; i > 0; i--) {
		update(num[i] + 1, 1);
		b[i] += getsum(num[i]);
	}
	long long ans = 0;
	for(i = 1; i <= n; i++) {
		ans += (long long)b[i] * (b[i] + 1) / 2;
	}
	printf("%I64d\n", ans);
	return 0;
}



评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值