2014蓝桥杯 小朋友排队(归并排序 和 数状数组 求逆序对)

标题:小朋友排队

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。

资源约定:
峰值内存消耗 < 256M
CPU消耗 < 1000ms

归并排序可以求总的逆序对,这个大家应该是知道的
他人 归并排序参考
此题所求并非是总的逆序对, 而是相对的。

解题思想:

如果有如下序列:
3 6 4 7 5 2 0 1
那么对于1来说,要想回到前面, 必须要经过 2 5 7 4 6 3 次序并不重要, 重要的是个数。
那么对于2来说,要被0 1经过。

那么总结一下就是:
就是相对于 2 来说有几个逆序对 即: 小于2并且在2右边的,大于2并且在其左边的, 取之和即为所求。

接下来一张草图,来解释归并求逆序对的过程。
在这里插入图片描述
归并排序求逆序对代码如下:
去掉含有 下面两个变量的代码所在行, 就是归并排序 ( ni_num_sum,ni_num_sum_temp )

#define MAX 1000005
// 原数组 
long num[MAX] = {0};
// 辅助排序 
long num_sort[MAX] = {0};

// 去掉含有 下面两个变量的代码所在行, 就是归并排序
// 记录左右两边的逆序 个数 
long ni_num_sum[MAX] = {0}; 
// 辅助记录 
long ni_num_sum_temp[MAX] = {0};
void pai_sort(int l, int mid, int r){
	int i = l;
	int j = mid + 1;
	int index = l;
	while(i <= mid && j <= r){
		if(num[i] < num[j]){	// 后面大于前面 
			num_sort[index] = num[i]; 
			ni_num_sum[index] = ni_num_sum_temp[i] + (j - mid - 1); // 逆序数 
			++i; 
		}else if(num[i] > num[j]){ // 前面 大于 后面  出现逆序 
			num_sort[index] = num[j]; 
			ni_num_sum[index] = ni_num_sum_temp[j] + (mid - i + 1);	// 逆序数 
			++j; 
		} else{// 等于, 不需要加逆序数 
			num_sort[index] = num[i]; 
			++i; 
			num_sort[index] = num[j]; 
			++j; 
		}
		++index;
	}
	while(i <= mid){
		if(num[i] != num_sort[index])	// 如果出现相等的情况, 就不要计算
			ni_num_sum[index] = ni_num_sum_temp[i] + (j - mid - 1);
		num_sort[index] = num[i];
		++index;
		++i;
	}
	while(j <= r){ 
		if(num[i] != num_sort[index])	// 如果出现相等的情况, 就不要计算
			ni_num_sum[index] = ni_num_sum_temp[j] + (mid - i + 1);
		num_sort[index] = num[j];
		++index;
		++j;
	}
	// 排序完成 放回原数组, 并且将逆序结果缓存等待取用。
	for(i = l; i <= r; ++i){
		num[i] = num_sort[i];
		ni_num_sum_temp[i] = ni_num_sum[i]; // 缓存上一次的逆序对数 
	}
}
void gui_pai(int l, int r){
	if(l < r){
		int mid = (l + r) >> 1;
		gui_pai(l, mid);
		gui_pai(mid + 1, r);
		pai_sort(l, mid, r);	
	}
} 

那么求出逆序对之后列 ?

之后便是转换成不满意程度了。

也好解决:
比如 2 有 7 个逆序对,那么就是 1 + 2 + 3 + 4 + 5 + 6 + 7 也就是n - 1求和
对于 n 到 1的求和 求和公式为 **( ( (n + 1) * n) / 2 ) ** 。
代码如下:

// 1 - n  连续数  求和 
long sum_0_to_n(long n){
	return ((n + 1) * n )>> 1;
} 

完整代码:

#include<stdio.h>
#define MAX 1000005
// 时间复杂度 O( N*log2(N) ) 
long N = 0;

// 原数组 
long num[MAX] = {0};
// 辅助排序 
long num_sort[MAX] = {0};

// 记录左右两边的逆序 个数 
long ni_num_sum[MAX] = {0}; 
// 辅助记录 
long ni_num_sum_temp[MAX] = {0};

