Ynoi做题记录

1.P5355 [Ynoi Easy Round 2017] 由乃的玉米田

标签:莫队,bitset,根号分治。

用 bitset 作为一个桶维护当前区间出现了多少个,因为值域不超过 \(10^5\)

假设新出现一个数 \(p\),那么就在 bitset 的 \(now1\) 的位置 \(p\) 设成 1、\(now2\) 的位置 \(10^5-p\) 设成 1。如果要去掉一个就改成 0。

\(N=10^5\),对每一种操作分类讨论:

  1. 假设需要找的数是 \(y,z\),需要满足 \(z-y=x\),反过来就是 \(z=x+y\)。因此把 \(now1\) 中的信息全部右移 \(x\) 位与 \(now1\) 比较是否有交集。

  2. 还是 \(y,z\),需要满足 \(z+y=x\)。令 \(y = N-y'\),那么 \(z + N - y' = x\),即 \(z - y' = -N + x\)。这样就和情况一相同,看 \(now2\) 左移 \(N-x\)\(now1\) 是否有交集即可。

  3. 可以直接枚举 \(x\) 的约数,在 \(now1\) 看是否存在即可。

  4. 考虑根号分治:

  • 对于 $x > \sqrt N $ 的情况,我们可以枚举 \(\frac{y}{z}=x\) 中的 \(z\),看 \(zx\) 是否存在即可。最多只需要枚举 \(\sqrt N\) 次。
  • 对于 \(x \le \sqrt N\) 的情况,可以把这些询问不放在莫队中离线,而是单独离线,枚举每一个 \(i \le \sqrt N\),然后从前往后遍历序列,每一个位置 \(j\) 记录自己即自己前面可以使得 \(\frac{a_t}{a_j}=i\)\(a_t \times a_j = i\) 的最大一个 \(t\),然后记录目前每一个前缀序列中最大的 \(t\)。就可以在总共 \(n \sqrt N\) 的时间复杂度下完成所有这些询问。

时间复杂度 \(O(n(\sqrt m + \frac{N}{w}) + n \sqrt N)\)

点击查看代码
#include <iostream>
#include <bitset>
#include <math.h>
#include <algorithm>
#include <vector>
#include <string.h>
using namespace std;
int n,m,k=0,p=0,len;
const int N=1e5,B=318;
#define M N+5
struct que{
	int l,r,id,opt,x;
	bool operator < (const que &other) const {
		if(l/len != other.l/len) return l < other.l;
		if((l/len)&1) return r/len > other.r/len;
		return r/len < other.r/len;
	}
}q[M];
struct node{int l,r,id;};
vector<node> qu[400];
bitset<M> now1,now2;
int cnt[M],ans[M],a[M],res[M];
void add(int x){if(cnt[x]==0) now1[x]=1,now2[N-x]=1; ++cnt[x];}
void del(int x){--cnt[x]; if(cnt[x]==0) now1[x]=0,now2[N-x]=0;}
int main(){
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;++i) scanf("%d",&a[i]);
	len=sqrt(n)+1;
	for(int i=1;i<=m;++i){
		int L,R,X,OPT; scanf("%d%d%d%d",&OPT,&L,&R,&X);
		if(X<=B&&OPT==4) qu[X].push_back((node){L,R,i});
		else q[++k]=((que){L,R,i,OPT,X});
	}
	sort(q+1,q+k+1);
	int L=1,R=0;
	now1.reset(); now2.reset();
	for(int i=1;i<=k;++i){
		while(R<q[i].r) add(a[++R]);
		while(L>q[i].l) add(a[--L]);
		while(R>q[i].r) del(a[R--]);
		while(L<q[i].l) del(a[L++]);
		if(q[i].opt==1) ans[q[i].id]=((now1<<(q[i].x))&now1).count();
		if(q[i].opt==2) ans[q[i].id]=((now2>>(N-q[i].x))&now1).count();
		if(q[i].opt==3){
			int r=0;
			for(int j=1;j*j<=q[i].x;++j) if(q[i].x%j==0&&now1[j]&&now1[q[i].x/j])
			{r=1;break;}
			ans[q[i].id]=r;
		}
		if(q[i].opt==4){
			int r=0;
			for(int j=1;j*q[i].x<=N;++j) if(now1[j*q[i].x]&&now1[j])
			{r=1;break;}
			ans[q[i].id]=r;
		}
	}
	for(int t=1;t<=B;++t){
		if(qu[t].empty()) continue;
		memset(cnt,0,sizeof cnt);
		memset(res,0,sizeof res);
		for(int i=1;i<=n;++i){
			res[i]=res[i-1];
			cnt[a[i]]=i;
			if((a[i]*t<=N && cnt[a[i]*t]))
				res[i]=max(res[i],cnt[a[i]*t]);
			if(a[i]%t==0 && cnt[a[i]/t])
				res[i]=max(res[i],cnt[a[i]/t]);
		}
		for(auto v:qu[t]) ans[v.id]=(res[v.r]>=v.l);
	}
	for(int i=1;i<=m;++i){
		if(ans[i]) printf("yuno\n");
		else printf("yumi\n");
	}
}

