1 条题解
-
0
题目传送门:冒泡排序
写在前面
学完线段树后的练习题,听了老师评讲才做出来,于是写一篇题解报告巩固温习。本题解报告会详细的写出推导过程乃至部分分的获得办法,但依旧需要认真梳理逻辑。
题意描述
给定一个序列,进行 次修改,每次修改后从前往后冒泡,求把序列排序成升序的次数(冒泡完成后要还原数组)。
题解
算法一
暴力模拟,复杂度拉满了是 ,如果写点优化应该能过子任务一。反正我没过,T 了两个点。
算法二
我们模拟一下冒泡的过程,比如这里有个数列 ,一次冒泡后变成了:。我们发现 前面比它们小的数都减少了一个。进行一次冒泡,数列变成了 。 前面比它小的数减少了一个。我们一直冒泡下去,直到每个数前面的数都严格不大于它。
所以我们可以得到规律,设 为 前面比 小的数的个数,其实一次冒泡就是让所有 ,因为一次冒泡我们只会把一个大于 的数移到 后面。那很显然,。
于是我们可以树套树来维护,用权值线段树维护 ,用普通线段树维护 。
复杂度 ,可以通过子任务 。
但因为我太弱了,没学过树套树,所以不会这个部分分。算法三,正解
设 为第 内 的数,显然存在 。然而,如果存在两个数使得 ,则一定有 。为什么呢?因为 所以 在 后面,被减去的数更少。又因为 ,所以比 小的数一定严格不小于比 小的数,也就是 ,减去的数更多。综上,如果存在两个数使得 ,则一定有 。
所以如果我们把 的定义改成整个数组内 的数,其实答案是不变的。这又是为什么呢? 考虑这样改变定义后对 的影响:这样更改后,如果 后面有比 小的数 ,那么 会增大, 会变小。但又因为 ,根据上面的证明可知,,所以本来 就不会变成答案了,现在变小了肯定还是不可能。反之,如果没有 ,那么 可能成为答案,但这样定义修改后是不会对这个 产生影响的。如此证明完毕。于是我们可以用权值线段树实现,同时维护至于内数的个数,以及 。复杂度降低到 。
救命究竟是怎样的大佬能想出这样的解法啊!代码实现
说实话我觉得此题代码写起来还是很有难度的,
当然不排除是因为我太弱了的问题。这里详细解释一下代码的一些实现问题。离散化
首先值域是从 ,所以要建立权值线段树需要离散化。 是原数组, 是询问数组(我把询问离线下来了)。 是离散化数组。不过要注意一下,如果两个元素值相同,相对位置关系不会改变。所以离散化的时候就要同是记录 和 ,然后 sort 的时候也注意一下。
cin>>n>>m; for(int i=1;i<=n;i++){ cin>>a[i].val; a[i].id=i; b[++cnt]=a[i]; } for(int i=1;i<=m;i++){ cin>>q[i].id>>q[i].val;//把id号元素修改为val q[i].id++;//原编号从0开始,我把它们修改成从1开始,所以id++ b[++cnt]=q[i];//离线询问 } build(1,1,cnt);//建立空树 sort(b+1,b+1+cnt,cmp);//离散化维护线段树
update_change & update_add
线段树维护两个元素:, 表示 内的 , 表示 内有多少个数。于是我们需要分别写两个函数维护:update_change 是插入、删除元素后进行修改,update_add 则修改 。
void update_change(int p,int x,int d){//对应题目的修改操作 if(tr[p].l==tr[p].r){ if(d!=-INF) tr[p].sum=1;//对应加入 else tr[p].sum=0;//对应删除 tr[p].maxx=d; return; } push_down(p); int mid=(tr[p].l+tr[p].r)>>1; if(x<=mid) update_change(p<<1,x,d); else update_change(p<<1|1,x,d); push_up(p); }void update_add(int p,int l,int r,int d){//对应修改后对maxx的更新 if(l>r) return; if(l<=tr[p].l&&tr[p].r<=r){ f(p,d);//修改 return; } push_down(p); int mid=(tr[p].l+tr[p].r)>>1; if(l<=mid) update_add(p<<1,l,r,d); if(mid<r) update_add(p<<1|1,l,r,d); push_up(p); }query
首先建一棵空树,然后把 离散化后的映射值插入树内。假设第 个元素的映射值为 ,插入之后,我们首先在对应位置的 。然后考虑维护 :跟上文一样, 表示 的数,我们还需要函数 查询一个元素的排名。
int query(int p,int x){//查询一个数的排名 if(tr[p].l==tr[p].r) return tr[p].sum; push_down(p); int mid=(tr[p].l+tr[p].r)>>1; if(x<=mid) return query(p<<1,x); else return tr[p<<1].sum+query(p<<1|1,x); }插入 中元素
值得注意的是,因为元素有重复的,所以我们在求 的时候,应该求 的排名,然后 ,而不是直接 。
然后根据 修改对应位置的 。接着我们又发现,如果在 的位置插入了一个数,那么后面所有数的 都会加一, 则减一。
for(int i=1;i<=n;i++){//插入a中元素 int x=lower_bound(b+1,b+1+cnt,a[i])-b;//查找映射值 int y=query(1,x-1);//查找映射值前一位的排名,因为有重复的值 update_change(1,x,i-y-1);//在x的位置加上1,g(x)=i-y-1 update_add(1,x+1,cnt,-1);//x上的数多了一个,表明g(x+1)~g(cnt)都+1,于是f对应-1 c[a[i].id]=x;//把a映射值复制给c }此外我们用 数组存一下映射值,方便以后的修改操作。
修改
主要分为两步:删除原来的数与插入新的数。具体操作看代码吧,注释写的超详细了。
for(int i=1;i<=m;i++){//查询 update_change(1,c[q[i].id],-INF);//把要更改的元素移除,利用c找到映射值,然后在值域线段树上修改为0 update_add(1,c[q[i].id]+1,cnt,1);//从该映射值往后到cnt的fi+1,因为这个值被移除了 int x=lower_bound(b+1,b+1+cnt,q[i])-b;//要修改成为哪个映射值 c[q[i].id]=x;//把对应id的数修改为新的映射值 int y=query(1,x-1);//查找映射值前一位的排名 update_change(1,x,q[i].id-y-1);//同上 update_add(1,x+1,cnt,-1);//同上 cout<<tr[1].maxx<<'\n'; }AC code
#include<bits/stdc++.h> #define INF 0x7fffffff using namespace std; const int N=5e5+5; struct NODE{ int id,val; bool operator < (const NODE &a) const{ if(a.val!=val) return val<a.val;//权值为第一关键字 return id<a.id;//顺序为第二关键字 } }a[N],q[N],b[N<<1]; struct TREE{ int maxx,sum,lz; //maxx是f也就是答案,sum记录值域内的数的个数,lz懒标记 int l,r; }tr[N<<3]; int n,m,cnt,c[N<<1]; void push_up(int p){ tr[p].sum=tr[p<<1].sum+tr[p<<1|1].sum; tr[p].maxx=max(tr[p<<1].maxx,tr[p<<1|1].maxx); } void f(int p,int d){//给f_max打懒标记 if(tr[p].maxx==-INF) return; tr[p].lz+=d; tr[p].maxx+=d; } void push_down(int p){ if(tr[p].lz){ f(p<<1,tr[p].lz); f(p<<1|1,tr[p].lz); tr[p].lz=0; } } void build(int p,int l,int r){//建空的权值线段树 tr[p].l=l; tr[p].r=r; if(l==r){ tr[p].maxx=-INF; tr[p].sum=0; return; } int mid=(l+r)>>1; build(p<<1,l,mid); build(p<<1|1,mid+1,r); push_up(p); } void update_change(int p,int x,int d){//对应题目的修改操作 if(tr[p].l==tr[p].r){ if(d!=-INF) tr[p].sum=1;//对应加入 else tr[p].sum=0;//对应删除 tr[p].maxx=d; return; } push_down(p); int mid=(tr[p].l+tr[p].r)>>1; if(x<=mid) update_change(p<<1,x,d); else update_change(p<<1|1,x,d); push_up(p); } void update_add(int p,int l,int r,int d){//对应修改后对maxx的更新 if(l>r) return; if(l<=tr[p].l&&tr[p].r<=r){ f(p,d);//修改 return; } push_down(p); int mid=(tr[p].l+tr[p].r)>>1; if(l<=mid) update_add(p<<1,l,r,d); if(mid<r) update_add(p<<1|1,l,r,d); push_up(p); } int query(int p,int x){//查询一个数的排名 if(tr[p].l==tr[p].r) return tr[p].sum; push_down(p); int mid=(tr[p].l+tr[p].r)>>1; if(x<=mid) return query(p<<1,x); else return tr[p<<1].sum+query(p<<1|1,x); } bool cmp(NODE a1,NODE a2){ if(a1.val!=a2.val) return a1.val<a2.val; return a1.id<a2.id; } int main(){ cin>>n>>m; for(int i=1;i<=n;i++){ cin>>a[i].val; a[i].id=i; b[++cnt]=a[i]; } for(int i=1;i<=m;i++){ cin>>q[i].id>>q[i].val;//把id号元素修改为val q[i].id++; b[++cnt]=q[i];//离线询问 } build(1,1,cnt); sort(b+1,b+1+cnt,cmp);//离散化 for(int i=1;i<=n;i++){//建树 int x=lower_bound(b+1,b+1+cnt,a[i])-b;//查找映射值 int y=query(1,x-1);//查找映射值前一位的排名,因为有重复的值 update_change(1,x,i-y-1);//在x的位置加上1,g(x)=i-y-1 update_add(1,x+1,cnt,-1);//x上的数多了一个,表明g(x+1)~g(cnt)都+1,于是f对应-1 c[a[i].id]=x;//把a映射值复制给c } for(int i=1;i<=m;i++){//查询 update_change(1,c[q[i].id],-INF);//把要更改的元素移除,利用c找到映射值,然后在值域线段树上修改为0 update_add(1,c[q[i].id]+1,cnt,1);//从该映射值往后到cnt的fi+1,因为这个值被移除了 int x=lower_bound(b+1,b+1+cnt,q[i])-b;//要修改成为哪个映射值 c[q[i].id]=x;//把对应id的数修改为新的映射值 int y=query(1,x-1);//查找映射值前一位的排名 update_change(1,x,q[i].id-y-1);//同上 update_add(1,x+1,cnt,-1);//同上 cout<<tr[1].maxx<<'\n'; } return 0; }写在后面
此题就我而言还是觉得是比较难的紫题了,主要难点是对 的推导与推广,正如另一篇题解所说,“对于这种求极值的题,我们可以不用盯着最优决策考虑,适当放宽条件,既可以保证取极值的正确性,又可以保证代码的码量较小,有点类似于不等式放缩。还是需要灵活地思维。”
此外也需要对线段树的一定理解,推一手我线段树的复习总结博客:线段树复习小结。
码字不易,求过审,求点赞!
- 1
信息
- ID
- 10160
- 时间
- 5000ms
- 内存
- 512MiB
- 难度
- 10
- 标签
- 递交数
- 1
- 已通过
- 1
- 上传者