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}+\sum^{\sqrt{n}}_{i=1}(add_{i,i} \times (\lfloor \frac{r}{i} \rfloor - \lfloor \frac{l-1}{i} \rfloor) + add_{i,r \bmod i} - add_{i,(l-1) \bmod i}) \]

其中,\(\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\) 小,通常有以下几种思路

  1. 莫队配合值域分块

  2. 二分答案

  3. 主席树

  4. ……

首先,我不会主席树,其次,莫队无法高效地完成区间加操作,所以考虑使用二分答案来解决本题的查询操作。

在二分答案时,如果当前的值在区间中的排名小于 \(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;
}

posted @ 2024-05-29 16:03  ZnHF  阅读(37)  评论(2编辑  收藏  举报