【蓝桥杯】 历届试题 小朋友排队(树状数组)—— 酱懵静

历届试题 小朋友排队

问题描述
n 个小朋友站成一排。现在要把他们按身高从低到高的顺序排列,但是每次只能交换位置相邻的两个小朋友。每个小朋友都有一个不高兴的程度。开始的时候,所有小朋友的不高兴程度都是0。如果某个小朋友第一次被要求交换,则他的不高兴程度增加1,如果第二次要求他交换,则他的不高兴程度增加2(即不高兴程度为3),依次类推。当要求某个小朋友第k次交换时,他的不高兴程度增加k。

请问,要让所有小朋友按从低到高排队,他们的不高兴程度之和最小是多少
(如果有两个小朋友身高一样,则他们谁站在谁前面是没有关系的)

输入格式
输入的第一行包含一个整数n(1<=n<=100000),表示小朋友的个数。
第二行包含 n 个整数 H1 H2 … Hn(0<=Hi<=1000000),分别表示每个小朋友的身高
输出格式
输出一行,包含一个整数,表示小朋友的不高兴程度和的最小值

样例输入
3
3 2 1
样例输出
9
样例说明
首先交换身高为3和2的小朋友,再交换身高为3和1的小朋友,再交换身高为2和1的小朋友,每个小朋友的不高兴程度都是3,总和为9



---分割线---



分析:
看到这道题的第一反应便是冒泡排序,因为形成升序排列的情况和冒泡排序的过程完全贴切。而我也抱着试一试的心态(因为我还是不确定是否用冒泡排序得到的不高兴程度是最小)用冒泡排序写了一个程序提交,得了50分[/笑哭]。后面那50
分的数据超时了,不过想想确实会超时。
冒泡排序如下(这是每趟选小的上浮):

for(int i=0;i<n-1;i++)
		for(int j=n-1;j>i;j--)
			if(h[j-1][0]>h[j][0])	//h[j][0]保存的是序号为j的小朋友的身高信息,h[j][1]保存的是其不高兴度
			{
				int temp=h[j-1][0];
				h[j-1][0]=h[j][0],h[j][0]=temp;
				temp=h[j-1][1];
				h[j-1][1]=h[j][1],h[j][1]=temp;
				h[j-1][1]++,h[j][1]++;		//不高兴度加1
				sum+=h[j-1][1]+h[j][1];		//叠加不高兴度
			}

可以看到这是一个O(n2)的时间复杂度,那么在10 0000的极限下,最多会发生100 0000 0000次比较和
100 0000 0000 * 4 交换,显然这程序是跑不动的
并且刚才谈到了冒泡排序是否一定会得到最优解(即不高兴度最小)的问题,通过查阅资料发现并不会

问题到了瓶颈,于是开始另寻出路。后来在网上找到了关于逆序对的相关知识,才知道这是一道寻找逆序数的题目,比如题目中的3,2,1。和3有关的逆序对为<3,2>和<3,1>,和2有关的逆序对为<3,2>和<2,1>,和1有关的逆序对为<3,1>和<2,1>,为了完成排序,任何一个逆序对的两个元素都必须交换一次,于是可知,每个小朋友都完成了两次交换,从而可以计算出不高兴度为[(1+2)+(1+2)+(1+2)]=9

说白了就是针对每一个元素,看看它之前有几个大于它的元素,之后有几个小于它的元素,然后记录这个元素在逆序对中出现的次数,也就是含这个元素的逆序对的个数,然后再将每个元素的不高兴程度相加即可

于是现在问题的关键便落在了查找逆序对个数的身上,最容易想到的还是暴力搜索,不过一样的,这个查找过程也是O(n2)级的,超时无疑!问题再一次到了瓶颈。

