Ynoi 做题记录
[Ynoi2011] 初始化
第一道通过的 Ynoi 题,虽然似乎大概也许并不太难。
题目分析
查询操作为求区间和,可以使用分块。
看到这种修改操作满足“跳着加”性质的题目,可以尝试根号分治。
那么如何进行根号分治呢?
当 \(x \ge \sqrt{n}\) 时,需要修改的位置最多有 \(\sqrt{n}\) 个,故可以暴力地修改。
当 \(x < \sqrt{n}\) 时,需要使用另外一种方式维护:
观察需要修改的位置不难发现,假如我们把原数列分成块长为 \(x\) 的若干个块,那么修改操作相当于在每个块内的第 \(y\) 个位置上加 \(z\),定义 \(f_{i,j}\) 表示当 \(x=i\) 时,每个块内的第 \(j\) 个位置上一共被加了多少,然后定义 \(add_{i,j}\) 满足 \(add_{i,j}=\sum^{j}_{k=1}f_{i,k}\),于是查询操作的结果可以表示为:
其中,\(\sum^r_{i=l}a_{i}\) 可以通过分块快速求出。
卡常小技巧:本题的需要维护的各种数据都不会超过 long long
类型的上限,所以对于查询操作,可以在求得结果后再进行取模。
代码
点击查看代码
#include<bits/stdc++.h> using namespace std; inline int read(){register int t1=0,t2=0;register char x=getchar();while(x<'0' ||x>'9'){if(x=='-') t2|=1;x=getchar();}while(x>='0' && x<='9'){t1=(t1<<1)+(t1<<3)+(x^48),x=getchar();}return t2?-t1:t1;} inline void write(int x){register int sta[64],top=0;if(x<0) putchar('-'),x=-x;do{sta[top++]=x%10,x/=10;}while(x);while(top) putchar(sta[--top]+48);} const int mod=(1e9)+7; int n,m,L[200005],R[200005],pos[200005],t; long long a[200005],sum[200005],add[505][505]; inline void change(int x,int y,int z){ if(x>=t){ for(register int i=y;i<=n;i+=x){ a[i]+=z; sum[pos[i]]+=z; } } else for(register int i=y;i<=x;i++) add[x][i]+=z; } inline long long ask(int l,int r){ long long ans=0; int p=pos[l],q=pos[r]; if(p==q){ for(register int i=l;i<=r;i++){ ans+=a[i]; } } else{ for(register int i=p+1;i<=q-1;i++) ans+=sum[i]; for(register int i=l;i<=R[p];i++) ans+=a[i]; for(register int i=L[q];i<=r;i++) ans+=a[i]; } for(register int i=1;i<=t;i++){ ans+=add[i][i]*(r/i-(l-1)/i)+add[i][r%i]-add[i][(l-1)%i]; } return ans; } int main(){ n=read(); m=read(); for(register int i=1;i<=n;i++) a[i]=read(); t=sqrt(n); for(register int i=1;i<=t;i++){ L[i]=(i-1)*t+1; R[i]=i*t; } if(R[t]<n){ t++; L[t]=R[t-1]+1; R[t]=n; } for(int i=1;i<=t;i++){ for(int j=L[i];j<=R[i];j++){ pos[j]=i; sum[i]+=a[j]; } } while(m--){ int t1=read(); if(t1==1){ int t2=read(),t3=read(),t4=read(); change(t2,t3,t4); } else{ int t2=read(),t3=read(); write(ask(t2,t3)%mod); putchar('\n'); } } return 0; }
[Ynoi2017] 由乃打扑克
题目分析
看到查询区间第 \(k\) 小,通常有以下几种思路
-
莫队配合值域分块
-
二分答案
-
主席树
-
……
首先,我不会主席树,其次,莫队无法高效地完成区间加操作,所以考虑使用二分答案来解决本题的查询操作。
在二分答案时,如果当前的值在区间中的排名小于 \(k\),就考虑右半边,如果大于 \(k\),就考虑左半边。
那么应该如何快速地查询一个值在区间中的排名呢?
定义数组 \(a\) 表示原数组,我们可以将 \(a\) 分块,然后新建一个数组 \(b\),\(a\) 与 \(b\) 中的元素大小完全相同,但是 \(b\) 数组保证每一块中的元素从小到大排序。对于区间中的整块部分,我们在 \(b\) 数组的相应块内进行二分;对于零散部分,暴力统计即可。这样我们就实现了快速查询一个值在区间中的排名。
关于区间加,对于区间中的整块部分,直接打上懒标记即可;对于零散部分,暴力修改,对 \(b\) 数组相应的位置进行修改并重新排序。
代码
点击查看代码
#include<bits/stdc++.h> #define int long long using namespace std; inline int read(){register int t1=0,t2=0;register char x=getchar();while(x<'0' ||x>'9'){if(x=='-') t2|=1;x=getchar();}while(x>='0' && x<='9'){t1=(t1<<1)+(t1<<3)+(x^48),x=getchar();}return t2?-t1:t1;} inline void write(int x){register int sta[35],top=0;if(x<0) putchar('-'),x=-x;do{sta[top++]=x%10,x/=10;}while(x);while(top) putchar(sta[--top]+48);} int n,m,a[100005],b[100005],t,pos[100005],L[100005],R[100005],add[100005]; void change(int l,int r,int d){ int p=pos[l],q=pos[r]; if(p==q){ for(int i=l;i<=r;i++) a[i]+=d; for(int i=L[p];i<=R[p];i++) b[i]=a[i]; sort(b+L[p],b+R[p]+1); } else{ for(int i=p+1;i<=q-1;i++) add[i]+=d; for(int i=l;i<=R[p];i++) a[i]+=d; for(int i=L[p];i<=R[p];i++) b[i]=a[i]; sort(b+L[p],b+R[p]+1); for(int i=L[q];i<=r;i++) a[i]+=d; for(int i=L[q];i<=R[q];i++) b[i]=a[i]; sort(b+L[q],b+R[q]+1); } } int check(int l,int r,int x){ int p=pos[l],q=pos[r],ans=0; if(p==q){ for(int i=l;i<=r;i++){ if(a[i]+add[pos[i]]<=x) ans++; } return ans; } for(int i=p+1;i<=q-1;i++){ if(b[L[i]]+add[i]>x) continue; if(b[R[i]]+add[i]<=x){ ans+=R[i]-L[i]+1; continue; } //由于b数组从大到小排序,所以如果在b数组中该块的左端点的位置上的值都大于x,则说明该块内没有比x小的数。 //同理,如果在b数组中该块的右端点的位置上的值都不大于x,则说明该块内没有比x大的数。 int t1=L[i],t2=R[i]; while(t1<t2){ int mid=(t1+t2>>1)+1; if(b[mid]+add[i]<=x) t1=mid; else t2=mid-1; } if(b[t1]+add[i]<=x) ans+=t1-L[i]+1; } for(int i=l;i<=R[p];i++){ if(a[i]+add[pos[i]]<=x) ans++; } for(int i=L[q];i<=r;i++){ if(a[i]+add[pos[i]]<=x) ans++; } return ans; } int ask(int l,int r,int k){ if(r-l+1<k) return -1; int p=pos[l],q=pos[r],t1=0x3f3f3f3f,t2=-0x3f3f3f3f; if(p==q){ for(int i=l;i<=r;i++){ t1=min(t1,a[i]+add[pos[i]]); t2=max(t2,a[i]+add[pos[i]]); } } else{ for(int i=p+1;i<=q-1;i++){ t1=min(t1,b[L[i]]+add[i]); t2=max(t2,b[R[i]]+add[i]); } for(int i=l;i<=R[p];i++){ t1=min(t1,a[i]+add[pos[i]]); t2=max(t2,a[i]+add[pos[i]]); } for(int i=L[q];i<=r;i++){ t1=min(t1,a[i]+add[pos[i]]); t2=max(t2,a[i]+add[pos[i]]); } }//二分答案的左右端点只需要取查询区间内的最大值和最小值即可。 int ans=-1; while(t1<=t2){ int mid=t1+t2>>1; if(check(l,r,mid)<k) t1=mid+1; else{ t2=mid-1; ans=mid; } } return ans; } signed main(){ n=read(); m=read(); for(int i=1;i<=n;i++){ a[i]=read(); b[i]=a[i]; } t=sqrt(n); for(int i=1;i<=t;i++){ L[i]=(i-1)*t+1; R[i]=i*t; } if(R[t]!=n){ t++; L[t]=R[t-1]+1; R[t]=n; } for(int i=1;i<=t;i++){ for(int j=L[i];j<=R[i];j++) pos[j]=i; sort(b+L[i],b+R[i]+1); } while(m--){ int t1=read(),t2=read(),t3=read(),t4=read(); if(t1==1){ write(ask(t2,t3,t4)); putchar('\n'); } else change(t2,t3,t4); } return 0; }
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 使用C#创建一个MCP客户端
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现