unsigned long long ans = 0;
// 1 - n  连续数  求和 
long sum_0_to_n(long n){
	return ((n + 1) * n )>> 1;
} 
void pai_sort(int l, int mid, int r){
	int i = l;
	int j = mid + 1;
	int index = l;
	while(i <= mid && j <= r){
		if(num[i] < num[j]){	// 后面大于前面 
			num_sort[index] = num[i]; 
			
			ni_num_sum[index] = ni_num_sum_temp[i] + (j - mid - 1); // 逆序数 
			++i; 
		}else if(num[i] > num[j]){ // 前面 大于 后面  出现逆序 
			num_sort[index] = num[j]; 
			ni_num_sum[index] = ni_num_sum_temp[j] + (mid - i + 1);	// 逆序数 
			++j; 
		} else{// 等于, 不需要加逆序数 
			num_sort[index] = num[i]; 
			++i; 
			num_sort[index] = num[j]; 
			++j; 
		}
		index++;
	}
	while(i <= mid){
		if(num[i] != num_sort[index]) 
			ni_num_sum[index] = ni_num_sum_temp[i] + (j - mid - 1);
		num_sort[index] = num[i];
		++index;
		++i;
	}
	while(j <= r){ 
		if(num[i] != num_sort[index])
			ni_num_sum[index] = ni_num_sum_temp[j] + (mid - i + 1);
		num_sort[index] = num[j];
		++index;
		++j;
	}
	// 放回 
	for(i = l; i <= r; i++){
		num[i] = num_sort[i];
		ni_num_sum_temp[i] = ni_num_sum[i]; // 缓存上一次的逆序对数 
	}
}
void gui_pai(int l, int r){
	if(l < r){
		int mid = (l + r) >> 1;
		gui_pai(l, mid);
		gui_pai(mid + 1, r);
		pai_sort(l, mid, r);	
	}
} 
int main(){
	scanf("%d", &N);
	for(int i = 0; i < N; i++){
		scanf("%d", num + i);
	} 
	gui_pai(0, N - 1);
	// 使用逆序对 计算结果
	for(int i = 0; i < N; ++i){
		ans += sum_0_to_n(ni_num_sum[i]);
	} 
	printf("%lld\n", ans);
	for(int i = 0; i < N; i++){
		printf("%d ", ni_num_sum[i]);
	}
	return 0;	
}

解题思想二 数状数组:

同样是求逆序对, 但是求解方式不同。
同样一张草图, 表示解题过程。

在这里插入图片描述

#include<stdio.h>
#define N_MAX 100005
// 时间复杂度 O( N*log2(N) ) 

long N = 0;
// 原数组 
long num[N_MAX] = {0};
// 这里需要使用数字的最大值存放数据, 因为这相等于背包 
#define MAX 1000005 
// 树状数组解法
// 需要两个数组 
long tree_num_left[MAX];
long tree_num_right[MAX];
// 存放求和结果 
long ans_num_left[MAX];
long ans_num_right[MAX];
// 存放最大方便确定扫描区间 减少复杂度 
long max_num = -999999;
inline int low_bit(int t){
	return t&(-t);
} 
// 求和 
inline long tree_sum(long tree_num[], long index){
	long sum = 0;
	for(long i= index; i > 0; i -=low_bit(i)){
		sum +=  tree_num[i];
	}
	return sum;
} 
// 更新单节点
inline void update(long tree_num[], int index, long num){
	for(int i = index; i <= max_num; i += low_bit(i)){
		tree_num[i] += num;
	}
}
int main(){
	
	scanf("%d", &N);
	for(int i = 0; i < N; i++){
		scanf("%d", num + i);
		if(num[i] > max_num){
			max_num = num[i];
		} 
	} 
	max_num ++; // 因为会出现0的情况, 将最大值加1就可以, 相当于向右偏移1, 当然 多加也无所谓 
	
	// 树状数组解法 
	
	// 更新 由左向右扫描 
	for(int i = 0; i < N; ++i){
		update(tree_num_left, num[i] + 1, 1); 
		// 若是 替换成 ans_num_right[num[i]] 数据将变得有序 , 但是后续合并和计算复杂度 最差 将会变高10倍 
		// 而且并不需要有序, 只需要结果就好 
		ans_num_left[i] = tree_sum(tree_num_left, max_num) - tree_sum(tree_num_left, num[i] + 1); 
	}
	// 更新 由右向左扫描 
	for(int i = N - 1; i >= 0; --i){
		update(tree_num_right, num[i] + 1, 1); 
		ans_num_right[i] = tree_sum(tree_num_right, num[i]); 
	}
	// 合并 
	unsigned long long ans_2 = 0;
	for(int i = 0; i < N; ++i){
		ans_num_right[i] += ans_num_left[i]; 
		// 求值
		ans_2 += sum_0_to_n(ans_num_right[i]);
		printf("%ld ", ans_num_right[i]);
	} 
	puts("");
	printf("树状数组求得:%lld\n", ans_2);
	
	return 0;	
}

运行截图:

归并排序 和 数状数组全部代码

#include<stdio.h>
#define N_MAX 100005
// 时间复杂度 O( N*log2(N) ) 

long N = 0;
// 原数组 
long num[N_MAX] = {0};
// 辅助排序 
long num_sort[N_MAX] = {0};

// 记录左右两边的逆序 个数 
long ni_num_sum[N_MAX] = {0}; 
// 辅助记录 
long ni_num_sum_temp[N_MAX] = {0};