涉及到数组,求和,便立马想到了树状数组(有不清楚这个数据结构的同学们,建议先看一下我的前一篇关于树状数组的博客,你才会更清楚本题的在选用数据结构和算法时的巧妙之处点击链接)!!!可是这里的问题是,树状数组求和怎么与逆序对建立起关系呢?
如果现在还依然用前面的数组(即用h[ i ][ 0 ]表示序号为 i 的小朋友的身高,h[ i ][ 1 ]表示其不高兴度),那么确实和树状数组扯不上半毛钱关系。
此时便需要开拓一下思维了。
试想,对于身高,其录入的前后顺序,其实在逻辑上是能够反应出物理上的前后关系的!!!
这样看来,我们是可以利用输入顺序来将每个人的身高视为一个树状数组的索引。然后将这个身高信息更新到树状数组中,至于更新的值,我们将其定为1(意义就是代表一个人)。如此一来,在每次读取一个身高信息时,我们都去求当前树状数组的和(也就是查询从1到以当前身高为上限的所有元素之和)。
当我们顺序读取身高信息时,查询结果便仅仅是针对当前小朋友前面的那些人的身高情况,进一步根据树状数组的求和规则知其表示了“在读取当前身高小朋友前,有多少个身高低于当前小朋友的”。
但是逆序对不是有两个部分么?
1.前面身高高于当前小朋友的;
2.后面身高低于当前小朋友的;
你这里查询的结果不对嘛!
这很简单!你在读取小朋友身高信息时不是用了一个循环么?那你直接用控制循环次数的这个变量(建议这个变量从1开始,这样更加符合逻辑)减去当前读取到的前面身高低于当前小朋友的查询结果,不就得到了前面身高高于当前小朋友的数量了么,也就是逆序对的第一部分。
同理,你可以反过来读取这个身高信息,那么这时候的查询结果便仅仅是针对当前小朋友后面的那些人的身高情况,即“在当前这个身高的小朋友的后面有多少个身高低于他的”。这也也正是逆序对的第二部分。

下面我以题目给出的例子为例,来展示一下这个过程(注意:由于树状数组的索引是从1开始的,而题目中小盆友的身高可以为0(真是长见识了),所以我们将每个小盆友的身高加1然后作为树状数组的下标,再将数值1存到相应的位置):

第一次读入3,此时读入的数据量为1个,变成这样

C[1] C[2] C[3] C[4] C[5] C[6] C[7] C[8]
0 0 0 1 0 0 0 0

可以看到sum(C[1],C[4])=1(可以由树状数组的统计数组得到),即身高小于等于4的数只有1个(本身),也就是说当输入第一个数字3的时候没有比它更小的数字存在(即在这个身高的小朋友前没有比他更矮的),这时我们用“输入数字总数-sum(C[1],C[4])”得到的便是在这身高的小朋友之前比他更高的人数。当然,这里1-1=0,也就是说大于3的数字的个数为0(即在这个身高的小朋友之前也没有比他更高的),我们令reserverOrder[1]=0(该序号位置的逆序数,注:仅算了其前面身高高于他的)。

第二次读入2,此时读入的数据量为2个,变成这样

C[1] C[2] C[3] C[4] C[5] C[6] C[7] C[8]
0 0 1 1 0 0 0 0

可以看到sum(C[1],C[3])=1,仍然不存在比它小的数,但此时输入的数据总量为2,而2-1=1,也就是说,存在一个数在2之前并且大于2(换言之,存在一个身高高于该小朋友的,并且在他的前面)这个数当然就是3,我们令reserveOrder[2]=1

第三次读入1,此时读入的数据量为3,变成这样

C[1] C[2] C[3] C[4] C[5] C[6] C[7] C[8]
0 1 1 1 0 0 0 0

可以看到sum(C[1],C[2])=1,仍然不存在比它小的数,但此时输入的数据总量为3,而3-1=2,也就是说,存在两个数在1之前并且大于1(换言之,存在两个身高高于该小朋友的在其之前)这两个数当然就是2和3,我们令reserveOrder[3]=2

