插入排序的优化【不靠谱地讲可以优化到O(nlogn)】 USACO 丑数

  首先我们先介绍一下普通的插排,就是我们现在一般写的那种,效率是O(n^2)的。

  普通的插排基于的思想就是找位置,然后插入进去,其他在它后面的元素全部后移,下面是普通插排的代码:

  

 1 #include<iostream>
 2 #include<fstream>
 3 #include<stdio.h>
 4 using namespace std;
 5 int a[200000];
 6 int p[200000];
 7
 8 int main(){
 9     ios::sync_with_stdio(false);
10     int n;
11     cin>>n;
12     for(int i=1;i<=n;i++){
13         cin>>a[i];
14     }
15     int len = 1;
16     p[len] = a[1];
17     for(int i=2;i<=n;i++){  //插入第i个元素
18         int k = a[i];
19         int j;
20         for(j=len;j>=1;j--){
21             if(k < p[j]){
22                 p[j+1] = p[j];
23             }else
24                 break;
25         }
26         p[j+1] = k;
27         len++;
28     }
29     for(int i=1;i<=n;i++)
30         cout<<p[i]<<" ";
31     return 0;
32 }

  可以看出,这个代码的复杂度应该是T((1+n)*n/2)=O(n^2)的,让我们仔细分析到底时间费在哪里。

  1.查找过程太费时间,仔细观察我们就可以发现,查找它应当插入元素的位置的时间是O(n)的,我们可以想办法优化成O(log2n),没错,二分查找,于是我们写出了下面这份代码。

 1 #include<iostream>
 2 #include<cstdio>
 3 #include<cstdlib>
 4 #include<vector>
 5 #include<algorithm>
 6 using namespace std;
 7 int a[200000];
 8 vector <int> vec;
 9
10 int main(){
11     ios::sync_with_stdio(false);
12     int n;
13     cin >> n;
14     for(int i =1;i<=n;i++)
15         cin>>a[i];
16     vec.push_back(a[1]);
17     for(int i=2;i<=n;i++){
18         int k=a[i];
19         int j=lower_bound(vec.begin(),vec.end(),k)-vec.begin();
20         vec.insert(vec.begin()+j,k);    //这是O(n)的
21     }
22     for(int i=0;i<n;i++){
23         cout<<vec[i]<<" ";
24     }
25     return 0;
26 }

  显然,这份代码的复杂度,应该是T(n*(log2n+n))=O(n^2)的,但是有一点好的,就是不会被某些专门卡插排的数据卡。

  我们再分析一下另一个耗时间的地方。

  2.将所有元素前移的时间的上界是O(n)的,我们也要想办法优化到O(logn)。若我们只针对这一点优化,那么我们可以想到一种比O(logn)更快的数据结构来优化这一点,链表。

  如果我们用链表来储存,我们完全没必要将元素前移,只要连接起来,是O(1)的。不难写出下面这份代码

 1 #include<iostream>
 2 #include<cstdio>
 3 #include<cstdlib>
 4 #include<cstring>
 5 using namespace std;
 6 struct node{
 7     int data;
 8     node* next;
 9 };
10 node *start=new node,*end = new node;
11 int a[200000];
12
13 int main(){
14     end->data = -1;
15     ios::sync_with_stdio(false);
16     int n;
17     cin >> n;
18     for(int i=1;i<=n;i++)
19         cin>>a[i];
20     start->next = end;
21     for(int i=1;i<=n;i++){
22         int k=a[i];
23         node *p = start;
24         while(p->next!=end&&p->next->data < k)
25             p = p->next;
26         node *q = p->next;
27         node *now = new node;
28         now->data = k;
29         now->next = q;
30         p->next = now;
31     }
32     node *p =start->next;
33     while(p!=end){
34         cout<<p->data<<" ";
35         p = p->next;
36     }
37     return 0;
38 }

