[CSP-J 2021] 插入排序、小熊的果篮
题目描述
插入排序是一种非常常见且简单的排序算法。小 Z 是一名大一的新生,今天 H 老师刚刚在上课的时候讲了插入排序算法。
假设比较两个元素的时间为 ,则插入排序可以以 的时间复杂度完成长度为 的数组的排序。不妨假设这 个数字分别存储在 之中,则如下伪代码给出了插入排序算法的一种最简单的实现方式:
这下面是 C/C++ 的示范代码:
for (int i = 1; i <= n; i++) for (int j = i; j >= 2; j--) if (a[j] < a[j-1]) { int t = a[j-1]; a[j-1] = a[j]; a[j] = t; }
这下面是 Pascal 的示范代码:
for i:=1 to n do for j:=i downto 2 do if a[j]<a[j-1] then begin t:=a[i]; a[i]:=a[j]; a[j]:=t; end;
为了帮助小 Z 更好的理解插入排序,小 Z 的老师 H 老师留下了这么一道家庭作业:
H 老师给了一个长度为 的数组 ,数组下标从 开始,并且数组中的所有元素均为非负整数。小 Z 需要支持在数组 上的 次操作,操作共两种,参数分别如下:
:这是第一种操作,会将 的第 个元素,也就是 的值,修改为 。保证 ,。注意这种操作会改变数组的元素,修改得到的数组会被保留,也会影响后续的操作。
:这是第二种操作,假设 H 老师按照上面的伪代码对 数组进行排序,你需要告诉 H 老师原来 的第 个元素,也就是 ,在排序后的新数组所处的位置。保证 。注意这种操作不会改变数组的元素,排序后的数组不会被保留,也不会影响后续的操作。
H 老师不喜欢过多的修改,所以他保证类型 的操作次数不超过 。
小 Z 没有学过计算机竞赛,因此小 Z 并不会做这道题。他找到了你来帮助他解决这个问题。
输入格式
第一行,包含两个正整数 ,表示数组长度和操作次数。
第二行,包含 个空格分隔的非负整数,其中第 个非负整数表示 。
接下来 行,每行 个正整数,表示一次操作,操作格式见【题目描述】。
输出格式
对于每一次类型为 的询问,输出一行一个正整数表示答案。
样例 #1
样例输入 #1
3 4 3 2 1 2 3 1 3 2 2 2 2 3
样例输出 #1
1 1 2
提示
【样例解释 #1】
在修改操作之前,假设 H 老师进行了一次插入排序,则原序列的三个元素在排序结束后所处的位置分别是 。
在修改操作之后,假设 H 老师进行了一次插入排序,则原序列的三个元素在排序结束后所处的位置分别是 。
注意虽然此时 ,但是我们不能将其视为相同的元素。
【样例 #2】
见附件中的 sort/sort2.in
与 sort/sort2.ans
。
该测试点数据范围同测试点 。
【样例 #3】
见附件中的 sort/sort3.in
与 sort/sort3.ans
。
该测试点数据范围同测试点 。
【样例 #4】
见附件中的 sort/sort4.in
与 sort/sort4.ans
。
该测试点数据范围同测试点 。
【数据范围】
对于所有测试数据,满足 ,,,。
对于所有测试数据,保证在所有 次操作中,至多有 次操作属于类型一。
各测试点的附加限制及分值如下表所示。
测试点 | 特殊性质 | ||
---|---|---|---|
无 | |||
无 | |||
无 | |||
保证所有输入的 互不相同 | |||
无 | |||
保证所有输入的 互不相同 | |||
无 |
算法分析
这是普及组第二题啊,啥时候普及组第二题也这么有难度了?我们来看100%的数据,即使回答是,时间复杂度是16亿,也不行,更何况排序的时间复杂度是,根本过不去
我一直纠结在这个问题上,感觉这个题无解
但是这个题有一个非常重要的性质,保证修改的次数最多有5000次,但是看到这个性质的时候,第一想法就是询问次数这么多,时间复杂度太大了,但最终的答案是要利用这一个性质做文章,我们将之前的序列排好序,每次修改只会修改一个元素,我们只需要的给这个元素找好位置即可。使用冒泡排序的思想就可以,所以这样的时间复杂度是5000*8000,回答第二类询问的时候,只需要的回答即可。
当然,之前做这个题的时候,还有一个问题一直解决不了,我们使用表示第i个元素在新排序的数组中的位置,当我们在冒泡排序的时候,这个数组很难记录,当时没有想到结构体数组,我们使用结构体可以记录每个元素的值val和位置id,那么这样的话,id和val始终进行关联,同时我们也能很方便的记录了
代码
点击查看代码
#include<bits/stdc++.h> using namespace std; const int maxn=8005; int n,q,w[maxn]; struct Val{ int val,id; }v[maxn]; bool cmp(Val a,Val b){ if(a.val==b.val) return a.id<b.id; else return a.val<b.val; } int main(){ scanf("%d%d",&n,&q); for(int i=1;i<=n;i++){ scanf("%d",&v[i].val); v[i].id=i; } sort(v+1,v+1+n,cmp); for(int i=1;i<=n;i++){ w[v[i].id]=i; } for(int k=1;k<=q;k++){ int op,x,y; scanf("%d",&op); if(op==1){ scanf("%d%d",&x,&y); int pos; for(int i=1;i<=n;i++) if(v[i].id==x) { v[i].val=y; pos=i; } for(int i=pos;i>=2;i--){ if(v[i].val<v[i-1].val){ swap(v[i],v[i-1]); continue; } if(v[i].val==v[i-1].val&&v[i].id<v[i-1].id){ swap(v[i],v[i-1]); } } for(int i=pos;i<n;i++){ if(v[i].val>v[i+1].val){ swap(v[i],v[i+1]); continue; } if(v[i].val==v[i+1].val&&v[i].id>v[i+1].id){ swap(v[i],v[i+1]); } } for(int i=1;i<=n;i++){ w[v[i].id]=i; } }else{ scanf("%d",&x); printf("%d\n",w[x]); } } return 0; } /** 5 20 1 4 9 5 6 1 2 10 1 3 2 2 2 2 3 1 4 1 2 4 2 3 2 2 2 5 1 4 12 2 4 2 3 2 1 1 1 19 2 3 2 1 2 4 1 1 1 2 3 2 2 */
[CSP-J 2021] 小熊的果篮
题目描述
小熊的水果店里摆放着一排 个水果。每个水果只可能是苹果或桔子,从左到右依次用正整数 编号。连续排在一起的同一种水果称为一个“块”。小熊要把这一排水果挑到若干个果篮里,具体方法是:每次都把每一个“块”中最左边的水果同时挑出,组成一个果篮。重复这一操作,直至水果用完。注意,每次挑完一个果篮后,“块”可能会发生变化。比如两个苹果“块”之间的唯一桔子被挑走后,两个苹果“块”就变成了一个“块”。请帮小熊计算每个果篮里包含的水果。
输入格式
第一行,包含一个正整数 ,表示水果的数量。
第二行,包含 个空格分隔的整数,其中第 个数表示编号为 的水果的种类, 代表苹果, 代表桔子。
输出格式
输出若干行。
第 行表示第 次挑出的水果组成的果篮。从小到大排序输出该果篮中所有水果的编号,每两个编号之间用一个空格分隔。
样例 #1
样例输入 #1
12 1 1 0 0 1 1 1 0 1 1 0 0
样例输出 #1
1 3 5 8 9 11 2 4 6 12 7 10
样例 #2
样例输入 #2
20 1 1 1 1 0 0 0 1 1 1 0 0 1 0 1 1 0 0 0 0
样例输出 #2
1 5 8 11 13 14 15 17 2 6 9 12 16 18 3 7 10 19 4 20
样例 #3
样例输入 #3
见附件中的 fruit/fruit3.in。
样例输出 #3
见附件中的 fruit/fruit3.ans。
提示
【样例解释 #1】
这是第一组数据的样例说明。
所有水果一开始的情况是 ,一共有 个块。
在第一次挑水果组成果篮的过程中,编号为 的水果被挑了出来。
之后剩下的水果是 ,一共 个块。
在第二次挑水果组成果篮的过程中,编号为 的水果被挑了出来。
之后剩下的水果是 ,只有 个块。
在第三次挑水果组成果篮的过程中,编号为 的水果被挑了出来。
最后剩下的水果是 ,只有 个块。
在第四次挑水果组成果篮的过程中,编号为 的水果被挑了出来。
【数据范围】
对于 的数据,。
对于 的数据,。
对于 的数据,。
对于 的数据,。
【提示】
由于数据规模较大,建议 C/C++ 选手使用 scanf
和 printf
语句输入、输出。
核心思路
其实这个题,非常容易想到链表,但是这个题有一个难点:假设某个块和上一个块的颜色一样的话,我们最好的方式是合并,如果不合并的话,每次都会枚举到这个块,都会浪费时间,但是我之前只是维护了一个链表,所以两个块合并的时候,无法将两个块的元素合并起来,后来收到答案启发,应该维护两个链表,还有一个链表维护每个元素的下一个元素是谁,当我们我们合并的时候需要将前一块右端点的下一个元素维护好,同时输出的时候,每一个元素左端点的下一个元素作为左端点
我写这个代码的时候,犯了一个严重的错误,当两个块合并的时候,需要判断前一块是否可用,如果可用才能合并,就这个点,卡了我好久
点击查看代码
#include<bits/stdc++.h> using namespace std; const int maxn=200005; struct kuai{ int l,r,k,nxt,pre; }a[maxn],d[maxn]; int n,b[maxn],xy,cnt; bool f[maxn]; void fk(){ int i=a[0].nxt; for(i;i!=-1;i=a[i].nxt){ if(a[i].l>a[i].r){//说明这个区间已经不行了,这里我并没有设置a[i]的后一个区间,所以下次枚举的还是i的下一个区间 int q=a[i].pre; int h=a[i].nxt; a[h].pre=q; a[q].nxt=h; }else{ //这时面临一个问题,尽管pre和nxt连接起来了,但是如果两个块的值一样的话,仍然不需要输出 if(a[i].k!=a[a[i].pre].k){ if(a[i].l>0) {//如果这个值大于0 cout<<a[i].l<<" "; a[i].l=d[a[i].l].nxt; xy--; //这时就应该判断这个区间是否有用, //00100第一次中间的1没用,第二次两面的0都没用了,但是 } } else{ int q=a[i].pre; if(a[q].l>a[q].r) continue; //只要和前面的值相同就合并 d[a[q].r].nxt=a[i].l; a[q].r=a[i].r; a[q].nxt=a[i].nxt; a[a[i].nxt].pre=q; } } } cout<<endl; } int main(){ scanf("%d",&n); int p,x; cnt=0; a[0].k=-1; a[0].nxt=1; a[1].pre=0; for(int i=1;i<=n;i++){ scanf("%d",&x); if(i<n) d[i].nxt=i+1; else d[i].nxt=-1; if(cnt==0||p!=x){ a[cnt].r=i-1; a[cnt].nxt=cnt+1; cnt++; a[cnt].l=i; a[cnt].k=x; a[cnt].pre=cnt-1; } p=x; } a[cnt].r=n; a[cnt].nxt=-1; xy=n; while(xy){ fk(); } return 0; } /** 100 0 0 0 0 0 1 1 1 1 1 1 1 0 1 0 1 0 0 1 0 0 1 0 0 1 1 0 1 0 1 0 1 1 1 0 1 1 0 1 1 0 1 1 1 0 1 0 0 1 1 1 1 1 1 0 0 1 0 0 0 0 0 0 0 0 0 1 0 1 0 0 0 1 1 0 1 1 0 0 0 0 0 0 1 0 0 1 0 1 1 0 0 0 1 1 1 1 1 0 0 1 6 13 14 15 16 17 19 20 22 23 25 27 28 29 30 31 32 35 36 38 39 41 42 45 46 47 49 55 57 58 67 68 69 70 73 75 76 78 84 85 87 88 89 91 94 99 2 7 18 26 48 50 56 74 79 90 92 95 100 3 8 21 33 59 77 80 96 4 9 24 34 60 97 5 10 61 98 11 62 12 63 37 64 40 65 43 66 44 71 51 72 52 81 53 82 54 83 86 93 */
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】