插入排序的优化非希尔【不靠谱地讲可以优化到O(nlogn)】 USACO 丑数
首先我们先介绍一下普通的插排,就是我们现在一般写的那种,效率是O(n^2)的。
普通的插排基于的思想就是找位置,然后插入进去,其他在它后面的元素全部后移,下面是普通插排的代码:
#include<iostream> #include<fstream> #include<stdio.h> using namespace std; int a[200000]; int p[200000]; int main(){ ios::sync_with_stdio(false); int n; cin>>n; for(int i=1;i<=n;i++){ cin>>a[i]; } int len = 1; p[len] = a[1]; for(int i=2;i<=n;i++){ //插入第i个元素 int k = a[i]; int j; for(j=len;j>=1;j--){ if(k < p[j]){ p[j+1] = p[j]; }else break; } p[j+1] = k; len++; } for(int i=1;i<=n;i++) cout<<p[i]<<" "; return 0; }
可以看出,这个代码的复杂度应该是T((1+n)*n/2)=O(n^2)的,让我们仔细分析到底时间费在哪里。
1.查找过程太费时间,仔细观察我们就可以发现,查找它应当插入元素的位置的时间是O(n)的,我们可以想办法优化成O(log2n),没错,二分查找,于是我们写出了下面这份代码。
#include<iostream> #include<cstdio> #include<cstdlib> #include<vector> #include<algorithm> using namespace std; int a[200000]; vector <int> vec; int main(){ ios::sync_with_stdio(false); int n; cin >> n; for(int i =1;i<=n;i++) cin>>a[i]; vec.push_back(a[1]); for(int i=2;i<=n;i++){ int k=a[i]; int j=lower_bound(vec.begin(),vec.end(),k)-vec.begin(); vec.insert(vec.begin()+j,k); //这是O(n)的 } for(int i=0;i<n;i++){ cout<<vec[i]<<" "; } return 0; }
显然,这份代码的复杂度,应该是T(n*(log2n+n))=O(n^2)的,但是有一点好的,就是不会被某些专门卡插排的数据卡。
我们再分析一下另一个耗时间的地方。
2.将所有元素前移的时间的上界是O(n)的,我们也要想办法优化到O(logn)。若我们只针对这一点优化,那么我们可以想到一种比O(logn)更快的数据结构来优化这一点,链表。
如果我们用链表来储存,我们完全没必要将元素前移,只要连接起来,是O(1)的。不难写出下面这份代码
#include<iostream> #include<cstdio> #include<cstdlib> #include<cstring> using namespace std; struct node{ int data; node* next; }; node *start=new node,*end = new node; int a[200000]; int main(){ end->data = -1; ios::sync_with_stdio(false); int n; cin >> n; for(int i=1;i<=n;i++) cin>>a[i]; start->next = end; for(int i=1;i<=n;i++){ int k=a[i]; node *p = start; while(p->next!=end&&p->next->data < k) p = p->next; node *q = p->next; node *now = new node; now->data = k; now->next = q; p->next = now; } node *p =start->next; while(p!=end){ cout<<p->data<<" "; p = p->next; } return 0; }
显然,这份代码也是O(n^2)的,慢在哪了?又是查找。
所以我们现在要做的事,就是把二分融合在链表里面,这就设下了一个大难关,但是,对数级的优化又启发了我们,我们必须在有限的次数(可以预知)内筛掉一半以上的数。
我们不妨考虑一个简化版的问题:给定n个有序的元素,现在要插入1个元素,用链表实现,效率是O(logn)怎么搞。
对这个问题,我有两个方法:
法1:在读入的时候预处理每个点到另一个点的中点的位置,空间复杂度高达O(n^2),铁定MLE。我们不得不另寻他法
法2:我们不妨分层存储,比如对于一个8个元素的链表,我们可以设计出以下数据结构:
#include<iostream> #include<cstdio> #include<cstdlib> #include<ctime> #include<cstring> using namespace std; struct node{ int data; int level; //所在层数 node* under; //下一层的相同结点 node* next; }; struct llist{ //level of list int level; node* start; llist(){ start = new node; start->data = -1; } }; int siz[200]; //定义为一个天文数字 int a[200000]; int log2(int); node* end = new node; int top = 1; //表示层数 llist le[200]; int want,n; int insert(int now,int lev,node* place,node *last = NULL){ node *f = place; while(f->next->data < now && f->next!=end) f = f->next; if(lev != 1){ int pd = insert(now,lev-1,f->under,f); if(pd == true){ int trid = rand()%2; if(trid == 1&&lev<want){ node* p = f->next; node *q = new node; q->data = p->data; q->level = p->level+1; q->under = p; if(last!=NULL){ q->next = last->next; last->next = q; }else{ q->next = end; le[lev+1].start->next = q; } if(siz[lev+1] == 0) top = lev+1; siz[lev+1] = 1; return true; } }else return false; }else{ siz[1]++; node *p = new node; p->level = 1; p->data = now; p->next = f->next; f->next = p; //移花接木 int trid = rand()%2; if(trid == 1&&lev<want){ node *q = new node; q->data = p->data; q->level = p->level+1; q->under = p; if(last!=NULL){ q->next = last->next; last->next = q; }else{ q->next = end; le[lev+1].start->next = q; } if(siz[lev+1] == 0) top = lev+1; siz[lev+1]++; return 1; }else return 0; } } int main(){ // freopen("sort.in","r",stdin); // freopen("sort.out","w",stdout); srand(time(NULL)); end->data = 2147483647; end->level = -1; //确认身份 ios::sync_with_stdio(false); cin >> n; want = log2(n); for(int i=1;i<=want;i++){ le[i].level = i; le[i].start->level = i; le[i].start->next = end; if(i!=1){ le[i].start->under = le[i-1].start; } } for(int i=1;i<=n;i++) cin>>a[i]; node* q = new node; q->data = a[1]; q->next = end; le[1].start->next = q; //为第一层加上一个结点 siz[1]++; for(int i=2;i<=n;i++){ insert(a[i],top,le[top].start); //插 ♂入 a[i] } node *p = le[1].start; p = p->next; while(p!=end){ cout<<p->data<<" "; p = p->next; } return 0; } int log2(int n){ int val = 1,k = 0; while(val*2 < n){ k++; val*=2; } return k+1; }View Code (前面的那份代码是错误的,现在是更正后的!!)
多美妙的代码啊。
下面是它与其它几种排序方法在时间上的比较:
首先是对于测试点的说明:
对于测试点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)。代码的话,我不会打!!!!毕竟这么晚了,改天有空了我会把代码补上。放心这次不会和前面那个三分,莫比乌斯一样烂尾的!
好了,代码写完了,其实这份代码有个可以优化的地方,大概可以优化O(nk)吧,就是对于某个数,从哪开始计算后面的这里,可以考虑用数组储存,而不是像我的代码一样每次都算出来。注意!这份代码因为写得烂(我这蒟蒻就这水平),会被卡常,所以要再优化优化,优化很简单,每次出链表,maxlen-1,可以优化到最后一个点不超时,请自行优化。
#include<stdio.h> #include<stdlib.h> #include<iostream> #include<algorithm> #include<ctime> #include<cstring> #include<cstring> #include<vector> using namespace std; struct node{ long long data; node *next,*last; node* under; node* on; node(){ under =NULL; on = NULL; } }; struct llist{ long long level; node *start; llist(){ start = new node; } }le[200]; //1<<200是个天文数字 long long siz[200]; long long p[200],top=1,want; long long log2(long long); long long insert(long long,long long,node*,node*); node* end=new node; node* point = end; //指向第一层最后一个 long long del(node*); int main(){ srand(time(NULL)); end->data = (1ll<<62ll); //初始化end为一个很大的数,表明它的身份 ios::sync_with_stdio(false); long long n,k; cin>>n>>k; want=log2(k); //期望的层数,最大不能超过它(其实超过一点点可以加速,懒得那么写) for(long long I=1;I<=n;I++) cin>>p[I]; make_heap(p+1,p+n+1); sort_heap(p+1,p+n+1); for(long long i=1;i<=want;i++){ le[i].level = i; le[i].start->next = end; if(i!=1){ le[i].start->under = le[i-1].start; } if(i!=want) le[i].start->on = le[i+1].start; } for(long long i=1;i<=n;i++) //插入初始结点 insert(p[i],top,le[top].start,NULL); point =le[1].start; while(point->next!=end) point = point->next; long long maxlen = max(k,n); for(long long i=1;i<k;i++){ //O(n) long long num=le[1].start->next->data; del(le[1].start->next); long long Table[200]; memset(Table,0,sizeof(Table)); long long relation; for(long long j=1;j<=n;j++) //O(k) if(num%p[j]==0) relation = j; for(long long j=relation;j<=n;j++){ //O(k) Table[j-relation+1] = num*p[j]; } for(long long j=1;j<=(n-relation+1);j++){ //O(k) if(Table[j]>point->data&&siz[1]>=maxlen) break; insert(Table[j],top,le[top].start,NULL);//O(logn) if(siz[1]>maxlen){ //删除明显不可能是答案的结点 node* key = point->last; long long level_now = 1; while(point!=NULL){ point->last->next = end; node *p=point->on; delete point; point=p; siz[level_now++]--; } point = key; } } } cout<<le[1].start->next->data; return 0; } long long del(node* place){ //删去元素。 long long levelnow = 1; while(place!=NULL){ place->last->next = place->next; place->next->last = place->last; node *p=place->on; place=p; siz[levelnow++]--; } } long long insert(long long now,long long lev,node* place,node *last){ node *f = place; while(f->next->data < now && f->next!=end) f = f->next; if(lev != 1){ //如果不是第一行就继续往下走 long long pd = insert(now,lev-1,f->under,f); if(pd == true){ //这个链表的性质,必须下一层有上面一层才会有 long long trid = rand()%2; if(trid == 1&&lev<want){ //如果随机中了,就给上层加结点 node* p = f->next; node *q = new node; q->data = p->data; q->under = p; p->on = q; if(last!=NULL){ q->next = last->next; q->next->last = q; last->next = q; q->last = last; }else{ q->next = end; le[lev+1].start->next = q; q->last = le[lev+1].start; } if(siz[lev+1] == 0) top = lev+1; siz[lev+1]++; return true; }else return false; }else return false; }else{ siz[1]++; node *p = new node; //创建p结点连接起来 p->data = now; p->next = f->next; p->last=f; f->next->last = p; f->next = p;if(p->next==end)point=p; long long trid = rand()%2; if(trid == 1&&lev<want){ node *q = new node; //为上一层创造q结点 q->data = p->data; q->under = p; p->on = q; if(last!=NULL){ q->next = last->next; q->next->last = q; last->next = q; q->last = last; }else{ q->next = end; le[lev+1].start->next = q; q->last = le[lev+1].start; } if(siz[lev+1] == 0)//其实siz数组有没有都无所谓啦,只要有个bool的siz数组就行 top = lev+1; siz[lev+1]++; return 1; }else return 0; } } long long log2(long long n){ //如代码名,计算log2n long long Value = 1,k = 0; while(Value*2 < n){ k++; Value*=2; } return k+1; }
- 详解插入排序和优化之后的希尔(shell)排序
- 优化的直接插入排序(二分查找插入排序,希尔排序)
- 【BZOJ1597】【USACO 2008 Mar】土地购买(斜率优化DP)
- 【bzoj2442/Usaco2011 Open】修剪草坪——单调队列优化dp
- 视图应用竟然还可以这么优化?不得不收藏(8)
- 图论;克鲁斯卡尔算法;最小生成树;通过权值排序可以优化时间效率;
- C# 数据库连接池 的使用 可以优化数据库
- 再看代码中的映射(利用数组优化你的代码---好的代码结构可以让人少加班, 少通宵)
- 排序----冒泡排序的优化_选择排序_插入排序
- 【转】关于LIS和一类可以用树状数组优化的DP 预备知识
- [BZOJ1584][Usaco2009 Mar]Cleaning Up 打扫卫生(dp+数学相关优化)
- C语言中快速排序和插入排序优化的实现
- C语言中快速排序和插入排序优化的实现
- Mysql相关命令,优化数据库的时候可以提供帮助
- 网页优化。-->其实看了注意一下就可以了
- Arch Linux 是个 针对 i686 优化的 Linux 发行版(通过可以轻松使用的二进制包系统 - pacman)
- POJ 2739(优化,也可以无敌打表)
- USACO 3.1 Humble Numbers丑数_优先队列
- dijkstra+堆优化 USACO 3.2.6 Sweet Butter
- HTML5中SEO可以用那些代码来做优化