显然,这份代码也是O(n^2)的,慢在哪了?又是查找。

  所以我们现在要做的事,就是把二分融合在链表里面,这就设下了一个大难关,但是,对数级的优化又启发了我们,我们必须在有限的次数(可以预知)内筛掉一半以上的数。

  我们不妨考虑一个简化版的问题:给定n个有序的元素,现在要插入1个元素,用链表实现,效率是O(logn)怎么搞。

  对这个问题,我有两个方法:

    法1:在读入的时候预处理每个点到另一个点的中点的位置,空间复杂度高达O(n^2),铁定MLE。我们不得不另寻他法

    法2:我们不妨分层存储,比如对于一个8个元素的链表,我们可以设计出以下数据结构:

    显然,层数是log2n层的,每一层的元素个数都是上一层的1/2,那么我们的查找显然是O(logn)的,我们从最顶上一层开始找,如果下一个不是我想要的,那么就排掉了一半,往下面走,以此类推。

    这样的空间复杂度是O(n)的,显然第一层是有n个元素,此外每一层的元素个数都是上一层的1/2,回忆二叉树的知识,若我们在最上面再补一层,那么就有n+n-1=2n-1,那么再减去刚才补上的一层,就是2n-2个元素,所以空间复杂度是O(n)的。

  推广这个问题,我们可以发现,这样子只能对某个固定的链表使用,而不能动态地更改,于是我们就想到了随机数。对每个元素,我们有1/2的几率让它成为上面一层的元素,这样的话每层元素的期望也就跟这个差不多,可以证明,这样做插入的复杂度是O(logn)<-递归log2n层,查找的复杂度也是O(logn),那么插入排序的复杂度就变成了O(nlogn),下面是代码:

  1 #include<iostream>
  2 #include<cstdio>
  3 #include<cstdlib>
  4 #include<ctime>
  5 #include<cstring>
  6 using namespace std;
  7 struct node{
  8     int data;
  9     int level;  //所在层数
 10     node* under;  //下一层的相同结点
 11     node* next;
 12 };
 13 struct llist{   //level of list
 14     int level;
 15     node* start;
 16     llist(){
 17         start = new node;
 18         start->data = -1;
 19     }
 20 };
 21 int siz[200]; //定义为一个天文数字
 22 int a[200000];
 23 int log2(int);
 24 node* end = new node;
 25 int top = 1; //表示层数
 26 llist le[200];
 27 int want,n;
 28 int insert(int now,int lev,node* place,node *last = NULL){
 29     node *f = place;
 30     while(f->next->data < now && f->next!=end)
 31         f = f->next;
 32     if(lev != 1){
 33         int pd = insert(now,lev-1,f->under,f);
 34         if(pd == true){
 35             siz[lev]++;
 36             int trid = rand()%2;
 37             if(trid == 1){
 38                 node* p = f->next;
 39                 node *q = new node;
 40                 q->data = p->data;
 41                 q->level = p->level+1;
 42                 q->under = p;
 43                 if(last!=NULL){
 44                     q->next = last->next;
 45                     last->next = q;
 46                 }else{
 47                     q->next = end;
 48                     le[lev+1].start->next = q;
 49                 }
 50                 if(siz[lev+1] == 0)
 51                     top = lev+1;
 52                 siz[lev+1] = 1;
 53                 return true;
 54             }
 55         }else
 56             return false;
 57     }else{
 58         siz[1]++;
 59         node *p = new node;
 60         p->level = 1;
 61         p->data = now;
 62         p->next = f->next;
 63         f->next = p;       //移花接木
 64         int trid = rand()%2;
 65         if(trid == 1&&lev<want){
 66             node *q = new node;
 67             q->data = p->data;
 68             q->level = p->level+1;
 69             q->under = p;
 70             if(last!=NULL){
 71                 q->next = last->next;
 72                 last->next = q;
 73             }else{
 74                 q->next = end;
 75                 le[lev+1].start->next = q;
 76             }
 77             if(siz[lev+1] == 0)
 78                 top = lev+1;
 79             siz[lev+1]++;
 80             return 1;
 81         }else
 82             return 0;
 83     }
 84 }
 85
 86 int main(){
 87     freopen("sort.in","r",stdin);
 88     freopen("sort.out","w",stdout);
 89     srand(time(NULL));
 90     end->data = 2147483647;
 91     end->level = -1; //确认身份
 92     ios::sync_with_stdio(false);
 93     cin >> n;
 94     want = log2(n);
 95     for(int i=1;i<=want;i++){
 96         le[i].level = i;
 97         le[i].start->level = i;
 98         le[i].start->next = end;
 99         if(i!=1){
100             le[i].start->under = le[i-1].start;
101         }
102     }
103     for(int i=1;i<=n;i++)
104         cin>>a[i];
105     node* q = new node;
106     q->data = a[1];
107     q->next = end;
108     le[1].start->next = q;   //为第一层加上一个结点
109     siz[1]++;
110     for(int i=2;i<=n;i++){
111         insert(a[i],top,le[top].start);  //插 ♂入 a[i]
112     }
113     node *p = le[1].start;
114     p = p->next;
115     while(p!=end){
116         cout<<p->data<<" ";
117         p = p->next;
118     }
119     return 0;
120 }
121
122 int log2(int n){
123     int val = 1,k = 0;
124     while(val*2 < n){
125         k++;
126         val*=2;
127     }
128     return k+1;
129 }