2.P4688 [Ynoi Easy Round 2016] 掉进兔子洞

标签:莫队,bitset,离散化。

假设一个询问得到的三个可重集合是 \(A_1,A_2,A_3\),那么答案就是 \(|A_1|+|A_2|+|A_3|-3|A_1 \cap A_2 \cap A_3|\)。因此对于一个询问可以拆成三个子询问扔进莫队中。

\(N=10^5\)

集合维护因为需要取交集,所以理所当然用 bitset。但是最多有 \(N\) 个数,每个数最多出现 \(10^5\) 次,显然不能开 \(N \times N\) 的 bitset 维护。但是这些数最多一共才出现 \(N\) 次,所以只需要离散化后压位存进 bitset 中。

对于每个大询问也需要开 bitset 维护集合,但这样也是 \(N \times N\) 的空间。因此可以把这些询问分成 5 次即可。

时间复杂度 \(O(n(\sqrt m + \frac{N}{w}))\)

点击查看代码
#include <bitset>
#include <iostream>
#include <algorithm>
#include <math.h>
using namespace std;
const int N=2e5+5;
const int M=2e4;
bitset<100005> ans[M+5],Bit;
int n,m,cnt[N],res[N],a[N],b[N],fir[N],len;
int k=0,tot,vis[N];
int pl[5]={1,20001,40001,60001,80001};
int pr[5]={20000,40000,60000,80000,100000};
struct Que{
	int id,l,r;
	bool operator < (const Que &other) const{
		if(l/len != other.l/len) return l < other.l;
		if((l/len)&1) return r/len > other.r/len;
		return r/len < other.r/len;
	}
}q[N*5];
void add(int x){cnt[x]++;Bit[fir[x]+cnt[x]]=1;}
void del(int x){Bit[fir[x]+cnt[x]]=0;--cnt[x];}
void sol(int now){
	k=0;
	Bit.reset();
	for(int i=0;i<=tot;++i) cnt[i]=0;
	for(int i=pl[now];i<=min(m,pr[now]);++i){
		for(int j=0;j<3;++j){
			int x,y; scanf("%d%d",&x,&y);
			++k;
			q[k].id=i-now*M;
			q[k].l=x; q[k].r=y;
			res[i]+=(y-x+1);
		}
		vis[i-now*M]=0;
	}
	sort(q+1,q+k+1);
	int L=1,R=0;
	for(int i=1;i<=k;++i){
		while(R<q[i].r) add(a[++R]);
		while(L>q[i].l) add(a[--L]);
		while(R>q[i].r) del(a[R--]);
		while(L<q[i].l) del(a[L++]);
		if(!vis[q[i].id]) ans[q[i].id]=Bit,vis[q[i].id]=1;
		else ans[q[i].id]&=Bit;
	}
	for(int i=pl[now];i<=min(m,pr[now]);++i) res[i]-=(ans[i-M*now].count())*3;
}
int main(){
	scanf("%d%d",&n,&m);
	len=sqrt(n)+1;
	for(int i=1;i<=n;++i) scanf("%d",&a[i]),b[i]=a[i];
	sort(b+1,b+n+1);
	fir[0]=-999;
	for(int i=1;i<=n;++i){
		int now=lower_bound(b+1,b+n+1,b[i])-b;
		if(now!=fir[k]+2) fir[++k]=now-2;
	}
	tot=unique(b+1,b+n+1)-b-1;
	for(int i=1;i<=n;++i) a[i]=lower_bound(b+1,b+tot+1,a[i])-b;
	
	for(int t=0;t<5;++t){
		sol(t);
		if(pr[t]>=m) break;
	}
	for(int i=1;i<=m;++i) printf("%d\n",res[i]);
}

3.P5309 [Ynoi2011] 初始化

标签:分块,根号分治。

先把原序列分块。

