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\),对每一种操作分类讨论:
-
假设需要找的数是 \(y,z\),需要满足 \(z-y=x\),反过来就是 \(z=x+y\)。因此把 \(now1\) 中的信息全部右移 \(x\) 位与 \(now1\) 比较是否有交集。
-
还是 \(y,z\),需要满足 \(z+y=x\)。令 \(y = N-y'\),那么 \(z + N - y' = x\),即 \(z - y' = -N + x\)。这样就和情况一相同,看 \(now2\) 左移 \(N-x\) 与 \(now1\) 是否有交集即可。
-
可以直接枚举 \(x\) 的约数,在 \(now1\) 看是否存在即可。
-
考虑根号分治:
- 对于 $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\) 扫描线,用树状数组维护即可。

浙公网安备 33010602011771号