多美妙的代码啊。

下面是它与其它几种排序方法在时间上的比较:

首先是对于测试点的说明:

对于测试点1:n=100000,专门卡插入排序的测试点,因为是从小到大排序所以自然是从大到小的数据喽。

对于测试点2:n=1000,数据随机。基本上都能过

对于测试点3:n=10000,数据随机。卡卡常还是能过

对于测试点4:n=50000,数据随机。理论上分块能过,人懒就没写分块。

对于测试点5:n=100000,数据随机。只有O(nlogn)能过

STL都跑得快,最慢的跑了0.09秒。heap_sort最慢的0.24秒。插排优化最慢的0.43秒(常数略大)。

普通插排最慢的32.18秒,被第一个点卡了。链表插排没过最后一个点(常数太大)。

还有一个我删了的是二分优化的普通插排(数组实现O(n^2)),最慢的跑了3秒。

下面是一道实战题,丑数(USACOtraining 第三章)

  对于一给定的素数集合 S = {p1, p2, ..., pK},
  来考虑那些质因数全部属于 S 的数的集合.这个集合包括,p1, p1p2, p1p1, 和 p1p2p3 (还有其它).
  这是个对于一个输入的 S 的丑数集合.
  注意:我们不认为 1 是一个丑数.
  你的工作是对于输入的集合 S 去寻找集合中的第 N 个丑数.longint(signed 32-bit)对于程序是足
  够的.
  程序名: humble
  读入说明:
  第 1 行: 二个被空间分开的整数:K 和 N , 1<= K<=100 , 1<= N<=100,000.
  第 2 行: K 个被空间分开的整数:集合 S 的元素
  样例读入 (文件名 humble.in)
  4 19
  2 3 5 7
  输出说明
  单独的一行,写上对于输入的 S 的第 N 个丑数.
  样例输出 (文件名 humble.out)
  27

对于这道题,我们容易想到的一种做法是用堆来做,代码我就不贴了(其实就是我删了懒得重打了)。容易证明,这样做的复杂度是O(2^(k)*k*n)的,空间也是会爆掉的,所以这样做不行。

我们试着改变它,因为它是求第n小,所以假设一个由丑数构成的序列大于100000个元素,那么大于100,000的元素必然不可能是我想要的丑数(借用刘汝佳的一句话:想一想,为什么)。那么我们可以构建一个ans链表,来储存目前枚举出的每一个丑数,初始状态,ans数组仅有S集合中的元素,然后我们扫描一遍,求第n小,我们可以每次把当前第一小的数字出链表,同时加入它与它后面的数的乘积入链表(想一想为什么不加入它和前面的乘积,或者它和后面好几个的乘积),这个的复杂度显然是O(k*logn)的,如果这个链表大小超过了n,那么就砍尾巴喽。那么这个的复杂度就是O(n*k*logn),卡卡常还是能过的嘛。(毕竟是USACO,没像NOIP那样为难oier)。代码的话,我不会打!!!!毕竟这么晚了,改天有空了我会把代码补上。放心这次不会和前面那个三分,莫比乌斯一样烂尾的!

      

时间: 2024-10-09 07:19:28

插入排序的优化【不靠谱地讲可以优化到O(nlogn)】 USACO 丑数的相关文章

不靠谱的锤子手机

       在五月十五日,传说中的锤子手机终于发布了.龙哥是在昨天才在网上看到的整个发布会的视频,之前其实对这个事情是不怎么关注的.只是觉得一个是风口浪尖上的人物,一方面是广大群众和众多的水军而已.        而对于老罗这个人,我印象里还是那个说话风趣的英语老师.在得知要做手机的那一刻,也觉得是件很不靠谱的事情.别无他想!但是,昨天看了完整的发布会之后,我的想法完全是不一样的.        首先,对于目前的智能手机来说,龙哥也算是一个资深的玩家了.之前断断续续地用过了不少的智能手机,从i

友情变“友尽” 微信群发测试好友不靠谱

“好友测试”在七夕佳节掀起了微信圈子里的新一轮狂轰滥炸.针对这种“既愚蠢又野蛮”的病毒式骚扰,无数网友表示“友尽”(友谊走到尽头了),本来没想删的,看到发这个的就马上就删了. “好友测试”到底是否靠谱?它为何在一夜之间“血洗”朋友圈? 崩溃 遭500条测试“突袭” “使用方法,打开微信,设置,通用,功能,群发助手,全选,试下群发 ,看好友还在不.不在的就可以删了!弹出来好友验证就证明对方把你删了,你也删了吧……亲,谢谢你!如有打扰,请见谅!”从上个周末开始,微信上突然出现一条名为“好友测试”的微