unsigned long long ans = 0;

// 1 - n  连续数  求和 
long sum_0_to_n(long n){
	return ((n + 1) * n )>> 1;
} 

void pai_sort(int l, int mid, int r){
	int i = l;
	int j = mid + 1;
	int index = l;

	while(i <= mid && j <= r){
		if(num[i] < num[j]){	// 后面大于前面 
			num_sort[index] = num[i]; 
			
			ni_num_sum[index] = ni_num_sum_temp[i] + (j - mid - 1); // 逆序数 
			++i; 
		}else if(num[i] > num[j]){ // 前面 大于 后面  出现逆序 
			num_sort[index] = num[j]; 
			
			ni_num_sum[index] = ni_num_sum_temp[j] + (mid - i + 1);	// 逆序数 
			++j; 
		} else{// 等于, 不需要加逆序数 
			num_sort[index] = num[i]; 
			++i; 
			num_sort[index] = num[j]; 
			++j; 
		}
		index++;
	}
	while(i <= mid){
		
		if(num[i] != num_sort[index]) 
			ni_num_sum[index] = ni_num_sum_temp[i] + (j - mid - 1);
		num_sort[index] = num[i];
		++index;
		++i;
	}
	while(j <= r){ 
		if(num[i] != num_sort[index])
			ni_num_sum[index] = ni_num_sum_temp[j] + (mid - i + 1);
		num_sort[index] = num[j];
		++index;
		++j;
	}
	// 放回 
	for(i = l; i <= r; i++){
		num[i] = num_sort[i];
		ni_num_sum_temp[i] = ni_num_sum[i]; // 缓存上一次的逆序对数 
	}
 
}
void gui_pai(int l, int r){
	
	if(l < r){
		int mid = (l + r) >> 1;
		gui_pai(l, mid);
		gui_pai(mid + 1, r);
		
		pai_sort(l, mid, r);	
	}
	
} 


// 这里需要使用数字的最大值存放数据, 因为这相等于背包 
#define MAX 1000005 
// 树状数组解法
// 需要两个数组 
long tree_num_left[MAX];
long tree_num_right[MAX];
// 存放求和结果 
long ans_num_left[MAX];
long ans_num_right[MAX];
// 存放最大方便确定扫描区间 减少复杂度 
long max_num = -999999;
inline int low_bit(int t){
	return t&(-t);
} 
// 求和 
inline long tree_sum(long tree_num[], long index){
	long sum = 0;
	for(long i= index; i > 0; i -=low_bit(i)){
		sum +=  tree_num[i];
	}
	return sum;
} 
// 更新单节点
inline void update(long tree_num[], int index, long num){
	for(int i = index; i <= max_num; i += low_bit(i)){
		tree_num[i] += num;
	}
}

int main(){
	
	scanf("%d", &N);
	for(int i = 0; i < N; i++){
		scanf("%d", num + i);
		if(num[i] > max_num){
			max_num = num[i];
		} 
	} 
	max_num ++; // 因为会出现0的情况, 将最大值加1就可以, 相当于向右偏移1, 当然 多加也无所谓 
	
	// 树状数组解法 
	
	// 更新 由左向右扫描 
	for(int i = 0; i < N; ++i){
		update(tree_num_left, num[i] + 1, 1); 
		// 若是 替换成 ans_num_right[num[i]] 数据将变得有序 , 但是后续合并和计算复杂度 最差 将会变高10倍 
		// 而且并不需要有序, 只需要结果就好 
		ans_num_left[i] = tree_sum(tree_num_left, max_num) - tree_sum(tree_num_left, num[i] + 1); 
	}
	// 更新 由右向左扫描 
	for(int i = N - 1; i >= 0; --i){
		update(tree_num_right, num[i] + 1, 1); 
		ans_num_right[i] = tree_sum(tree_num_right, num[i]); 
	}
	// 合并 
	unsigned long long ans_2 = 0;
	for(int i = 0; i < N; ++i){
		ans_num_right[i] += ans_num_left[i]; 
		// 求值
		ans_2 += sum_0_to_n(ans_num_right[i]);
		printf("%ld ", ans_num_right[i]);
	} 
	puts("");
	printf("树状数组求得:%lld\n", ans_2);
	 
	
	gui_pai(0, N - 1);
	// 使用逆序对 计算结果
	for(int i = 0; i < N; ++i){
		ans += sum_0_to_n(ni_num_sum[i]);
	} 
	for(int i = 0; i < N; ++i){
		printf("%ld ", ni_num_sum[i]);
	} 
	puts("");
	printf("归并排序求得: %lld\n", ans);
	
	
	
		
	
	return 0;	
}



运行截图:
在这里插入图片描述

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

梦鸢MoYuan

谢谢投喂!!!QWQ!!

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值