学习总结:
今天学习状态不好,一度怀疑我是否真的适合这条道路,这本书全在讲解如何优化,可我也却是只觉得有解就很不容易了,而且天天与代码在一起……我是否喜欢代码呢?
心境紊乱,不知道该如何平静。确实发现我是一个比较情绪化的人,偏偏情绪还会影响行动力,真是羡慕哪些稳着看书打代码的人,仿佛什么都激不起内心的涟漪……
嗯,废话到此为止,既然我还坐在这里,即使状态不好,也不能任由自己放弃,该干什么还是要努力干的。
今天学习的是排序,小小一个排序居然这么多内容!看的我眼花缭乱,中位数、离散化、逆序对……一些看起来很简答也是在题目中难以应用的东西。必须学会找到题目之间的共同点,才能解决这一个问题。
练习赛:其中一个过不了的题CodeForces - 224B
e题还是测试16超时!!!啊!!超时!!所以说,最讨厌优化了啦!!!我感觉明明已经够快的了!还用了队列和set!!
听了他们的解释之后,好厉害啊!崭新的视野,终于会了这个题:从前往后找到第m个不同的,然后从这个位置向前找,继续找第m个不同的就可以了。
我的思路是找区间段最小的那个区间,区间大小从所求大小开始慢慢变大,然后循环,所以造成了超时,实际上是找左区间最小的!(那么问题来了,为什么我就ac了十五个样例呢!)
排序:
1.sort排序补充
sort()是公认的最喜欢的排序——不需要动脑子,带入数据就可以了。
sort众所周知,是一种快速排序,好的时候只需要o(nlogn)就可以求得答案,但是面对大数据,却也有可能变成o(n^2)的时间复杂度。而且,不同的sort应用所造成的时间复杂度是不一样的。
①sort(array+1,array+1+n);->最快
②sort(array+1,array+1+n,cmp);->这个就慢了很多,因为引入了bool返回类型的函数来进行特定顺序的排序。
③sort(struct+1,struct+1+n);->比①慢,比②稍快,重载了结构体里面的<或者>号
④sort(struct+1,struct+1+n,cmp);->用类里面的函数来排序,倒是跟①差不多的速度了,慢了一点点。
2.其他排序(想知道具体怎么排的请点击链接)
1.o(n^2)时耗:选择排序、插入排序、冒泡排序
2.o(nlogn)时耗:堆排序、归并排序(两个数组之间的合并排序,常用于在微修改下求逆序数)、快速排序(选定基值,大的放右边,小的放左边)
3.特殊的排序,时耗与数列长度有关。
3.离散化
排序算法的应用之一:离散化。
离散化就是把无穷大集合中的若干个元素映射为有限集合中便于统计的方法,这些映射与大小无关,只与他们的相对位置有关。
b[++m]=a[i];(去掉了a之间的空格,把他们密集化了,但是a的数值带了下来。)
这样的话,循环就会变小了(从1到a的最后一个数这个区间段自然变小了),不知道能不能随便使用。
要想获得某个a的位置,需要用二分查找带入这个a[i]即可。
4.中位数
中位数很好求,可以用来求某个坐标到其余所有位置的距离之和什么时候最小。
奇数个数时:中位数在a[(n+1)/2]的位置。
偶数个数时:中位数在a[n/2]的位置。
有个七夕祭的例题好难看不懂不想看惹……咳咳咳,没有的事情。但是它的一个推到过程中的一个纸牌均分的题倒是看懂了(老师讲过一次还不会!还不会!实在该打啊!)
纸牌均分思路:求出平均值,然后每一组减去这个平均值,大为正,小则为负,正的将排给下一组,负的从下一组拿牌。
动态维护中位数:
题意:依次读入一个整数序列,每当读入整数个数为奇数时,输出已读入整数构成序列的中位数。
解决方法:①“对顶堆”的在线做法②“链表+Hash”的离线做法。
方法①“对顶堆”算法:建立两个二叉堆:一个大根堆,一个小根堆。
在依次读入这个序列的过程中,设当前序列长度为m,始终保持这两个性质:
(1)序列从小到大排名为1~m/2的整数放在大根堆中(堆顶的是其中的最大值)
(2)序列从小到大排名为m/2+1~m的整数存在小根堆中(堆顶是其中的最小值)
任何时候,如果某个堆中的元素过多,那么就取出该堆的堆顶插入另一个堆,这样以来,序列中的中位数就是小根堆的堆顶(实则是编号m/2+1的数,它在小根堆中,所以结果是小根堆。
每次读入一个数值x之后,若x比小根堆堆顶小,则插入大根堆,否则插入小根堆,再进行维护,保证序列的两个性质即可。
5.第k大数
改进快排,变成倒叙的排列,那么第k个数就是第k大的数。
每快排一次返回当前的基准线i:即第i大的数,如果i+1>h,说明第k大在前面(第八大当然是小于第六大,且为倒叙,所以第六大在前面,解释给我自己听~可能只有我迷糊了一下),在快排strat~i-1。i+1<h则相反。直到找到i+1==k,这个i+1代表的数->temp就是第k大的值(这是基准线,每次快排最后都会被换到这里。
6.逆序对
逆序对,根据大一线代知识,排在前面的数数值却大于排在后面的数,这样的一对数就构成了逆序对。
逆序对的求法有
(1)暴力(这个需要o(n^2)的时间复杂度),耗时就太长了,实际上等于冒泡排序冒几次泡。
(2)归并排序(只需要o(nlogn)的时间复杂度),标准求解方法。
(3)还有树状数组(以前学过,现在也忘得差不多了!!)
代码:
#include<iostream>
#include<cstdio>
using namespace std;
int n,a[100005],b[100005],ans=0;
void merge(int l,int r)
{
if (l>=r) return;
int mid=(l+r)/2;
merge(l,mid);//左右小区间的归并
merge(mid+1,r);
int i=l,j=mid+1,k=0;
while(i<=mid&&j<=r)//左右半区间比较
{
if(a[i]<=a[j])
b[k++]=a[i++];//正常的左边数字小于右边的数字
else//左边数字大于右边数字的时候
{
b[k++]=a[j++];//放小的那个
ans+=mid-i+1;//因为经过递归的回溯过程,左边应当都是小的哪些(无序),右边大。所以i~mid的数字全都比这个a[j]小,全都是逆序数
}
}
//左右区间有一个到底了,剩下的都比较大了
while(i<=mid)//先把小的装进去(顺序绝对不能错!!)
b[k++]=a[i++];
while(j<=r)//再装大的。
b[k++]=a[j++];
for (int i=0;i<k;i++)
a[i+l]=b[i];//b是临时的,a还要继续排列,这时的a已经是小的在前,大的在后了。
}
int main() {
scanf("%d",&n);
for(int i=1;i<=n;i++)
scanf("%d",&a[i]);
merge(1,n);
printf("%d\n",ans);
return 0;
}
逆序对虽看起来不堪大用,其实是很有作用的,在某些一排数字组成求解的题目中很厉害(呵,看不出来什么时候能用逆序对)。
例题1:一个序列a,只允许交换相邻两个数,问至少几次交换可以把数字从小到大排列。
解:每交换一次,说明前面的数字大于后面的数字,交换之后逆序对数目减一,所以逆序对的数目就是交换的数目啦~总之就是冒泡排序的次数呗。醒醒,题目不会让你冒泡排序过的,必然超时(太过分的题目只让ac一个)!!
例题2:奇数码问题。八个数一个空格,排成一个九宫格,空格可以与上下左右的数字交换。给定一个原九宫格,和变换后的九宫格,问原九宫格能不能经过有限次变换变成第二个九宫格。
解:把这些九宫格写成一行,发现不论经过什么变换,逆序对的个数的奇偶性是不变的(每次变换都会改变偶数个数),所以只要求出两个九宫格的逆序对的奇偶性,比较是否相等就可以啦。