转自36Kr --不靠谱产品经理养成

我是一个没做过成功产品的产品经理,至今负责过的项目并不少,但不是夭折就是已经奄奄一息.我不是王兴,不是张小龙,不是老罗,更不是乔布斯.夜深人静的时候,我常常在马桶上坐着反省自己存在的意义是啥,把老板的钱烧完吗? 每当我快被挫败感折磨死的时候,我会通过各种变态的方式给自己打鸡血--比如写这篇文章.(以下纯属个人观点,欢迎吐槽谢绝丢砖) 首先我想谈一下,产品为什么会走到「不靠谱」那一步.这种悲剧就跟恋爱一样,从一开始就注定了.至于这个「开始」,主要有以下几种情况: 1.老板不靠谱.这个忒别严重,因为

一道题识别不靠谱的程序员

这是一道关于符号调试器实现原理的讨论题.目的不是考察调试经验或者调试器设计,而是想借助这个话题来考察候选人的计算机基础知识和工作性格.一般这样开头: 有用过调试器吧?都用过那些功能?接下来和候选人探讨调试器背后的实现原理,比如如何实现查看变量,查看内存,查看调用栈,如何实现断点等. 选择这个话题的原因之一是有话可说,几乎所有程序员都有使用调试器的经历;其二是绝大多数人都没有亲自设计调试器的机会,反馈出来的信息能反映候选人的真实水平. 好处之一,识别没有钻研精神的候选人 我理解很多工程师在生产活动

为什么我觉得做在线教育平台的都不靠谱

写在前面的话: 本文是我在2014年,也就是2年前应教育行业媒体"多知网"之邀所写的一篇在线教育行业分析+评论.虽然写得早,但文中所有观点,站在今天看,仍然是完全适用的,并无不同.包括文中提到的很多观点,我相信今天也已经都得到了验证. 在2014年,我就感觉在线教育平台是个很不靠谱的模式,此后也曾就此与跟谁学CEO陈向东.果壳网CEO姬十三等人有过交流.可惜,跟谁学还是走进了我所预见的一些坑. 今天此文重发,是因为在三节课同学群里,我们又一次听到了有很多在线教育平台开始裁员的消息.于是

苦劳为什么不靠谱

一件事是劳是逸,一张嘴就能说出两个结果.可是别人有时看得出来,一个人真疲劳了,是能写在脸上的.而且每个人对于疲劳的承受可能也不同,好像百米选手跑马拉松. 所以劳逸也是相对的. 有些人干了很多,但能坚持,不显的劳,而无功. 有些人干的不多,而说的一步一个脚印,而得功绩. 作为管理者要明眼是非,看结果.一定不要看下属是否幸苦. 辛苦是可以体谅,但一定不能表扬. 苦劳为什么不靠谱,布布扣,bubuko.com

假如BOSS给了PM一个不靠谱的需求

假如BOSS给了一个PM不靠谱的需求,那么站在PM的角度上,我会怎么处理呢?在知乎上看了好多达人以及同行的真知灼见,自己也是有一番小的思考,这里做一个总结吧 其实,这种不靠谱的产生的最主要的原因无非是所处的位置和立场不同,因而会发出不同的声音. 作为一个BOSS,更可能是从当前整个市场的行情,走向去看问题,觉得我们应该做一个XXX产品,实现XXX需求,然后达到XXX目标. 作为一个PM,更多的是考虑一个产品满足这种需求所需要的成本,实现细节以及"性价比",如果PM再厉害点的话,可能技术

中国开源不靠谱,谈何服务万众创新?

2015年6月26日至27日,由中国开源软件推进联盟(COPU)主办,开源中国社区协办的"第十届开源中国开源世界高峰论坛"(以下简称"论坛")在广州召开.本届论坛的主题是:"构建开源平台,服务万众创新". 根据官方网站的报道,本届论坛还举行了"万众创新开源公共服务平台"(简称"开源平台"),由中国开源软件推进联盟.中国数字标识与安全产业联盟以及寄云科技三家联合共建"万众创新开源公共服务平台&quo

电商海外购靠不靠谱

电商海外购靠不靠谱桌抓滓紫踪昨装状驻灼宗桩孜浊综电商海外购靠不靠谱 http://www.songtaste.com/user/10249177/info http://www.songtaste.com/user/10249178/info http://www.songtaste.com/user/10249183/info http://www.songtaste.com/user/10249185/info http://www.songtaste.com/user/10249188/i