考虑修改:

  • \(x > \sqrt n\),就直接暴力跳进行修改,最多跳 \(\sqrt n\) 次。
  • \(x \le \sqrt n\),注意到 \(y \le x\),那么整个序列中所有的位置在 \(x\) 确定时都可以唯一确定 \(y\) 的位置。也就是说在 \(y\) 不同而 \(x\) 相同时,每个修改操作 \((x,y)\) 影响的位置没有交集,且这些并集就是原序列。所以可以维护 \(k=x\) 时对于 \(y\) 位置的前缀和、后缀和,不修改原序列和大块。复杂度也是 \(\sqrt n\) 的。(注意这里不能树状数组,因为后面查询需要 \(O(1)\) 查询)。

考虑查询:

  • 先求出这一段的和,复杂度 \(O(\sqrt n)\)
  • 然后枚举 \(k \le \sqrt n\),然后就能利用前面的前、后缀和 \(O(1)\) 求得答案。

时间复杂度 \(O(m \sqrt n)\)

点击查看代码
#include <iostream>
#include <math.h>
#include <stdio.h>
using namespace std;
const int N=2e5+5,M=5005;
#define ll long long

const ll mod=1e9+7;
ll pre[M][M],bac[M][M];int sq,len,n,m;
ll a[N];int L[M],R[M];ll sum[M];int val[N];

inline int read(){
	register int x=0,f=1;
	register char ch=getchar();
	while(ch<'0'||ch>'9'){
		if(ch=='-') f=-1;
		ch=getchar();
	}
	while(ch>='0'&&ch<='9') x=(x<<1)+(x<<3)+(ch^48),ch=getchar();
	return x*f;
}
inline void write(int x){
	if(x<0){
		putchar(45);
		x=~x+1;
	}
	static int sta[40];
	register int top=0;
	do{
		sta[top++]=x%10,x/=10;
	}while(x);
	while(top) putchar(sta[--top]+48);
	putchar('\n');
	return;
}
inline void build(){
	sq=(int)((double)sqrt(n)*0.3);
	if(sq<1) sq=1;
	len=0;
	for(;;++len){
		L[len]=(len-1)*sq+1;
		R[len]=len*sq;
		if(R[len]>n){R[len]=n; break;}
	}
	for(register int i(1);i<=len;++i)
		for(int j=L[i];j<=R[i];++j)
			val[j]=i,sum[i]=(sum[i]+a[j]);
}

inline void update(int x,int y,ll z){
	if(x>sq){
		for(;y<=n;y+=x) a[y]=(a[y]+z),sum[val[y]]=(sum[val[y]]+z);
		return ;
	}
	for(register int i(y);i<=x;++i) pre[x][i]=(pre[x][i]+z);
	for(register int i(1);i<=y;++i) bac[x][i]=(bac[x][i]+z);
}
inline ll ges(int l,int r){
	int pl,pr;ll ans=0;
	for(register int i(1);i<=sq;++i){
		pl=l%i; if(!pl) pl=i;
		pr=r%i; if(!pr) pr=i;
		(r-l==pr-pl)?(ans=(ans+pre[i][pr]-pre[i][pl-1])) : ans=(((ans+bac[i][pl])+pre[i][pr])+(ll)((ll)(r-l+1)-(i-pl+1)-(pr))/(ll)i * (ll)(pre[i][i]));
	}
	return ans;
}
inline ll Sum(int x,int y){
	ll ans=0;
	if(val[x]==val[y]){for(;x<=y;++x) ans+=a[x];return ans;}
	for(register int i(x);i<=R[val[x]];++i) ans+=a[i];
	for(register int i(L[val[y]]);i<=y;++i) ans+=a[i];
	for(register int i(val[x]+1);i<val[y];++i) ans+=sum[i];
	return ans;
}
inline ll query(int x,int y){
	ll ans=Sum(x,y)+ges(x,y);
	return ans;
}
signed main(){
	n=read(),m=read();
	for(register int i(1);i<=n;++i) a[i]=read();
	build();
	while(m--){
		int opt=read(),x=read(),y=read(),z;
		(opt==2)? (write(query(x,y)%mod)) : (z=read(),update(x,y,(ll)z));
	}
}

4.P5069 [Ynoi Easy Round 2015] 纵使日薄西山

标签:线段树,模拟

发现如果需要操作 \(a_x\),那么 \(a_{x-1}\)\(a_{x+1}\) 就一定不会被操作。所以答案就是这些需要操作的数的和,这一过程可以线段树维护。

但是直接合并两个区间时,左区间的右端点和右区间的左端点可能都被选择,所以考虑维护每个区间忽略左端点、忽略右端点、忽略左右两个端点、左右都不忽略的答案。

