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;
}