到此,我们已经算出了每个数前面的较大的数的个数,即算出了每个序号位置上,其前面存在的身高高于他的个数。现在我再反过来,先插入1,再插入2,再插入3(事前我把每个小朋友身高的数据存进hight[]中)但这次我们不再用总数减去sum了,而直接求sum,求出来的即是每个序号位置上,其后面身高低于他的个数,然后将得到的数值累加到相应的reserveOrder[i]中,最终便得到了每个位置上的逆序数(两部分:前面的更高的、后面的更矮的)。于是可以得到reserveOrder[1]=2,reserveOrder[2]=2,reserveOrder[3]=2,分别对应身高:hight[1]=3,hight[2]=2,hight[3]=1

求得了每个小盆友需要被移动的次数(即逆序数)后,我们就可以计算其不高兴程度了,这里实际上可以事先打个表,就是将被移动 n次后的不高兴值全算出来放进一个数组中,然后之后就可以直接用了。这里,我们将其存到total[]数组中,而对于本题而言total[2]=3,所以总不高兴值就是3+3+3=9

需要注意的是如果重复的数字出现怎么办(也就是遇到身高相同的怎么处理),如果出现,实际上出问题的会是求每个数之前较大数的那部分,因为用到了数的总个数,如果出现一样的数,就会导致相减后的结果偏大。但是仔细思考,对于求“每个身高前面较大的数”时,我们用到了:第i次输入-对应这次的前n个人的和。而正好当输入重复的身高时,i++了,而C[n]也++了,其实这样相减后得到的结果刚好就代表着其前面较大的数。即:重复数字并不会影响本程序的正确性

下面给出本题的完整代码:


---分割线---


#include<iostream>
using namespace std;

const int N=100010;
const int MAXH=1000010;
long long total[N],ans;				  //分别用于打表和输出最终值 
int hight[N];	      	 			  //用于记录每个小朋友的升高 
int C1[MAXH],C2[MAXH],reverseOrder[N];//分别用于装第1次“前面的高个子”的树状数组,第二次“后面的矮个子”的树状数组,以及对应每个序号上的小朋友的逆序对数 

void creat()						  //打表,用于创建好所有可能的不高兴度的累加值 
{
	total[0]=0;
	for(int i=1;i<N;i++)
		total[i]=total[i-1]+i;
}

int lowbit(int n)					  //用于完成取最低位1——也可以说是求2^k次方 
{	return n&(-n);	}

void add(int pos,int num,int *C)//完成单点更新操作 
{
	while(pos<MAXH)
	{
		C[pos]+=num;
		pos+=lowbit(pos);
	}
}

int sum(int pos,int *C)				  //求前pos位的和(区间查询) 
{
	int sum=0;
	while(pos>0)
	{
		sum+=C[pos];
		pos-=lowbit(pos);
	}
	return sum;
}

int main()
{
	int n;
	creat();
	cin>>n;
	for(int i=1;i<=n;i++)
	{
		cin>>hight[i];
		add(hight[i]+1,1,C1);				 //+1时为了防止身高为0时的情况,因为树状数组是从1开始的 
		reverseOrder[i]=i-sum(hight[i]+1,C1);//此时reverseOrder[i]存放的仅是在序号为i的前面的更高的逆序对数 
	}
	for(int i=n;i>0;i--)
	{
		add(hight[i]+1,1,C2);
		reverseOrder[i]+=sum(hight[i],C2);	//此时reverseOrder[i]添加的是在序号为i的后面的更低的逆序对数,注意,这里用sum函数时其第一个参数为hight[i],不能+1,因为这里不能算自己 
	}
	ans=0;
	for(int i=1;i<=n;i++) ans+=total[reverseOrder[i]];
	cout<<ans<<endl;
	return 0;
}

发布了30 篇原创文章 · 获赞 67 · 访问量 3049

猜你喜欢

转载自blog.csdn.net/the_ZED/article/details/100162356