为了更方便合并,还要记录每一种情况下左右端点是否被选择。

就是 pushup 有点不好写。

时间复杂度 \(O((q + n)\log n)\)

点击查看代码
#include <iostream>
using namespace std;
#define ll long long
#define lc (p<<1)
#define rc (p<<1|1)
#define mid ((l+r)>>1)
const int N=4e5+5;
ll t[N][4];
int L[N][4],R[N][4];
int a[N],n;
void pushup(int p,int l,int r){
	if(R[lc][0]&&L[rc][0]){
		if(a[mid]>=a[mid+1]){
			t[p][0]=t[lc][0]+t[rc][1];
			L[p][0]=L[lc][0],R[p][0]=R[rc][1];
		}
		else{
			t[p][0]=t[lc][2]+t[rc][0];
			L[p][0]=L[lc][2],R[p][0]=R[rc][0];
		}
	}
	else{
		t[p][0]=t[lc][0]+t[rc][0];
		L[p][0]=L[lc][0],R[p][0]=R[rc][0];
	}
	
	if(L[rc][0] && R[lc][1]){
		if(a[mid]>=a[mid+1]){
			t[p][1]=t[lc][1]+t[rc][1];
			L[p][1]=L[lc][1],R[p][1]=R[rc][1];
		}
		else{
			t[p][1]=t[lc][3]+t[rc][0];
			L[p][1]=L[lc][3],R[p][1]=R[rc][0];
		}
	}
	else{
		t[p][1]=t[lc][1]+t[rc][0];
		L[p][1]=L[lc][1],R[p][1]=R[rc][0];
	}
	
	if(R[lc][0] && L[rc][2]){
		if(a[mid]>=a[mid+1]){
			t[p][2]=t[lc][0]+t[rc][3];
			L[p][2]=L[lc][0],R[p][2]=R[rc][3];
		}
		else{
			t[p][2]=t[lc][2]+t[rc][2];
			L[p][2]=L[lc][2],R[p][2]=R[lc][2];
		}
	}
	else{
		t[p][2]=t[lc][0]+t[rc][2];
		L[p][2]=L[lc][0],R[p][2]=R[rc][2];
	}
	
	if(R[lc][1] && L[rc][2]){
		if(a[mid]>=a[mid+1]){
			t[p][3]=t[lc][1]+t[rc][3];
			L[p][3]=L[lc][1],R[p][3]=R[rc][3];
		}
		else{
			t[p][3]=t[lc][3]+t[rc][2];
			L[p][3]=L[lc][3],R[p][3]=R[rc][3];
		}
	}
	else t[p][3]=t[lc][1]+t[rc][2];
}
void build(int p,int l,int r){
	if(l==r){
		t[p][0]=a[l];
		t[p][1]=t[p][2]=t[p][3]=0;
		L[p][0]=R[p][0]=1;
		return ;
	}
	build(lc,l,mid);
	build(rc,mid+1,r);
	pushup(p,l,r);
}
void upd(int p,int l,int r,int x){
	if(l==r){
		t[p][0]=a[l];
		t[p][1]=t[p][2]=t[p][3]=0;
		return ;
	}
	if(x<=mid) upd(lc,l,mid,x);
	else upd(rc,mid+1,r,x);
	pushup(p,l,r);
}
int main(){
	scanf("%d",&n);
	for(int i=1;i<=n;++i) scanf("%d",&a[i]);
	build(1,1,n);
	int q; scanf("%d",&q);
	while(q--){
		int x,y; scanf("%d%d",&x,&y);
		a[x]=y; upd(1,1,n,x);
		printf("%lld\n",t[1][0]);
	}
}

5.P5524 [Ynoi2012] NOIP2015 充满了希望

标签:扫描线,树状数组,线段树。

目前最简单的一题。

首先发现一次询问的答案一定是之前某一个操作得到的值,手玩一下发现,假设第 \(i\) 次操作是查询,这次操作得到的值来自第 \(j\) 次操作。如果在区间 \([l,r]\) 中没有第 \(j\) 次操作,那么第 \(i\) 次操作对答案就没有贡献。反正则有贡献且只能是第 \(j\) 次操作。

这样就可以处理处所有询问的答案来自哪一个操作,令 \(f(x)\) 表第 \(x\) 次操作的答案来自的操作,那么答案为:\(\sum ans(f(i)) \times[l \le i,f(i) \le r]\)

\(r\) 扫描线,用树状数组维护即可。

posted @ 2025-08-27 21:18  StarsIntoSea_SY  阅读(19)  评论(0)    收藏  举报