Data Structure 2

扫描线与根号数据结构

202403

写一篇正经一点的东西。

前言

若无特殊说明,一下题目的数据范围 \(n,m \le 10^5\),认为 \(n,m\) 同阶。


扫描线

2023.11.30


扫描线,一个再 DS 中经常会用到的技巧,它的做法也是比较套路的。

关于定义:我们分为两种东西,维度自由度,自由度就是操作中可以动的量。

扫描线 解决的问题一般都是先操作再查询的 静态 问题,扫描线就是将问题减少一维而将 静态 问题变成 动态 问题的方法。

对于扫描线的题目,我们主要分为三种方法。

  1. 反演:对于有关每一个值的,我们可以把它转化成求 每一个元素对区间的贡献,具体来说将区间的询问变成二维平面上面的每一个点,将每一个元素贡献的区间变成有贡献的矩形,最后跑一遍扫描线求值即可。

  2. 区间子区间问题:(lxl:这是一个批量出题的好方法)

    我们将 区间子区间 问题表示到二维的平面上面,同样用一个点表示一个区间,

    那么区间子区间问题就是求一个二维矩阵中的答案个数,这样我们从左往右扫过来的过程中我们需要维护区间的历史信息和就可以了。

    这样相当于会在线段树上面维护两个标记,需要分析一下两个标记交错时如何处理。

  3. 换维扫描线:在做 DS 问题的时候我们可以采用枚举算法的方法,而在扫描线的时候我们也需要枚举从哪里扫,

    简单来说,扫描线一般都是按照时间的先后顺序依次扫过去的,

    而在有些题目中,这是非常不好做的,所以我们考虑 换维

    具体来说假设现在构成的二维平面又两维,时间和序列,我们可以尝试按照序列依次扫过去,把区间的修改变成单点的修改和查询。

这是这节课最主要所讲的,而还有一些挺有用的思维方式。

  1. 对于森林计数,我们考虑用 点 - 边。(还是挺好理解的)

  2. 数颜色 相关的题目,我们常常考虑容斥,去计算没有这些颜色没出现的区间(出现 \(0\) 次总会比你去讨论出现 \(1,2,\dots\) 次简单)

  3. 最大值 所影响的区间的两种方法:

    • 单调栈维护(简单易懂)

    • 最大值分治:

      当最大值在 \(i\) 位置时,那么他所影响的区间是左端点在 \([1,i]\) 中而右端点在 \([i,n]\) 的区间。

      于是我们进行分治,将序列分成两个部分 \([1,i-1]\)\([i+1,n]\) 再分别递归下去找最大值。

  4. 由于区间的 \(l \lt r\),所以有一些 4-side 问题可以直接转化成 2-side 问题。

感觉题也不是很多,也没有 Day 1 写着那么恶心。

扫描线的问题主要难在思路部分,但总是还是有许多套路的。


反演

在上面也提到。

对于有关每一个值的,我们可以把它转化成求 每一个元素对区间的贡献,具体来说将区间的询问变成二维平面上面的每一个点,将每一个元素贡献的区间变成有贡献的矩形,最后跑一遍扫描线求值即可。


这种思路非常有用,不仅仅是在 DS 中,在一些 dp 题目或者其他题目中我们也需要考虑这种方法。(例如 WC2024T1)


CF1000F One Occurrence

CF1000F One Occurrence

给定长为 \(n\) 的序列,\(m\) 次查询区间中有多少值只出现一次。

\(1 \le n,m \le 5 \times 10^5\)

一上来没什么感觉直接不会做了。


发现把每一个数出现的位置画出来,容易发现只出现一次的区间范围是很容易得到的。

图中左端点在绿色区间,右端点在红色区间就是一个例子,同理我们可以以此类推下去。

image

而对于这样的 \(l,r\) 区间我们可以转化成二维平面上面的区间,

横纵坐标分别表示 \(l,r\),于是可以离线下来和询问一起跑一次扫描线即可。

这样会有 \(n\) 个矩形和 \(m\) 个询问的点,时间是完全满足的。


实现的时候我写的类似 Hanghang 的项链的方法,10 分钟完成。

#include <bits/stdc++.h>
using namespace std;

const int N=1e6+5;
int n,m,c[N],ans[N];
struct node{
  int pos,val;
  bool operator<(const node &rhs) const{return pos<rhs.pos;}
}tr[N<<2],a[N];
struct Nod{
  int l,r,id;
  bool operator <(const Nod &rhs) const{return r<rhs.r;}
}q[N];

#define mid ((l+r)>>1)
#define lc p<<1
#define rc p<<1|1
#define lson l,mid,lc
#define rson mid+1,r,rc

void pu(int p){tr[p]=min(tr[lc],tr[rc]);}
void upd(int l,int r,int p,int x,node val){
  if(l==r) return tr[p]=val,void();
  if(x<=mid) upd(lson,x,val);
  else upd(rson,x,val);pu(p);
}
node qry(int l,int r,int p,int x,int y){
  if(x<=l&&y>=r) return tr[p];
  if(y<=mid) return qry(lson,x,y);
  if(x>mid) return qry(rson,x,y);
  return min(qry(lson,x,y),qry(rson,x,y));
}

int main(){
  /*2023.11.30 H_W_Y CF1000F One Occurrence SGT*/
  ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
  cin>>n;
  for(int i=1,x;i<=n;i++) cin>>x,a[i]=(node){c[x],x},c[x]=i;
  cin>>m;
  for(int i=1;i<=m;i++) cin>>q[i].l>>q[i].r,q[i].id=i;
  sort(q+1,q+m+1);int it=0;
  for(int i=1;i<=n;i++){
  	if(a[i].pos) upd(1,n,1,a[i].pos,(node){n+1,0});
  	upd(1,n,1,i,a[i]);
  	while(it<m&&q[it+1].r==i){
  	  ++it;node res=qry(1,n,1,q[it].l,q[it].r);
  	  if(res.pos<q[it].l) ans[q[it].id]=res.val;
  	  else ans[q[it].id]=0;
  	}
  }
  for(int i=1;i<=m;i++) cout<<ans[i]<<'\n';
  return 0;
}

UOJ637【美团杯2021】A. 数据结构

【美团杯2021】A. 数据结构

给一个长为 \(n\) 的序列,\(m\) 次查询:

如果将区间 \([l,r]\) 中所有数都 \(+1\),那么整个序列有多少个不同的数?

询问间独立,也就是说每次查询后这个修改都会被撤销。

\(1 \le n,m \le 10^6\)

由于每一次我们询问的是整个序列的,

所以现在考虑离线下来反演,也就是枚举每一个元素。


发现每个颜色的出现个数是不好去枚举的,于是我们考虑枚举在那些区间操作时没有出现数 \(x\)

这也是好分析的,和上一道题有类似的方式 ,

我们在求出这些区间之后还要去与 \(x-1\) 的区间取交即可。

这都是比较好做的,于是这道题就做完了。


代码是简单的,要简单分情况讨论一下(还有一些存留的注释语句)。

#include <bits/stdc++.h>
using namespace std;
#define pb push_back
#define pii pair<int,int>
#define fi first
#define se second

const int N=1e6+5;
int n,m,cnt=0,ans[N],tr[N<<2];
vector<int> pos[N];
struct node{int l,r,v,ts;};
vector<node> g[N];
vector<pii> q[N];

#define mid ((l+r)>>1)
#define lc p<<1
#define rc p<<1|1
#define lson l,mid,lc
#define rson mid+1,r,rc

void pd(int p){tr[lc]+=tr[p],tr[rc]+=tr[p],tr[p]=0;}
void upd(int l,int r,int p,int x,int y,int val){
  if(x<=l&&y>=r) return tr[p]+=val,void();pd(p);
  if(x<=mid) upd(lson,x,y,val);
  if(y>mid) upd(rson,x,y,val);
}
int qry(int l,int r,int p,int x){
  if(l==r) return tr[p];pd(p);
  if(x<=mid) return qry(lson,x);
  return qry(rson,x);
}

int main(){
  /*2023.11.30 H_W_Y #637. 【美团杯2021】A. 数据结构 SGT*/
  ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
  cin>>n>>m;
  for(int i=1,x;i<=n;i++) cin>>x,pos[x].pb(i);
  for(int i=1,l,r;i<=m;i++) cin>>l>>r,q[l].pb({r,i});
  for(int i=1,l,r;i<=n+1;i++){
  	if(!pos[i].size()&&!pos[i-1].size()){g[1].pb((node){1,n,1,i});continue;}
  	if(!pos[i].size()){
  	  for(int j=1;j<pos[i-1].size();j++){
  	  	g[pos[i-1][j-1]+1].pb({pos[i-1][j-1]+1,pos[i-1][j]-1,1,i});
  	  	g[pos[i-1][j]].pb({pos[i-1][j-1]+1,pos[i-1][j]-1,-1,i});
  	  }
  	  if(pos[i-1][0]>1) g[1].pb({1,pos[i-1][0]-1,1,i}),g[pos[i-1][0]].pb({1,pos[i-1][0]-1,-1,i});
  	  if(pos[i-1].back()<n) g[pos[i-1].back()+1].pb({pos[i-1].back()+1,n,1,i});
  	  continue;
  	}
  	if(!pos[i-1].size()){
  	  g[1].pb({pos[i].back(),n,1,i}),g[pos[i][0]+1].pb({pos[i].back(),n,-1,i});
  	  continue;
  	}
  	l=pos[i][0],r=pos[i].back();
  	//cout<<i<<"->(l,r)= ("<<l<<","<<r<<")"<<endl;
  	for(int j=1;j<pos[i-1].size();j++){
  	  if(pos[i-1][j-1]+1<=l&&pos[i-1][j]-1>=r){
  	  	g[pos[i-1][j-1]+1].pb({r,pos[i-1][j]-1,1,i});
  	  	g[l+1].pb({r,pos[i-1][j]-1,-1,i});break;
  	  }
  	}
  	if(pos[i-1][0]-1>=r) g[1].pb({r,pos[i-1][0]-1,1,i}),g[l+1].pb({r,pos[i-1][0]-1,-1,i});
  	if(pos[i-1].back()+1<=l) g[pos[i-1].back()+1].pb({r,n,1,i}),g[l+1].pb({r,n,-1,i});
  }
  for(int i=1;i<=n;i++){
  	for(auto j:g[i]) upd(1,n,1,j.l,j.r,j.v);//cout<<i<<"= ("<<j.l<<","<<j.r<<","<<j.v<<") "<<j.ts<<endl;
  	for(auto j:q[i]) ans[j.se]=qry(1,n,1,j.fi);
  }
  for(int i=1;i<=m;i++) cout<<(n+1-ans[i])<<'\n';
  return 0;
}

Hdu5603 the soldier of love

Hdu5603 the soldier of love

\(n\) 个区间。

\(m\) 次询问,每次询问给出一些点(点是一维的),求对于这些点而言,有多少个初始给定的区间包含了至少这些点中的一个点。

\(n,m\),总点数 \(\le 3\times 10^5\)

也是比较套路的,我们直接考虑那些区间没有出现这些点,

他一定是一个阶梯状的,而每一个矩形是不交的,于是我们最后跑一遍扫描线就可以了。


这样用扫描线思维是不差的,但是实现很差!

还不如用 Hanghang 的项链的方法,用上区间数颜色的经典操作—— 维护颜色上一次出现的位置 即可。


#include <bits/stdc++.h>
using namespace std;
#define pii pair<int,int>
#define fi first
#define se second
#define pb push_back

const int N=1e6+5,mx=1e6;
int n,m,ans[N],tr[N],l[N];
vector<int> g[N];
vector<pii> q[N];

int lowbit(int i){return i&(-i);}
void upd(int x,int val){for(int i=x;i<=mx;i+=lowbit(i)) tr[i]+=val;}
int qry(int x){int res=0;for(int i=x;i>=1;i-=lowbit(i)) res+=tr[i];return res;}

int main(){
  /*2023.11.30 H_W_Y hdu5603 the soldier of love BIT*/
  while(scanf("%d%d",&n,&m)!=EOF){
  	for(int i=0;i<=mx;i++) ans[i]=tr[i]=l[i]=0,g[i].resize(0),q[i].resize(0);
    for(int i=1,x,y;i<=n;++i) scanf("%d%d",&x,&y),g[y].pb(x);
    for(int i=1,c;i<=m;++i){
      scanf("%d",&c);int lst=0;
      for(int j=1,x;j<=c;++j) scanf("%d",&x),q[x].pb({lst,i}),lst=x;
      l[i]=lst;
    }
    for(int i=1;i<=mx;++i){
  	  for(auto j:q[i]) ans[j.se]+=qry(i)-qry(j.fi);
  	  for(auto j:g[i]) upd(j,1);
    }
    for(int i=1;i<=m;++i) ans[i]+=n-qry(l[i]);
    for(int i=1;i<=m;++i) printf("%d\n",n-ans[i]);
  }
  return 0;
}

CF526F Pudding Monsters

CF526F Pudding Monsters

给定一个 \(n \times n\) 的棋盘,其中有 \(n\) 个棋子,每行每列恰好有一个棋子。

对于所有的 \(1 \leq k \leq n\),求有多少个 \(k \times k\) 的子棋盘中恰好有 \(k\) 个棋子,输出所有 \(k\) 对应的答案之和。

\(n \le 3 \times 10^5\)


容易发现这是一个排列,我们把二维的问题转化到一维上面。

一个区间 \([l,r]\) 满足条件当且仅当这个区间里面的 \(max-min\)\(r-l\) 相等,这是好理解的。

因为只有这样我们才能做到中间出现了 \(k\) 个数。


于是现在再来考虑如何统计。

同样用一个二维的平面来表示每一个区间,即点 \((i,j)\) 表示 \([i,j]\) 的区间。

这个区间可以被统计是当且仅当 \(max-min-(r-l)=0\) 的,而我们就希望取维护这个值。

首先对于 \(-j+i\) 是好维护的,就直接用 \(2n\) 个矩形的加减。

而对于这里的 \(max\)\(min\),我们可以用 Keynote 中提到的方法算出每一个值所影响的区间,

同样变成 \(2n\) 个矩形的加减就可以了。


这样我们就可以用 \(4n\) 个矩形的加减算出我们想要的值,

那么现在的问题就在于如何去维护 \(0\) 的个数。

容易发现 \(0\) 一定是这个值的最小值,所以我们直接维护最小值和最小值出现次数即可。


最后跑一次扫描线就做完了。

#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define pb push_back

const int N=2e6+5;
int n,a[N],st[N],tp=0;
ll ans=0;
struct node{int l,r,v;};
vector<node> g[N];

struct sgt{
  int val,cnt,tag;
}tr[N<<2];

#define mid ((l+r)>>1)
#define lc p<<1
#define rc p<<1|1
#define lson l,mid,lc
#define rson mid+1,r,rc

void pu(int p){
  if(tr[lc].val==tr[rc].val) tr[p].val=tr[lc].val,tr[p].cnt=tr[lc].cnt+tr[rc].cnt;
  else if(tr[lc].val<tr[rc].val) tr[p].val=tr[lc].val,tr[p].cnt=tr[lc].cnt;
  else tr[p].val=tr[rc].val,tr[p].cnt=tr[rc].cnt;
}

void pd(int p){
  if(!tr[p].tag) return ;
  tr[lc].tag+=tr[p].tag;
  tr[rc].tag+=tr[p].tag;
  tr[lc].val+=tr[p].tag;
  tr[rc].val+=tr[p].tag;
  tr[p].tag=0;
}

void upd(int l,int r,int p,int x,int y,int val){
  if(x<=l&&y>=r){tr[p].tag+=val;tr[p].val+=val;return;}pd(p);
  if(x<=mid) upd(lson,x,y,val);
  if(y>mid) upd(rson,x,y,val);pu(p);
}

void build(int l,int r,int p){
  if(l==r){tr[p].cnt=1,tr[p].val=tr[p].tag=0;return;}
  build(lson);build(rson);pu(p);
}

int main(){
  /*2023.11.30 H_W_Y CF526F Pudding Monsters SGT*/
  ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
  cin>>n;tp=0;build(1,n,1);
  for(int i=1,x,y;i<=n;i++){
  	cin>>x>>y,a[x]=y;
  	g[i].pb({1,n,i});g[i+1].pb({1,n,-i});g[1].pb({i,i,-i});
  }a[n+1]=n+1;
  for(int i=1;i<=n+1;i++){
  	while(tp>0&&a[st[tp]]<a[i]){
  	  g[st[tp-1]+1].pb({st[tp],i-1,a[st[tp]]});
  	  g[st[tp]+1].pb({st[tp],i-1,-a[st[tp]]});
  	  tp--;
  	}
  	st[++tp]=i;
  }a[n+1]=0;tp=0;
  for(int i=1;i<=n+1;i++){
  	while(tp>0&&a[st[tp]]>a[i]){
  	  g[st[tp-1]+1].pb({st[tp],i-1,-a[st[tp]]});
  	  g[st[tp]+1].pb({st[tp],i-1,a[st[tp]]});
  	  tp--; 
  	}
  	st[++tp]=i;
  }
  for(int i=1;i<=n;i++){
  	for(auto j:g[i]) upd(1,n,1,j.l,j.r,j.v);
  	if(tr[1].val==0) ans+=1ll*tr[1].cnt;
  }
  cout<<ans<<'\n';
  return 0;
}

区间子区间

一类非常常见的问题,也比较有套路性。

我们将 区间子区间 问题表示到二维的平面上面,同样用一个点表示一个区间,

那么区间子区间问题就是求一个二维矩阵中的答案个数,这样我们从左往右扫过来的过程中我们需要维护区间的历史信息和就可以了。

这样相当于会在线段树上面维护两个标记,需要分析一下两个标记交错时如何处理。


标记的处理可以自己手推,也可以采用 矩阵乘法 的方式(但是可能还是需要拆标记,因为常数较大)。


P8868 [NOIP2022] 比赛

P8868 [NOIP2022] 比赛

给定 \(A_{1,2,\dots,n},B_{1,2,\dots,n}\),以及 \(m\) 个询问,每次询问给出 \(l,r\),求

\[\sum_{p=l}^r \sum_{q=p}^r (\max_{i=p}^q A_i)(\max_{i=p}^q B_i) \]

\(1 \le n,m \le 2.5 \times 10^5\)

区间子区间问题典。


区间子区间问题的常规处理方法已经在 Keynote 中讲的很清楚了,

这道题的 \(\max\) 我们同样可以用单调栈维护出来,分析一下去加信息累加时的标记。


其实题解部分已经讲得很清楚了

主要的思想就是先将同类的标记合并之后,我们对于交错的标记再进行一个讨论就可以了,

也就是分组,累加时分类讨论。具体看代码吧。


代码中我们钦定一个标记组是先历史值累加再赋值。

#include <bits/stdc++.h>
using namespace std;
#define ull unsigned long long
#define pii pair<int,int>
#define fi first
#define se second
#define pb push_back

const int N=1e6+5;
int n,m,a[N],b[N],st[N],tp=0,fa[N],fb[N];
ull ans[N];
vector<pii> g[N];

struct tag{ull cx,cy,xy,x,y,c;}tg[N<<2];
struct sgt{
  ull s,xy,x,y;
  sgt operator +(const sgt &t)const{return (sgt){s+t.s,xy+t.xy,x+t.x,y+t.y};}
}tr[N<<2];

#define mid ((l+r)>>1)
#define lson l,mid,p<<1
#define rson mid+1,r,p<<1|1
#define lc p<<1
#define rc p<<1|1

void pu(int p){tr[p]=tr[lc]+tr[rc];}

/*先历史值累计再更新*/
void change(int l,int r,int p,tag t){
  ull &cx=tg[p].cx,&cy=tg[p].cy,&xy=tg[p].xy,&x=tg[p].x,&y=tg[p].y,&c=tg[p].c;
  int len=r-l+1;
  
  if(cx&&cy) c+=t.xy*cx*cy+t.x*cx+t.y*cy+t.c;
  else if(cx) y+=t.y+cx*t.xy,c+=cx*t.x+t.c;
  else if(cy) x+=t.x+cy*t.xy,c+=cy*t.y+t.c;
  else x+=t.x,y+=t.y,xy+=t.xy,c+=t.c;
  if(t.cx) cx=t.cx;
  if(t.cy) cy=t.cy;
  
  ull &s=tr[p].s,&sxy=tr[p].xy,&sx=tr[p].x,&sy=tr[p].y; 
  s+=t.xy*sxy+t.x*sx+t.y*sy+1ull*t.c*len;
  if(t.cx&&t.cy) sxy=t.cx*t.cy*len,sx=t.cx*len,sy=t.cy*len;
  else if(t.cx) sxy=t.cx*sy,sx=t.cx*len;
  else if(t.cy) sxy=t.cy*sx,sy=t.cy*len;
}

void pd(int l,int r,int p){
  if(tg[p].cx||tg[p].cy||tg[p].x||tg[p].xy||tg[p].y||tg[p].c) 
    change(lson,tg[p]),change(rson,tg[p]),tg[p]=(tag){0,0,0,0,0,0};
}

void upd(int l,int r,int p,int x,int y,tag t){
  if(x<=l&&y>=r) return change(l,r,p,t);pd(l,r,p);
  if(x<=mid) upd(lson,x,y,t);
  if(y>mid) upd(rson,x,y,t);pu(p);
}

ull qry(int l,int r,int p,int x,int y){
  if(x<=l&&y>=r) return tr[p].s;pd(l,r,p);
  if(y<=mid) return qry(lson,x,y);
  if(x>mid) return qry(rson,x,y);
  return qry(lson,x,y)+qry(rson,x,y);
}

int main(){
  /*2023.11.30 H_W_Y P8868 [NOIP2022] 比赛 SGT*/
  ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
  cin>>n;cin>>n;
  for(int i=1;i<=n;i++){
  	cin>>a[i];
  	while(tp>0&&a[st[tp]]<a[i]) tp--;
  	fa[i]=st[tp]+1;st[++tp]=i;
  }tp=0;
  for(int i=1;i<=n;i++){
  	cin>>b[i];
  	while(tp>0&&b[st[tp]]<b[i]) tp--;
  	fb[i]=st[tp]+1;st[++tp]=i;
  }
  cin>>m;
  for(int i=1,l,r;i<=m;i++) cin>>l>>r,g[r].pb({l,i});
  for(int r=1;r<=n;r++){
  	upd(1,n,1,fa[r],r,(tag){1ull*a[r],0,0,0,0,0});
  	upd(1,n,1,fb[r],r,(tag){0,1ull*b[r],0,0,0,0});
  	upd(1,n,1,1,r,(tag){0,0,1,0,0,0});
  	for(auto j:g[r]) ans[j.se]=qry(1,n,1,j.fi,r);
  }
  for(int i=1;i<=m;i++) cout<<ans[i]<<'\n';
  return 0;
}

CF997E Good Subsegments

CF997E Good Subsegments

就是 CF526F 的全局统计变成在 \([l,r]\) 区间内的统计。

\(1 \le n,q \le 1.2 \times 10^5\)

区间子区间问题,扫描线维护区间历史值的累加即可。


很多时候这种历史值累加的标记都只是争对最小值来做的,所以直接维护最小值的历史累加记录就可以了。

一般的操作就是去判断一下左右区间是否存在最小值。


感觉多少有点绕,但是需要分析一下什么时候需要下传标记,还是比较好理解。

#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define pii pair<int,int>
#define fi first
#define se second
#define pb push_back

const int N=1e6+5;
int n,m,a[N],st[N],lst[N],tp=0;
ll ans[N];
struct nod{int l,r,v;};
struct node{int l,r,id,op;};
vector<node> q[N];
vector<nod> g[N];

#define mid ((l+r)>>1)
#define lc p<<1
#define rc p<<1|1
#define lson l,mid,p<<1
#define rson mid+1,r,p<<1|1

struct sgt{
  ll mn,add,s;
  int cnt,tag;
}tr[N<<2];

void pu(int p){
  tr[p].mn=min(tr[lc].mn,tr[rc].mn);
  tr[p].s=tr[lc].s+tr[rc].s;tr[p].cnt=0;
  if(tr[lc].mn==tr[p].mn) tr[p].cnt+=tr[lc].cnt;
  if(tr[rc].mn==tr[p].mn) tr[p].cnt+=tr[rc].cnt;
}

void cadd(int p,ll val){tr[p].mn+=val,tr[p].add+=val;}
void ctag(int p,int val){tr[p].s+=1ll*tr[p].cnt*val,tr[p].tag+=val;}

void pd(int p){
  if(tr[p].add) cadd(lc,tr[p].add),cadd(rc,tr[p].add),tr[p].add=0;
  if(tr[p].tag){
  	if(tr[lc].mn==tr[p].mn) ctag(lc,tr[p].tag);
  	if(tr[rc].mn==tr[p].mn) ctag(rc,tr[p].tag);
  	tr[p].tag=0;
  }
}

void build(int l,int r,int p){
  if(l==r){
  	tr[p].mn=tr[p].tag=tr[p].s=tr[p].add=0;
  	tr[p].cnt=1;return;
  }
  build(lson);build(rson);pu(p);
}

void upd(int l,int r,int p,int x,int y,ll val){
  if(x<=l&&y>=r) return cadd(p,val),void();pd(p);
  if(x<=mid) upd(lson,x,y,val);
  if(y>mid) upd(rson,x,y,val);pu(p);
}

ll qry(int l,int r,int p,int x,int y){
  if(x<=l&&y>=r) return tr[p].s;pd(p);
  if(y<=mid) return qry(lson,x,y);
  if(x>mid) return qry(rson,x,y);
  return qry(lson,x,y)+qry(rson,x,y);
}

int main(){
  /*2023.12.2 H_W_Y CF997E Good Subsegments SGT*/
  ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
  cin>>n;build(1,n,1);
  for(int i=1;i<=n;i++) cin>>a[i],g[1].pb((nod){i,i,-i}),g[i].pb((nod){1,n,i}),g[i+1].pb((nod){1,n,-i});
  a[n+1]=n+1;
  for(int i=1;i<=n+1;i++){
    while(tp>0&&a[st[tp]]<a[i]){
      g[st[tp-1]+1].pb((nod){st[tp],i-1,a[st[tp]]});
      g[st[tp]+1].pb((nod){st[tp],i-1,-a[st[tp]]});
      tp--;
    }
    st[++tp]=i;
  }
  tp=0;a[n+1]=0;
  for(int i=1;i<=n+1;i++){
  	while(tp>0&&a[st[tp]]>a[i]){
  	  g[st[tp-1]+1].pb((nod){st[tp],i-1,-a[st[tp]]});
  	  g[st[tp]+1].pb((nod){st[tp],i-1,a[st[tp]]});
  	  tp--;
  	}
  	st[++tp]=i;
  }
  cin>>m;
  for(int i=1,l,r;i<=m;i++) cin>>l>>r,q[l-1].pb((node){l,r,i,-1}),q[r].pb((node){l,r,i,1});
  for(int i=1;i<=n;i++){
  	for(auto j:g[i]) upd(1,n,1,j.l,j.r,1ll*j.v);
  	ctag(1,1);
  	for(auto j:q[i]) ans[j.id]+=1ll*j.op*qry(1,n,1,j.l,j.r);
  }
  for(int i=1;i<=m;i++) cout<<ans[i]<<'\n';
  return 0;
}

Codeforces GYM 103069G EC final2020 G

Codeforces GYM 103069G EC final2020 G

给定一个序列,求区间有多少子区间,其内部出现过的颜色数为奇数。

\(1 \le n,m \le 10^5\)


同样是用扫描线维护,而每一次操作就是区间的异或操作。

具体来说就是维护每一个 \(l\) 表示左端点是 \(l\) 的答案,我们从左往右扫描 \(r\) 即可。

我们去维护区间内 \(0,1\) 的个数分别时多少,每一次再进行一次历史值累加即可。


区间子区间问题。

#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define pb push_back
#define pii pair<int,int> 
#define fi first
#define se second

const int N=1e6+5;
int n,m,a[N],lst[N],c[N];
ll ans[N];
vector<pii> g[N];

#define mid ((l+r)>>1)
#define lc p<<1
#define rc p<<1|1
#define lson l,mid,p<<1
#define rson mid+1,r,p<<1|1

struct sgt{int c[2];ll s;}tr[N<<2];
struct tag{int t[2],add;}tg[N<<2];

void pu(int p){
  tr[p].s=tr[lc].s+tr[rc].s;
  tr[p].c[0]=tr[lc].c[0]+tr[rc].c[0];
  tr[p].c[1]=tr[lc].c[1]+tr[rc].c[1];
}

void change(int p,tag nw){
  if(tg[p].add==0) tg[p].t[0]+=nw.t[0],tg[p].t[1]+=nw.t[1];
  else tg[p].t[0]+=nw.t[1],tg[p].t[1]+=nw.t[0];
  tg[p].add^=nw.add;
  
  tr[p].s+=1ll*nw.t[0]*tr[p].c[0]+1ll*nw.t[1]*tr[p].c[1];
  if(nw.add) swap(tr[p].c[0],tr[p].c[1]);
}

void pd(int p){
  if(!tg[p].t[0]&&!tg[p].t[1]&&!tg[p].add) return;
  change(lc,tg[p]);change(rc,tg[p]);tg[p]=(tag){{0,0},0};
}

void build(int l,int r,int p){
  if(l==r){
  	tr[p].c[0]=1,tr[p].c[1]=tr[p].s=0;
  	tg[p]=(tag){{0,0},0};return;
  }
  build(lson);build(rson);pu(p);
}

void upd(int l,int r,int p,int x,int y,tag val){
  if(x<=l&&y>=r) return change(p,val);pd(p);
  if(x<=mid) upd(lson,x,y,val);
  if(y>mid) upd(rson,x,y,val);pu(p);
}

ll qry(int l,int r,int p,int x,int y){
  if(x<=l&&y>=r) return tr[p].s;pd(p);
  if(y<=mid) return qry(lson,x,y);
  if(x>mid) return qry(rson,x,y);
  return qry(lson,x,y)+qry(rson,x,y);
}

int main(){
  /*2023.12.2 H_W_Y EC final 2020 G. Prof. Pang's sequence SGT*/
  ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
  cin>>n;build(1,n,1);
  for(int i=1;i<=n;i++) cin>>a[i],lst[i]=c[a[i]]+1,c[a[i]]=i;
  cin>>m;
  for(int i=1,l,r;i<=m;i++) cin>>l>>r,g[r].pb({l,i});
  for(int i=1;i<=n;i++){
  	upd(1,n,1,lst[i],i,(tag){{0,0},1});
  	upd(1,n,1,1,i,(tag){{0,1},0});
  	for(auto j:g[i]) ans[j.se]=qry(1,n,1,j.fi,i);
  }
  for(int i=1;i<=m;i++) cout<<ans[i]<<'\n';
  return 0;
}

换维扫描线

一个非常巧妙的思路,有时想到了就完全胜利了。


在做 DS 问题的时候我们可以采用枚举算法的方法,而在扫描线的时候我们也需要枚举从哪里扫,

简单来说,扫描线一般都是按照时间的先后顺序依次扫过去的,

而在有些题目中,这是非常不好做的,所以我们考虑 换维

具体来说假设现在构成的二维平面又两维,时间和序列,我们可以尝试按照序列依次扫过去,把区间的修改变成单点的修改和查询。


LOJ3489 JOISC2021 饮食区

LOJ3489 JOISC2021 饮食区

有一个长为 \(n\) 的序列,序列每个位置有个队列。

\(m\) 个操作。

  1. 每个操作形如 \([l,r]\) 的每个队列中进来了 \(k\)\(type=c\) 的人。

  2. 或者 \([l,r]\) 的每个队列中出去了 \(k\) 个人(不足 \(k\) 个则全部出去)

  3. 还有查询某个队列中第 \(k\) 个人的 \(type\)(不足 \(k\) 个输出 \(0\)

\(1 \le n,m,q \le 2.5 \times 10^5\)


首先容易想到扫描线,但是直接做是困难的。

所以我们考虑 换维扫描线,从左往右扫描每一个序列,

于是每一个区间操作就变成了单点修改,而操作二是相当于清空一段前缀。


具体建出来平面直角坐标系就是这样的(lxl 的图):

image

这里的黑色线段表示着每一次修改,红色线是一次关于前缀的查询。


前面是好理解的,但是发现后面删除前面的 \(k\) 个人和找第 \(k\) 个人并不好处理。

我们用了线段树去维护时间轴,而对于每一次的修改操作看成一次单点的修改,

每一次的清空操作我们看作是前缀减去 \(k\)


那么每一次查询的时候,我们就只需要求到一个上一次清空到的位置再整体二分就可以了。

如何找到上一次清空到的位置?


发现前面被清空的前缀和一定是 \(\le 0\) 的,如果我们现在维护一个后缀和,

那么后缀和最大的位置就一定是上一次清空的位置,这是好理解的。


于是我们就可以轻松找到这个位置,于是再二分就可以了。

这道题的重点在于换维扫描线,感觉讲起来比较抽象。


实现部分代码中比较清楚。

#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define pii pair<int,int>
#define pll pair<int,ll>
#define fi first
#define se second
#define pb push_back

const int N=5e5+5;
int n,m,q,ans[N],col[N];
vector<pii> add[N],del[N];
vector<pll> f[N];

struct SGT{
  struct sgt{ll mx,mn,tag;}tr[N<<2];
  #define mid ((l+r)>>1)
  #define lc p<<1
  #define rc p<<1|1
  #define lson l,mid,lc
  #define rson mid+1,r,rc
  
  void pu(int p){
  	tr[p].mx=max(tr[lc].mx,tr[rc].mx);
  	tr[p].mn=min(tr[lc].mn,tr[rc].mn);
  }
  
  void change(int p,ll v){tr[p].mx+=v,tr[p].mn+=v,tr[p].tag+=v;}
  
  void pd(int p){
  	if(!tr[p].tag) return ;
  	change(lc,tr[p].tag);change(rc,tr[p].tag);tr[p].tag=0;
  }
  
  void upd(int l,int r,int p,int x,int y,ll v){
    if(x<=l&&y>=r) return change(p,v),void();pd(p);
    if(x<=mid) upd(lson,x,y,v);
    if(y>mid) upd(rson,x,y,v);pu(p);
  }
  
  ll qry(int l,int r,int p,int x){
  	if(l==r) return tr[p].mn;pd(p);
  	if(x<=mid) return qry(lson,x);
  	if(x>mid) return qry(rson,x);
  	return -1;
  }
  
  ll qry_mn(int l,int r,int p,int x,int y){
    if(x<=l&&y>=r) return tr[p].mn;pd(p);
    if(y<=mid) return qry_mn(lson,x,y);
    if(x>mid) return qry_mn(rson,x,y);
    return min(qry_mn(lson,x,y),qry_mn(rson,x,y));
  }
  
  int find(int l,int r,int p,int x,int y,ll v){
  	if(tr[p].mx<v) return 0;
  	if(l==r) return col[l];pd(p);
  	if(x<=l&&y>=r){
  	  if(tr[rc].mx>=v) return find(rson,x,y,v);
  	  return find(lson,x,y,v); 
  	}else if(y>mid){
  	  int nw=find(rson,x,y,v);
  	  if(nw) return nw;
  	}
  	return find(lson,x,y,v);
  }
}T1,T2;

int main(){
  /*2023.12.2 H_W_Y JOISC2021 C - フードコート(Foodcourt) SGT*/
  ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
  cin>>n>>m>>q;
  for(int i=1,op,l,r,c,k;i<=q;i++){
  	cin>>op;
  	if(op==1){
  	  cin>>l>>r>>c>>k;col[i]=c;
  	  add[l].pb({i,k});add[r+1].pb({i,-k});
  	}else if(op==2){
  	  cin>>l>>r>>k;
  	  del[l].pb({i,-k});del[r+1].pb({i,k});
  	}else{
  	  ll x;cin>>l>>x;
  	  f[l].pb({i,x});
  	}
  }
  memset(ans,-1,sizeof(ans));
  for(int i=1;i<=n;i++){
    for(auto j:add[i]) T1.upd(1,q,1,j.fi,q,1ll*j.se),T2.upd(1,q,1,1,j.fi,1ll*j.se);
    for(auto j:del[i]) T1.upd(1,q,1,j.fi,q,1ll*j.se);
    for(auto j:f[i]){
      ll x=T1.qry(1,q,1,j.fi)-min(0ll,T1.qry_mn(1,q,1,1,j.fi)),y=j.se;
      if(y>x) ans[j.fi]=0;
      else{
      	x=x-y+1;
      	ans[j.fi]=T2.find(1,q,1,1,j.fi,x+T2.qry(1,q,1,j.fi));
      }
    }
  }
  for(int i=1;i<=q;i++) if(ans[i]!=-1) cout<<ans[i]<<'\n';
  return 0;
}

P3863 序列

P3863 序列

给定一个长度为 \(n\) 的序列,给出 \(q\) 个操作,形如:

1 l r x 表示将序列下标介于 \([l,r]\) 的元素加上 \(x\) (请注意,\(x\) 可能为负)

2 p y 表示查询 \(a_p\) 在过去的多少秒时间内不小于 \(y\) (不包括这一秒,细节请参照样例)

开始时为第 \(0\) 秒,第 \(i\) 个操作发生在第 \(i\) 秒。

\(1 \le n,q \le 10^5\)

Mea orz


看到题目就很像是去用换维扫描线维护时间轴,

但是似乎并不是想象那么好做。


发现我们不知道如何去维护权值大小,这是非常难做的。

所以我们考虑一种根号数据结构——分块。


在每一个块中,我们将元素按照大小排序,查询和修改都是简单的。

于是时间复杂度是 \(\mathcal O(n \sqrt n \log n)\)\(2s\) 的时限是可以通过的。

#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define pb push_back

const int N=1e5+5;
int n,m,bl[N],L[N],R[N],cnt,ans[N];
ll a[N],t[N];
struct node{int op,pos;ll v;};
struct Nod{
  int pos;ll v;
  bool operator <(const Nod &rhs) const{return (v==rhs.v)?(pos<rhs.pos):(v<rhs.v);}
}p[N];
vector<node> g[N];

void upd(int l,ll v){
  for(int i=L[bl[l]];i<=R[bl[l]];i++) if(p[i].pos>=l) p[i].v+=v;
  sort(p+L[bl[l]],p+R[bl[l]]+1);
  for(int i=bl[l]+1;i<=bl[m];i++) t[i]+=v; 
}

int qry(int r,ll v){
  int res=0;
  for(int i=L[bl[r]];i<=R[bl[r]];i++) if(p[i].pos<r&&p[i].v+t[bl[r]]>=v) res++;
  for(int i=1;i<bl[r];i++) res+=cnt-(lower_bound(p+L[i],p+R[i]+1,(Nod){0,v-t[i]})-p-L[i]+1)+1;
  if(v<=0) res++;
  return res;
}

int main(){
  /*2023.12.2 H_W_Y P3863 序列 FK*/
  ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
  cin>>n>>m;
  for(int i=1;i<=n;i++) cin>>a[i];
  for(int i=1,op,l,r;i<=m;i++){
  	cin>>op;ll v;
  	if(op==1) cin>>l>>r>>v,g[l].pb((node){op,i,v}),g[r+1].pb((node){op,i,-v});
    else cin>>l>>v,g[l].pb((node){op,i,v});
  }
  cnt=(int)floor(sqrt(n));
  for(int i=1;i<=m;i++){
  	bl[i]=(i-1)/cnt+1;p[i]=(Nod){i,0};
  	if(!L[bl[i]]) L[bl[i]]=i,R[bl[i]-1]=i-1;
  }
  R[bl[m]]=m;
  memset(ans,-1,sizeof(ans));
  for(int i=1;i<=n;i++)
  	for(auto j:g[i]){
  	  if(j.op==1) upd(j.pos,j.v);
  	  else ans[j.pos]=qry(j.pos,j.v-a[i]);
	}
  for(int i=1;i<=m;i++) if(~ans[i]) cout<<ans[i]<<'\n';
  return 0;
}


P8518 [IOI2021] 分糖果

P8518 [IOI2021] 分糖果

Khong 阿姨正在给附近一所学校的学生准备 \(n\) 盒糖果。盒子的编号分别为 \(0\)\(n - 1\),开始时盒子都为空。第 \(i\) 个盒子 \((0 \leq i \leq n - 1)\) 至多可以容纳 \(c[i]\) 块糖果(容量为 \(c[i]\))。

Khong 阿姨花了 \(q\) 天时间准备糖果盒。在第 \(j\)\((0 \leq j \leq q - 1)\),她根据三个整数 \(l[j]\)\(r[j]\)\(v[j]\) 执行操作,其中 \(0 \leq l[j] \leq r[j] \leq n - 1\)\(v[j] \neq 0\)。对于每个编号满足 \(l[j] \leq k \leq r[j]\) 的盒子 \(k\)

  • 如果 \(v[j] > 0\),Khong 阿姨将糖果一块接一块地放入第 \(k\) 个盒子,直到她正好放了 \(v[j]\) 块糖果或者该盒子已满。也就是说,如果该盒子在这次操作之前已有 \(p\) 块糖果,那么在这次操作之后盒子将有 \(\min(c[k], p + v[j])\) 块糖果。

  • 如果 \(v[j] < 0\),Khong 阿姨将糖果一块接一块地从第 \(k\) 个盒子取出,直到她正好从盒子中取出 \(-v[j]\) 块糖果或者该盒子已空。也就是说,如果该盒子在这次操作之前已有 \(p\) 块糖果,那么在这次操作之后盒子将有 \(\max(0, p + v[j])\) 块糖果。

你的任务是求出 \(q\) 天之后每个盒子中糖果的数量。

\(1 \le n,q \le 2 \times 10^5\)


上周一次联考考到了,考场上想出来了,但是代码死活调不出来,

于是后面就没有心情去写了,直到现在。


发现直接做是几乎不能做的,

所以我们考虑 换维扫描线,枚举每一个位置,用线段树维护时间轴上的值。


发现对于最后的答案,我们其实只关心最后一次到达上界/下界的时刻,

而答案就是后面的时间所造成的区间和。


那么我们如何去找这个值呢?

(联考的时候没想清楚搞了很久)

发现其实我们去维护一个区间的前缀最小值和最大值,

对于序列的某一个后缀而言,如果它的前缀最小最大的差值 \(\gt c_i\) 就说明它撞了两次界,

这个求出来之后就可以轻松的求到最终的值了——分类讨论一下最后一次撞的是哪一个界。

而找到这个最靠后的点我们可以直接用线段树二分实现,于是这道题就做完了。


难点还是在于换维扫描线。

#include <bits/stdc++.h>
using namespace std;
#define pii pair<int,int>
#define ll long long
#define fi first
#define se second
#define pb push_back

const int N=2e5+5;
int n,m;
ll a[N];
vector<pii> g[N];
vector<int> ans;

#define mid ((l+r)>>1)
#define lc p<<1
#define rc p<<1|1
#define lson l,mid,lc
#define rson mid+1,r,rc

struct sgt{
  ll s,mx,mn;
  sgt operator +(const sgt &nw){return (sgt){s+nw.s,max(mx,s+nw.mx),min(mn,s+nw.mn)};}
  void init(ll x){s=x;mx=max(0ll,x);mn=min(0ll,x);}
}tr[N<<2];

void pu(int p){tr[p]=tr[lc]+tr[rc];}

void upd(int l,int r,int p,int x,ll v){
  if(l==r) return tr[p].init(tr[p].s+v),void();
  if(x<=mid) upd(lson,x,v);
  else upd(rson,x,v);pu(p);
}

ll qry(int l,int r,int p,ll v,sgt nw){
  if(l==r){
  	if((tr[p].s<0&&nw.mx>v)||(tr[p].s>0&&nw.mn>=-v)) return v+nw.s-nw.mx;
  	else return nw.s-nw.mn;
  }
  sgt cur=tr[rc]+nw;
  if(cur.mx-cur.mn<=v) return qry(lson,v,cur);
  return qry(rson,v,nw);
}

/*2023.12.2 H_W_Y P8518 [IOI2021] 分糖果 SGT*/
std::vector<int> distribute_candies(std::vector<int> c, std::vector<int> l,std::vector<int> r, std::vector<int> v){
  ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
  n=c.size();m=l.size();
  for(int i=0;i<n;i++) a[i+1]=c[i],g[i].resize(0);
  for(int i=0;i<m;i++) g[l[i]+1].pb({i+1,v[i]}),g[r[i]+2].pb({i+1,-v[i]});
  ans.resize(0);
  for(int i=1;i<=n;++i){
  	for(auto j:g[i]) upd(0,m,1,j.fi,1ll*j.se);
  	ans.pb(qry(0,m,1,a[i],(sgt){0,0,0}));
  }
  return ans;
}

分块

分块,俗称 优雅的暴力,用到了根号平衡的思路,对于区间加和区间和问题可以做到 \(O(\sqrt n)\) 修改,\(O(\sqrt n)\) 查询的复杂度。

如果在分支结构上很难快速合并某些信息时,我们就可以利用分块来做。


分块希望完成的:快速整块修改快速零散块重构

个人认为分块其实是根号数据结构的基础吧,而根号数据结构也有一些其他的用处,比如可以用根号平衡的思想对于一些算法去归约,例如下面就是一个例子:

矩阵乘法归约

感觉还挺有意思的,可以证明一些算法的时间复杂度一定高于矩阵乘法的时间复杂度。

我们设这里有一个代码 std.cpp,它是用来解决 树上多次询问路径颜色数的

现在我们考虑矩阵乘法,构造两个矩阵 \(A,B\),我们令我们把这两个矩阵丢给 std.cpp ,它可以帮我们算出 \(A \times B = C\)

这里我们钦定 \(A,B\) 的值域都是 \([0,1]\) 的,(如果不是也是可以完成的,就相当于进行一个二进制的拆位)

那么现在考虑如何转化?


我们设矩阵是 \(\sqrt n \times \sqrt n\) 的,

于是我们去构造一棵树,使得它有 \(\sqrt n\) 条链且每条链都是 \(\sqrt n\) 个节点。

那么 \(A_{i,j}\) 表示第 \(i\) 条链是否有第 \(j\) 种颜色。


同理我们将 \(B\) 矩阵转置后也像这样表示到一棵树上面,

这棵树就成了这样:

![](E:\H_W_Y\0-C++#代码编程\cdqz\lxl 数据结构\pic\jzcfgy.png)

绿色和红色分别表示 \(A,B\) 矩阵所构成的链。


现在我们考虑进行 \(n\) 次询问,

每一次询问绿色的端点 \(x\) 到红色的端点 \(y\) 上面的颜色数。

令得到的矩阵是 \(C\)

那么

\[C_{x,y} = \sum_{i=1}^\sqrt n A_{x,i} | B_{i,y} \]

由于我们的 \(B\) 是转置后才映射到图上面的,所以是正确的。


发现这个形式和矩阵乘法很像,而 \(|\) 可以等价于 \(\times\)

\(\sqrt n \times \sqrt n\) 矩阵乘法的时间复杂度是 \(\mathcal O(n \sqrt n)\) 的,

所以我们可以证明到这个问题的时间复杂度是 \(\ge n \sqrt n\) 的。


分块当然也有很多技巧,什么逐块处理,分块后分治之类的,详见下面的例题。


基础例题

P2801 教主的魔法

P2801 教主的魔法

维护一个序列,支持:

  1. 区间加。
  2. 区间查询 \(\lt x\) 的数的个数。

\(1 \le n \le 10^6,1 \le m \le 3 \times 10^3\)

典题是快乐的。


这和前面 序列 这道题似乎没有什么大的变化。

发现我们需要维护每个数的名次,这是分治数据结构不能在低时间复杂度之内完成的,于是只好考虑根号数据结构。

考虑分块维护,每个块内维护块内排好序的数组,修改时整块直接打标记,散块暴力重构即可,

查询时也只需要对于整块二分,散块暴力找。

这样的时间复杂度就是 \(\mathcal O(m \sqrt {n \log n})\) 的,非常优秀。代码

重构没有归并,实现非常草率。/kk

void upd(int l,int r,ll x){
  for(auto &i:g[bl(l)].G)
    if(i.se>=l&&i.se<=r) i.fi+=x;
  sort(g[bl(l)].G.begin(),g[bl(l)].G.end());
  
  if(bl(l)==bl(r)) return;
  
  for(int i=bl(l)+1;i<bl(r);i++)
    g[i].t+=x;
  
  for(auto &i:g[bl(r)].G)
    if(i.se<=r) i.fi+=x;
  sort(g[bl(r)].G.begin(),g[bl(r)].G.end());
}

int qry(int l,int r,ll x){
  int cnt=0;
  for(auto i:g[bl(l)].G){
    if(i.fi>=x) break;
	if(i.se>=l&&i.se<=r) ++cnt;
  }
  
  if(bl(l)==bl(r)) return (r-l+1)-cnt;
  
  for(int i=bl(l)+1;i<bl(r);i++){
  	ll nw=x-g[i].t;
  	int L=0,R=(int)g[i].G.size();
    while(L<R){
      int mid=((L+R)>>1);
      if(mid<(int)g[i].G.size()&&g[i].G[mid].fi>=nw) R=mid;
      else L=mid+1;
	}
	cnt+=L;
  }
  
  for(auto i:g[bl(r)].G){
  	if(i.fi>=x) break;
  	if(i.se<=r) ++cnt;
  }
  
  return r-l+1-cnt;
}

int main(){
  /*2024.3.24 H_W_Y P2801 教主的魔法 分块*/ 

  #ifdef H_W_Y
  freopen("1.in","r",stdin);
  #endif
  
  ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
  cin>>n>>m;
  for(int i=1;i<=n;i++) cin>>a[i];
  
  while(g[nw].R!=n){
  	++nw;
  	g[nw].L=g[nw-1].R+1,g[nw].R=min(g[nw].L+B-1,n);
  	g[nw].t=0;
  	
  	for(int i=g[nw].L;i<=g[nw].R;i++)
  	  g[nw].G.pb({a[i],i});
  	
  	sort(g[nw].G.begin(),g[nw].G.end());
  }
  
  while(m--){
  	char ch;
  	int l,r,x;
  	cin>>ch>>l>>r>>x;
  	if(ch=='M') upd(l,r,x);
  	else cout<<qry(l,r,x)<<'\n';
  }
  
  return 0;
}

似乎也有根号做法,但是巨难写并且也不一定不这个快啊。


P5356 [Ynoi2017] 由乃打扑克

P5356 [Ynoi2017] 由乃打扑克

维护一个序列,支持:

  1. 区间加。
  2. 区间查询第 \(k\) 小。

\(1 \le n,m \le 10^5\)

疑似有点过于失败了。


首先,如果直接按照上一道题的思路去完成,也就是每次二分后找 \(\le x\) 的数的个数,时间复杂度 \(\mathcal O(m \sqrt{n \log n} \log V)\),很遗憾,被 lxl 无情的卡掉了。

但真的不能用这个思路了吗?

和上一道题一样,转到分治数据结构上面也没法做啊。


考虑优化,首先查询的时候把两边的零散块拼起来变成一个假的块去做二分一定会更优。

我们假设块长为 \(B\),那么修改的时间复杂度 \(O(\frac n B + B)\),查询复杂度是 \(O(B + \frac n B \log B \log V)\)

那么我们如果取 \(B= \sqrt n \log n\),这时两边的时间复杂度相同,时间复杂度最优为 \(\mathcal O(m \sqrt n \log V)\)


但是实现时还是要求挺高,搞不好还是会被卡常。代码

本题同样存在 \(\mathcal O(m \sqrt {n \log n})\) 的做法,这里不做介绍。

代码实现同样很草率。/kk

#include <bits/stdc++.h>
using namespace std;
#define int long long

const int N=1e5+5,B=450;
int n,m,bl[N],L[N/B+5],R[N/B+5],tag[N/B+5],len;
struct Nod{
  int x,id;
  Nod(int X=0,int Y=0){x=X,id=Y;}
  bool operator <(const Nod &a) const{
    if(x!=a.x) return x<a.x;
    return id<a.id;
  }
}b[N],c[N];
struct nod{
  int l,r,nw;
}a[N/B+1];

void upd(int l,int r,int x){
  int ql=bl[l],qr=bl[r];
  for(int i=L[ql];i<=R[ql];i++) if(b[i].id>=l&&b[i].id<=r) b[i].x+=x;
  sort(b+L[ql],b+R[ql]+1);
  if(ql==qr) return;
  for(int i=ql+1;i<qr;i++) tag[i]+=x;
  for(int i=L[qr];i<=R[qr];i++) if(b[i].id<=r) b[i].x+=x;
  sort(b+L[qr],b+R[qr]+1);
}

int qry(int l,int r,int k){
  if(k>r-l+1) return -1;

  int ql=bl[l],qr=bl[r];len=0;
  for(int i=L[ql];i<=R[ql];i++) if(b[i].id>=l&&b[i].id<=r) c[++len]=Nod(b[i].x+tag[ql],b[i].id);
  if(ql!=qr) for(int i=L[qr];i<=R[qr];i++) if(b[i].id<=r) c[++len]=Nod(b[i].x+tag[qr],b[i].id);
  sort(c+1,c+len+1);
  
  int mn=2e9,mx=-2e9,al=1,ar=len,nw=0,ans=0;
  if(len>0) mn=min(mn,c[1].x),mx=max(mx,c[len].x);
  for(int i=ql+1;i<qr;i++){
    mn=min(mn,b[L[i]].x+tag[i]);
	mx=max(mx,b[R[i]].x+tag[i]);
    a[i].l=L[i],a[i].r=R[i],a[i].nw=L[i]-1;
  }
  
  while(mn<=mx){
  	int mid=(mn+mx)/2,s=0;
	
	nw=lower_bound(c+al,c+ar+1,Nod(mid+1,0))-c-1;
  	s+=nw;
  	
	for(int i=ql+1;i<qr;i++){
	  a[i].nw=lower_bound(b+a[i].l,b+a[i].r+1,Nod(mid-tag[i]+1,0))-b-1;	
	  s+=a[i].nw-L[i]+1;
	}
  	if(s<k){
  	  mn=mid+1;
	  al=nw;
	  for(int i=ql+1;i<qr;i++) a[i].l=max(a[i].nw,L[i]);	
	}else{
	  ans=mid;mx=mid-1;
      ar=nw;
      for(int i=ql+1;i<qr;i++) a[i].r=a[i].nw;
	}
  }
  return ans;
}

signed main(){
  /*2024.3.24 H_W_Y P5356 [Ynoi2017] 由乃打扑克 分块*/   
  ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
  cin>>n>>m;
  for(int i=1;i<=n;i++) cin>>b[i].x,b[i].id=i,bl[i]=(i-1)/B+1;
  for(int i=1;i<=bl[n];i++){
    L[i]=R[i-1]+1,R[i]=min(R[i-1]+B,n);
    sort(b+L[i],b+R[i]+1);
  }
  while(m--){
  	int op,l,r,x;cin>>op>>l>>r>>x;
  	if(op==1) cout<<qry(l,r,x)<<'\n';
  	else upd(l,r,x);
  }
  return 0;
}

P5048 [Ynoi2019 模拟赛] Yuno loves sqrt technology III

P5048 [Ynoi2019 模拟赛] Yuno loves sqrt technology III

给一个序列,每次查询一个区间的众数,强制在线。

头一天下午盯了一个小时没看出来,第二天早上一分钟干完了。消愁。


考虑分块,对块与块之间维护众数的出现次数。

散块暴力枚举,对于每一种数用一个 vector 存下出现的下标,每次可以用 \(\log n\) 的时间找到一个数的出现次数。

总时间复杂度 \(O(n \sqrt n \log n)\),很明显过不了。


考虑优化,我们发现,可以现找出块与块的众数,然后散块中用这个点在 vector 中的下标加/减去出现次数,判断那个数是否还在区间内。

如果在,用 vector 暴力拓展,由于散块只有 \(O(\sqrt n)\) 个,所以暴力拓展也只会有 \(O(\sqrt n)\) 次。

总时间复杂度 \(O(n \sqrt n)\)代码

int qry(int l,int r){
  ans=0;
  if(bl[l]==bl[r]){
    for(int i=l;i<=r;i++) ans=max(ans,++c[a[i]]);
    for(int i=l;i<=r;i++) c[a[i]]=0;
    return ans;
  }
  
  ans=f[bl[l]+1][bl[r]-1];
  for(int i=l;i<=R[bl[l]];i++)
    while(pos[i]+ans<(int)G[a[i]].size()&&G[a[i]][pos[i]+ans]<=r) ++ans;
  for(int i=L[bl[r]];i<=r;i++)
    while(pos[i]-ans>=0&&G[a[i]][pos[i]-ans]>=l) ++ans;
  return ans;
}

int main(){
  /*2024.3.26 H_W_Y P5048 [Ynoi2019 模拟赛] Yuno loves sqrt technology III 分块*/ 
  ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
  cin>>n>>m;
  for(int i=1;i<=n;i++) cin>>a[i],b[i]=a[i],bl[i]=(i-1)/B+1;
  sort(b+1,b+n+1);
  len=unique(b+1,b+n+1)-b-1;
  
  for(int i=1;i<=n;i++){
    a[i]=lower_bound(b+1,b+len+1,a[i])-b;
    G[a[i]].pb(i),pos[i]=(int)G[a[i]].size()-1;
  }
  for(int i=1;i<=bl[n];i++) L[i]=R[i-1]+1,R[i]=min(R[i-1]+B,n);
  
  for(int i=1;i<=bl[n];i++){
    ans=0;
    for(int j=i;j<=bl[n];j++){
      for(int k=L[j];k<=R[j];k++)
        ans=max(ans,++c[a[k]]);
      f[i][j]=ans;
    }
    
    for(int j=i;j<=bl[n];j++)
      for(int k=L[j];k<=R[j];k++)
        c[a[k]]=0;
  }
  
  while(m--){
    int l,r;cin>>l>>r;
    l^=lst,r^=lst;
    cout<<(lst=qry(l,r))<<'\n';
  }
  return 0;
}

P5046 [Ynoi2019 模拟赛] Yuno loves sqrt technology I

P5046 [Ynoi2019 模拟赛] Yuno loves sqrt technology I

给一个排列,查询区间逆序对,强制在线。

同一个错误犯两次还是非常难以忍受的。


考虑分块,容易发现贡献分为:

散块-散块,散块-整块,整块-整块。

同时后面两个部分还包括每一个块内的贡献。


考虑如何实现?

我们预处理出块内的贡献,并维护 \(f_{i,j}\) 表示前 \(i\)\(\le a_j\) 的数的个数和 \(g_{i,j}\) 表示前 \(i\) 块与第 \(j\) 块的贡献,

则每次可以以 \(O(\sqrt n)\) 的时间计算出整块-整块和散块-整块的贡献。

对于散块-散块,我们预处理出散块排序之后的数组,直接归并即可,时间复杂度同样 \(O(\sqrt n)\)


总时间复杂度 \(O((n+m)\sqrt n)\),常数较大。

不开 long long 的时候就别开,其实就只会有答案需要开。代码

const int N=1e5+5,B=320;
int n,m,a[N],b[N],w1[N],w2[N],c1=0,c2=0,bl[N],L[N],R[N],pos[N];
ll lst=0;
int s[N/B+3][N],t[N/B+3][N/B+3],S1[N/B+3],S2[N],f[N],pre[N],suf[N],g[B+3][N];
/*
s: 前 i 块 <= a_j 的个数
t: 前 j 块 <= i 块中元素的个数
pre: 块内前缀逆序对数
suf: 块内后缀逆序对数
g: j 和 L[bl[j]]+i 区间内的逆序对个数 
f: i 块内后面比你小的数的个数 
*/
struct Nod{
  int id,x;
  Nod(int X=0,int Y=0){id=X,x=Y;}
  bool operator <(const Nod &a) const{return x<a.x;}
}A[N];

inline void upd(int x,int k){
  for(rg int i=bl[x];i<=bl[n];++i) S1[i]+=k;
  for(rg int i=x;i<=R[bl[x]];++i) S2[i]+=k;
}
inline int qry(int x){return S1[bl[x]-1]+S2[x];}

inline void build(int x){
  c1=0;
  
  for(rg int i=L[x];i<=R[x];++i) w1[++c1]=a[i];
  sort(w1+1,w1+c1+1);
  
  for(rg int i=L[x];i<=R[x];++i) b[i]=w1[i-L[x]+1];
  for(rg int i=1,j=0;i<=n;++i){
    while(j<c1&&w1[j+1]<A[i].x) ++j;
    s[x][A[i].id]=j;
  }
  
  for(rg int i=R[x]+1;i>L[x];--i){
    if(i<=R[x]) upd(a[i],1);
    for(rg int j=L[x];j<i;++j) g[i-L[x]-1][j]=f[j]-qry(a[j]);
  }
  
  for(rg int i=1;i<=bl[n];++i){
    S1[i]=0;int nw=R[i];
    while(S2[nw]) S2[nw]=0,--nw;
  }
}

inline ll qry(int l,int r){
  ll res=0;
  if(bl[l]==bl[r]){
    for(rg int i=l;i<=r;++i) res+=1ll*g[r-L[bl[l]]][i];
    return res;
  }
  
  c1=c2=0;
  for(rg int i=L[bl[l]];i<=R[bl[l]];++i) if(pos[b[i]]>=l) w1[++c1]=b[i];
  for(rg int i=L[bl[r]];i<=R[bl[r]];++i) if(pos[b[i]]<=r) w2[++c2]=b[i];
  
  for(rg int i=1,j=0;i<=c1;++i){
    while(j<c2&&w2[j+1]<w1[i]) ++j;
    res+=j;
  }
  for(rg int i=l;i<=R[bl[l]];++i) res+=1ll*s[bl[r]-1][i]-1ll*s[bl[l]][i];
  for(rg int i=L[bl[r]];i<=r;++i) res+=1ll*(R[bl[r]-1]-R[bl[l]])-1ll*s[bl[r]-1][i]+1ll*s[bl[l]][i];
  for(rg int i=bl[l]+1;i<bl[r];++i) res+=1ll*(t[i][bl[r]-1]-t[i][i]+pre[R[i]]);
  return res+1ll*suf[l]+1ll*pre[r]; 
  
}

int main(){
  #ifdef H_W_Y
  freopen("1.in","r",stdin);
  freopen("1.out","w",stdout);
  #endif
  
  /*2024.3.27 H_W_Y P5046 [Ynoi2019 模拟赛] Yuno loves sqrt technology I 分块*/ 
  ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
  cin>>n>>m;
  
  for(rg int i=1;i<=n;++i) cin>>a[i],A[i]=Nod(i,a[i]),bl[i]=(i-1)/B+1,pos[a[i]]=i;
  sort(A+1,A+n+1);
  for(rg int i=1;i<=bl[n];++i) L[i]=R[i-1]+1,R[i]=min(R[i-1]+B,n);
  
  for(rg int i=1;i<=bl[n];++i){
    for(rg int j=L[i];j<=R[i];++j){
      if(j!=L[i]) pre[j]=pre[j-1]+j-L[i]-qry(a[j]);
      upd(a[j],1);
    }
    for(rg int j=L[i];j<=R[i];++j) upd(a[j],-1);
    for(rg int j=R[i];j>=L[i];--j){
      if(j!=R[i]) suf[j]=suf[j+1]+(f[j]=qry(a[j]));
      upd(a[j],1);
    }
    for(rg int j=L[i];j<=R[i];++j) upd(a[j],-1);
    build(i);
  }
  
  for(rg int i=1;i<=bl[n];++i)
    for(rg int j=1;j<=n;++j){
      s[i][j]+=s[i-1][j];
      t[bl[j]][i]+=s[i][j]; 
    }

  while(m--){
    ll l,r;cin>>l>>r;
    l^=lst,r^=lst;
    cout<<(lst=qry(l,r))<<'\n';
  }
  return 0;
}

逐块处理

离线方法。

如果对询问,每个块的答案之间相对独立,则我们可以对每个块统计每次询问其对答案的贡献,这样只需要同时考虑一个块。

原本我们一个块如果需要维护较为复杂的信息,一般会导致 \(O(\sqrt n)\) 个块,每个块维护了 \(O(n)\) 大小的数组,造成 \(O(n \sqrt n)\) 的空间。

使用逐块处理的方法则可以空间做到 \(O(n)\),并且常数上有改进(缓存原因)。

是非常常用的 trick,但如果不卡空间的话没有题一定要用这个。


P6779 [Ynoi2009] rla1rmdq

P6779 [Ynoi2009] rla1rmdq

给定一棵 \(n\) 个节点的树,树有边权,与一个长为 \(n\) 的序列 \(a\)

定义节点 \(x\) 的父亲为 \(fa(x)\),根 \(rt\) 满足 \(fa(rt)=rt\)

定义节点 \(x\) 的深度 \(dep(x)\) 为其到根简单路径上所有边权和。

\(m\) 次操作:

1 l r:对于 \(l \le i \le r\)\(a_i := fa(a_i)\)

2 l r :查询对于 \(l \le i \le r\),最小的 \(dep(a_i)\)

感觉就很复杂的题目。


考虑 分块

对于每一个整块而言,如果块内有 \(x\)\(y\) 上面,那么 \(y\) 是没用的。

这样的 \(y\) 是不需要统计的,

所以如果我们暴力维护每一个点所在位置,每一次往上跳的时候,

如果一个点跳到了块内有一个点跳过的位置,那么这个点就没有用了。

于是对于每一个块,它只会把树上的每一个点遍历一次,

均摊下来的复杂度是 \(\mathcal O(n \sqrt n)\) 的。


再来考虑剩下来的散块。

发现我们希望快速往上面跳 \(k\) 次祖先。

于是我们可以采用重链剖分去重构即可。


由于这题卡空间,所以需要用到逐块处理的技巧,对于每一个块分别处理。

注意树剖跳 \(k\) 级祖先的写法!!!小心常数。代码

inline int find(int u,int k){
  if(dep[u]-1<=k) return rt;
  while(dfn[u]-dfn[top[u]]+1<=k) k-=dfn[u]-dfn[top[u]]+1,u=fa[top[u]];
  return rev[dfn[u]-k];//不需要暴力跳! 
}

int c[N],st[N];
bool vis[N],inv[N];

inline void sol(int id){
  int L=(id-1)*B+1,R=min(id*B,n),tag=0,tmp=0,tp=0;
  res=inf;
  
  for(int i=1;i<=n;i++)
    vis[i]=inv[i]=c[i]=st[i]=0;
  
  for(int i=L;i<=R;i++)
    if(!vis[a[i]]) vis[a[i]]=inv[i]=1,st[++tp]=i,res=min(res,d[a[i]]);
  
  for(int i=1;i<=m;i++){
    int l=max(q[i].l,L),r=min(q[i].r,R);
    if(l>r) continue;

    if(q[i].op==1){
      if(l==L&&r==R){
        tmp=tp;
        tp=0,++tag;
        for(int i=1;i<=tmp;i++){
          c[st[i]]++;
          a[st[i]]=fa[a[st[i]]];
          if(vis[a[st[i]]]) inv[st[i]]=0;
          else{
            st[++tp]=st[i];
            res=min(res,d[a[st[i]]]);
            vis[a[st[i]]]=1;
          }
        }
      }else{
        for(int j=l;j<=r;j++){
          a[j]=find(a[j],tag-c[j]+1);
          c[j]=tag;
          if(!vis[a[j]]){
            res=min(res,d[a[j]]);
            vis[a[j]]=1;
            if(!inv[j]) st[++tp]=j,inv[j]=1;
          }
        }
      }
    }else{
      if(l==L&&r==R) ans[i]=min(ans[i],res);
      else
        for(int j=l;j<=r;j++)
          ans[i]=min(ans[i],d[a[j]=find(a[j],tag-c[j])]),c[j]=tag;
    }
  }
  
}

int main(){
  /*2024.3.25 H_W_Y P6779 [Ynoi2009] rla1rmdq 分块 + 均摊*/ 
  ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
  cin>>n>>m>>rt;
  for(int i=1,u,v,w;i<n;i++) cin>>u>>v>>w,add(u,v,w);
  dfs1(rt,rt);dfs2(rt,rt);
  
  for(int i=1;i<=n;i++) cin>>a[i],bl[i]=(i-1)/B+1;
  for(int i=1;i<=m;i++) cin>>q[i].op>>q[i].l>>q[i].r,ans[i]=inf;
  
  for(int i=1;i<=bl[n];i++) sol(i);
  for(int i=1;i<=m;i++) if(q[i].op==2) cout<<ans[i]<<'\n';
  return 0;
}

P4117 [Ynoi2018] 五彩斑斓的世界

P4117 [Ynoi2018] 五彩斑斓的世界

给你一个长度为 \(n\) 的序列 \(a\),有 \(m\) 次操作:

  1. 把区间 \([l,r]\) 所有大于 \(x\) 的数减去 \(x\)
  2. 查询区间 \([l,r]\) 内的 \(x\) 的出现次数。

值域 \(10^5\),空间 \(64MB\)

第二分块。


值域这么小,那复杂度不难想到和值域是相关的。

考虑每一次把 \(\gt x\) 的减去 \(x\),也可以转化成把所有的数都 \(-x\),再对 \(\le 0\) 加上 \(x\)

而如果我们能够做到以 \(O(x)\) 的时间复杂度完成修改操作,那么这道题也就做完了。


发现这是可以的,考虑分块,对于每一块逐块处理。

我们设一个块的最大值是 \(mx\),那么整块修改就是这样的:

  • \(mx \ge 2x\),我们将 \([1,x]\) 合并到区间 \([x+1,2x]\),相当于操作之后把 \(\le 0\) 的加上 \(x\),并打上标记。
  • \(mx \lt 2x\),我们将 \([x+1,mx]\) 合并到区间 \([1,x]\),即之间减去 \(x\),不打标记。

这两者的时间复杂度都是不超过 \(O(x)\) 的,所以对于每一块的时间复杂度不超过 \(O(n)\),认为值域与 \(n\) 同阶。

而对于散块直接暴力修改重构即可,时间复杂度是 \(O(n \sqrt n)\)


考虑如何维护?

原题题解给了三种方法,这里介绍用并查集维护的方法。

对于每个颜色维护一个并查集,那么上面讨论的其实就是合并两个并查集,同时我们可以维护每个并查集的大小,这样可以很好地解决查询的问题。

于是总时间复杂度 \(\mathcal O((n+m) \sqrt n)\),逐块处理之后空间复杂度 \(O(n)\)代码


大分块

一些比较难也比较恶心的分块。


P4119 [Ynoi2018] 未来日记

P4119 [Ynoi2018] 未来日记

给你一个长度为 \(n\) 的序列 \(a\),有 \(m\) 次操作:

  1. 把区间 \([l,r]\) 内所有的 \(x\) 变成 \(y\)
  2. 查询 \([l,r]\) 内第 \(k\) 小值。

第一分块。

又是把 \(i\) 打成 \(j\) 的一天~


首先考虑如何维护第二个操作,求区间 \(k\) 小值。

发现我们可以对序列和值域都进行分块,记 \(c1_{i,j}\) 表示前 \(i\) 块值域在第 \(j\) 块的个数,\(c2_{i,j}\) 表示前 \(i\) 块为 \(j\) 的个数。

查询时我们先对两边的散块进行处理使得它称为一个假的块,维护上面两者的信息,

在从小到大枚举每一个值域块,如果当前的个数已经比 \(k\) 大,那么就到这个块内枚举答案。

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

int qry(int l,int r,int k){
  if(bl[l]==bl[r]){
    find(bl[l]);
    for(int i=l;i<=r;i++) s2[i]=a[i];
    nth_element(s2+l,s2+l+k-1,s2+r+1);
    int res=s2[l+k-1];
    for(int i=l;i<=r;i++) s2[i]=0;
    return res;
  }
  
  find(bl[l]),find(bl[r]);
  for(int i=l;i<=R[bl[l]];i++) ++s1[bl[a[i]]],++s2[a[i]];
  for(int i=L[bl[r]];i<=r;i++) ++s1[bl[a[i]]],++s2[a[i]];
  
  int sum=0;
  for(int i=1;i<=bl[N-1];i++){
    
    if(sum+s1[i]+c1[bl[r]-1][i]-c1[bl[l]][i]>=k){
        
      for(int j=(i-1)*B+1;j<=i*B;j++){
          
        if(sum+s2[j]+c2[bl[r]-1][j]-c2[bl[l]][j]>=k){
          for(int p=l;p<=R[bl[l]];p++) --s1[bl[a[p]]],--s2[a[p]];
          for(int p=L[bl[r]];p<=r;p++) --s1[bl[a[p]]],--s2[a[p]];
          return j;
        }else sum+=s2[j]+c2[bl[r]-1][j]-c2[bl[l]][j];
      }
    }else sum+=s1[i]+c1[bl[r]-1][i]-c1[bl[l]][i];
  }
}

再来考虑如何维护修改操作。

对于两边的散块,直接暴力重构即可;

而对于中间的整块,如果根本没有 \(x\),就直接跳过;

如果没有 \(y\),我们可以直接把 \(x\) 直接映射成 \(y\)

否则这一块既有 \(x\) 又有 \(y\),这意味着 \(x\)\(y\) 之间发生了合并,不妨直接暴力重构整块。

因为有 \(c\) 数组,我们可以在 \(O(1)\) 的时间内知道某一块是否有某个数。


考虑什么时候会发生重构呢?

每次重构会合并两个数,而原本的序列提供了 \(O(n)\) 次重构,而由于每次修改对于散块可能使得不同数的个数 \(+1\),所以一共只会重构 \(O(n+m)\) 次。

由于每一次重构的时间复杂度都是 \(O(\sqrt n)\),所以总时间复杂度是 \(\mathcal O((n+m) \sqrt n)\),并不影响时间。

void upd(int l,int r,int x,int y){
  if(x==y||c2[bl[r]][x]-c2[bl[l]-1][x]==0) return ;

  for(int i=bl[n];i>=bl[l];i--){
    c1[i][bl[x]]-=c1[i-1][bl[x]],c1[i][bl[y]]-=c1[i-1][bl[y]];
    c2[i][x]-=c2[i-1][x],c2[i][y]-=c2[i-1][y];
  }
  
  if(bl[l]==bl[r]){
    find(bl[l]),bf(l,r,x,y),build(bl[l]);
    for(int i=bl[l];i<=bl[n];i++){
      c1[i][bl[x]]+=c1[i-1][bl[x]],c1[i][bl[y]]+=c1[i-1][bl[y]];
      c2[i][x]+=c2[i-1][x],c2[i][y]+=c2[i-1][y];
    }
    return ;
  }
  
  find(bl[l]),bf(l,R[bl[l]],x,y),build(bl[l]);
  find(bl[r]),bf(L[bl[r]],r,x,y),build(bl[r]);
  
  for(int i=bl[l]+1;i<bl[r];i++){
    if(!c2[i][x]) continue;
    if(c2[i][y]) find(i),bf(L[i],R[i],x,y),build(i);
    else{
      c1[i][bl[y]]+=c2[i][x],c2[i][y]+=c2[i][x];
      c1[i][bl[x]]-=c2[i][x],c2[i][x]=0;
      id[i][y]=id[i][x],rid[i][id[i][x]]=y,id[i][x]=0;
    }
  }

  for(int i=bl[l];i<=bl[n];i++){
    c1[i][bl[x]]+=c1[i-1][bl[x]],c1[i][bl[y]]+=c1[i-1][bl[y]];
    c2[i][x]+=c2[i-1][x],c2[i][y]+=c2[i-1][y];
  }
}

这题主要的思路就是通过对序列和权值一起进行分块,然后让序列分块的 \(O( \sqrt n)\) 部分乘上值域分块的 \(O( 1 )\) 部分,序列分块的 \(O( 1 )\) 部分乘上值域分块的 \(O( \sqrt n )\) 部分,从而达到了复杂度的平衡。

总时间复杂度 \(\mathcal O((n+m)\sqrt n)\)代码

void build(int x){
  int cnt=0;
  for(int i=1;i<=B;i++) id[x][rid[x][i]]=0;
  for(int i=L[x];i<=R[x];i++){
    if(!id[x][a[i]]) id[x][a[i]]=++cnt,rid[x][cnt]=a[i];
    pos[i]=id[x][a[i]];
  }
}

void find(int x){
  for(int i=L[x];i<=R[x];i++)
    a[i]=rid[x][pos[i]];
}

void bf(int l,int r,int x,int y){
  for(int i=l;i<=r;i++)
    if(a[i]==x){
      --c1[bl[l]][bl[x]],++c1[bl[l]][bl[y]];
      --c2[bl[l]][x],++c2[bl[l]][y];
      a[i]=y;
    }
}

void upd(int l,int r,int x,int y){

}

int qry(int l,int r,int k){

}

int main(){
  ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
  cin>>n>>m;
  
  for(int i=1;i<N;i++) bl[i]=(i-1)/B+1;
  for(int i=1;i<=n;i++) cin>>a[i];
  for(int i=1;i<=bl[n];i++){
    L[i]=R[i-1]+1,R[i]=min(R[i-1]+B,n);
    build(i);
    for(int j=1;j<=bl[N-1];j++) c1[i][j]=c1[i-1][j];
    for(int j=1;j<N;j++) c2[i][j]=c2[i-1][j];
    for(int j=L[i];j<=R[i];j++) ++c1[i][bl[a[j]]],++c2[i][a[j]];
  }
  
  while(m--){
    int op,l,r,x,y;
    cin>>op>>l>>r>>x;
    if(op==1) cin>>y,upd(l,r,x,y);
    else cout<<qry(l,r,x)<<'\n';
  }
  return 0;
}

P3203 [HNOI2010] 弹飞绵羊

P3203 [HNOI2010] 弹飞绵羊

某天,Lostmonkey 发明了一种超级弹力装置,为了在他的绵羊朋友面前显摆,他邀请小绵羊一起玩个游戏。

游戏一开始,Lostmonkey 在地上沿着一条直线摆上 \(n\) 个装置,每个装置设定初始弹力系数 \(k_i\),当绵羊达到第 \(i\) 个装置时,它会往后弹 \(k_i\) 步,达到第 \(i+k_i\) 个装置,若不存在第 \(i+k_i\) 个装置,则绵羊被弹飞。

绵羊想知道当它从第 \(i\) 个装置起步时,被弹几次后会被弹飞。为了使得游戏更有趣,Lostmonkey 可以修改某个弹力装置的弹力系数,任何时候弹力系数均为正整数。

LCT 的板题,但是我们不用 LCT。


考虑分块,每 \(\sqrt n\) 个元素为一块,

每个元素维护它下一个跳到的位置和第一次跳出这个块的点,同时记录一下跳出去的次数。

每一次修改直接对块内重构一下,查询时就不对往下面的那一个块跳,即可做到 \(O(n \sqrt n)\)

实现非常简单。代码


P7446 [Ynoi2007] rfplca

P7446 [Ynoi2007] rfplca

给定一棵大小为 \(n\)\(1\) 为根节点的树,树用如下方式给出:输入 \(a_2,a_3,\dots,a_n\),保证 \(1\leq a_i<i\),将 \(a_i\)\(i\) 连边形成一棵树。

接下来有 \(m\) 次操作,操作有两种:

  • 1 l r x\(a_i=\max(a_i-x,1)(l\leq i\leq r)\)
  • 2 u v 查询在当前的 \(a\) 数组构成的树上 \(u,v\) 的 LCA。

\(x \ge 1\)

被 smb 秒了。/bx,上一道题相当于它的铺垫吧。


我们同样考虑分块,每个点维护它下一次跳到的位置 \(fa_i\) 和第一次跳出块的地方 \(pa_i\)

考虑每一次修改,对于散块我们暴力重构,但是整块呢?

由于每次的 \(x \ge 1\),所以对于每一个块,最多做这样的整块修改 \(\sqrt n\) 次后所有的元素的 \(pa_i=fa_i\) 了。

所以当修改次数 \(\le \sqrt n\) 时,我们就直接暴力重构整个块;

而之后就直接对于整块打标记即可。


跳的时候类似于倍增往上跳就可以了。

总时间复杂度 \(\mathcal O(n \sqrt n)\)代码


P5063 [Ynoi2014] 置身天上之森

P5063 [Ynoi2014] 置身天上之森

线段树是一种特殊的二叉树,满足以下性质:

每个点和一个区间对应,且有一个整数权值;

根节点对应的区间是 \([1,n]\)

如果一个点对应的区间是 \([l,r]\),且 \(l<r\),那么它的左孩子和右孩子分别对应区间 \([l,m]\)\([m+1,r]\),其中 \(m=\lfloor\frac{l+r}{2}\rfloor\)

如果一个点对应的区间是 \([l,r]\),且 \(l=r\),那么这个点是叶子;

如果一个点不是叶子,那么它的权值等于左孩子和右孩子的权值之和。

珂朵莉需要维护一棵线段树,叶子的权值初始为 \(0\),接下来会进行 \(m\) 次操作:

操作 \(1\):给出 \(l,r,a\),对每个 \(x\)\(l\leq x\leq r\)),将 \([x,x]\) 对应的叶子的权值加上 \(a\),非叶节点的权值相应变化;

操作 \(2\):给出 \(l,r,a\),询问有多少个线段树上的点,满足这个点对应的区间被 \([l,r]\) 包含,且权值小于等于 \(a\)

崩。


对于一棵线段树,最多只有 \(O(\log n)\) 个长度不同的节点。

而不同长度的区间,修改是非常不好维护的,所以我们对于每一种节点大小分层,维护一个数据结构:

  1. 区间加
  2. 单点修改
  3. 区间 rank

这是经典问题,归约可以证明上界是 \(O(m \sqrt n)\)


可以证明线段树的每层只有 \(O(1)\) 种不同大小的节点,那么我们的总时间复杂度是 \(O( m \sqrt n + m \sqrt {\frac n 2} + m \sqrt {\frac n 4} +\cdots ) = O(m \sqrt n)\)

实现需要比较精细,否则要卡常的。代码

#include <bits/stdc++.h>
using namespace std;
#define ll long long

const int N=1e5+5;
int mp[N],tp=0,n,m;

struct Nod{
  int n,len,T,S;
  
  struct Nd{
    int l,r;
    ll v;
    Nd(int L=0,int R=0,ll V=0){l=L,r=R,v=V;}
    bool operator <(const Nd &a) const{return v<a.v;}
  }A[N];
  
  struct Bl{
    int L,R,lm,rm;
    ll tag;
    Bl(int a=0,int b=0,int c=0,int d=0,ll t=0){L=a,R=b,lm=c,rm=d,tag=t;}
    bool operator <(const Bl &a) const{return rm<a.rm;}
  }B[405];
  
  void init(){
    S=sqrt(n),T=0;
    while(B[T].R<n){
      ++T;
      B[T].L=B[T-1].R+1;
      B[T].R=B[T-1].R+S;
    }
    B[T].R=n;
    for(int i=1;i<=T;i++) B[i].lm=A[B[i].L].l,B[i].rm=A[B[i].R].r;
  }
  
  void upd(int x,int l,int r,ll v){
    for(int i=B[x].L;i<=B[x].R;i++){
      if(A[i].l>r||A[i].r<l) continue;
      A[i].v+=1ll*(min(r,A[i].r)-max(l,A[i].l)+1)*v;
    }
    sort(A+B[x].L,A+B[x].R+1);
  }
  
  void Upd(int l,int r,ll v){
    int x=lower_bound(B+1,B+T+1,Bl(-1,-1,-1,l,-1))-B;
    if(x>T) return;
    if(B[x].lm<=l&&B[x].rm>=r){
      if(B[x].lm==l&&B[x].rm==r) B[x].tag+=1ll*len*v;
      else upd(x,l,r,v);
      return;
    }
    if(B[x].lm<l) upd(x,l,r,v),++x;
    while(x<=T&&B[x].rm<=r) B[x++].tag+=1ll*len*v;
    if(x>T) return;
    if(B[x].lm<=r) upd(x,l,r,v);
  }
  
  ll qry(int x,bool fl,int l,int r,ll v){
    if(fl) return upper_bound(A+B[x].L,A+B[x].R+1,Nd(-1,-1,v-B[x].tag))-A-B[x].L;
    ll res=0;
    for(int i=B[x].L;i<=B[x].R;i++)
      res+=(A[i].l>=l&&A[i].r<=r&&A[i].v<=v-B[x].tag);
    return res;
  }
  
  ll Qry(int l,int r,ll v){
    int x=lower_bound(B+1,B+T+1,Bl(-1,-1,-1,l,-1))-B;
    if(x>T) return 0;
    if(B[x].lm<=l&&B[x].rm>=r){
      if(B[x].lm==l&&B[x].rm==r) return qry(x,1,l,r,v);
      return qry(x,0,l,r,v);
    }
    ll res=0;
    if(B[x].lm<l) res+=qry(x,0,l,r,v),++x;
    while(x<=T&&B[x].rm<=r) res+=qry(x,1,l,r,v),++x;
    if(x>T) return res;
    if(B[x].lm<=r) res+=qry(x,0,l,r,v);
    return res;
  }
}G[40];

#define mid ((l+r)>>1)

void build(int l,int r){
  int len=r-l+1;
  if(!mp[len]) mp[len]=++tp,G[tp].len=len;
  ++G[mp[len]].n;
  int nw=G[mp[len]].n;
  G[mp[len]].A[nw].l=l;
  G[mp[len]].A[nw].r=r;
  if(l==r) return;
  build(l,mid),build(mid+1,r);
}

void Upd(int l,int r,ll v){
  for(int i=1;i<=tp;i++) G[i].Upd(l,r,v);
}

ll Qry(int l,int r,ll v){
  ll res=0;
  for(int i=1;i<=tp;i++) res+=G[i].Qry(l,r,v);
  return res;
}

int main(){
  /*2024.3.28 H_W_Y P5063 [Ynoi2014] 置身天上之森 分块*/ 
  ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
  cin>>n>>m;
  build(1,n);
  for(int i=1;i<=tp;i++) G[i].init();
  
  while(m--){
    int op,l,r,a;
    cin>>op>>l>>r>>a;
    if(op==1) Upd(l,r,a);
    else cout<<Qry(l,r,a)<<'\n';
  }
  return 0;
}

分块部分到此结束!有点过于折磨人了。


莫队

莫队算法其实是包含了很多的数据结构算法,lxl:这是我最擅长的板块。

莫队相当于 二维扫描线,它是有两个自由度的。

而我们普通的扫描线其实也相当于一个 前缀莫队,所以莫队是包含扫描线的。


而莫队解决的问题就是没有办法同时降两维的问题,也就是 区间不独立

具体来说,

类似于区间颜色数,区间小 Z 的袜子,和区间逆序对。

这些贡献都是于区间本身相关,是扫描线不能完成的,这个时候就只能用莫队完成。


而把莫队拍到树上,就会变成两个东西:树上莫队子树莫队

子树莫队 说简单一点就是 dsu on tree,它是可以以 \(\mathcal O(n \log n)\) 时间完成的。


关于莫队的复杂度,具体来说是 \(\mathcal O(n \sqrt m+m)\) 的,前者为修改复杂度,后者为查询复杂度。


而在具体的做题当中,

差分 是一种很重要的思想,它可以 无代价地降低自由度,从而使得问题更好处理。

而差分又不只是前缀差分,还可以是后缀差分,后面有道题会用到。


再来讲一讲 区间子区间 问题,

我们在 Day 2 中已经知道可以用扫描线完成了,

而区间子区间的另一种解决方案就是 莫队 + 差分,后面的题目也会涉及到。


莫队还有一个应用就是 回滚莫队

其实它于普通莫队的时间最多是 \(\log\) 的,因为回滚莫队就相当于把莫队线段树分治一下。


关于 分块和莫队 有很多小技巧,

感觉课上用的最多的就是去均摊时间。

比如修改的时候莫队的时间已经是 \(n \sqrt m\),为了使整个复杂度不带 \(\log\)

我们可以使用分块去做到 \(\mathcal O(1)\) 修改和 \(\mathcal O(\sqrt n)\) 查询。

这样就可以把时间复杂度变成 \(\mathcal O(n \sqrt m+m \sqrt n)\)


而对于那些插入代价非 \(\mathcal O(1)\) 的题目,我们可以考虑拆点使得插入的代价变成 \(\mathcal O(1)\)


很多时候我们还会去 改变块的长度去均摊复杂度,或许在后面也会用到。


莫队基础


P1494 [国家集训队] 小 Z 的袜子

P1494 [国家集训队] 小 Z 的袜子

给出一个长度为 \(n\) 的序列 \(a\)

\(m\) 次询问每次求区间 \([l,r]\) 中有多少 \(i,j\) 满足 \(a_i = a_j\)

还是比较容易的一道题目。


\(c\) 数组表示每一种颜色的出现次数,

那么我们其实就是求

\[\begin{align} & \frac{c_0(c_0-1)/2+c_1(c_1-1)/2\dots}{(r-l+1)(r-l)/2}\\ = & \frac{{c_0}^2+{c_1}^2 + \dots -(c_0 + c_1 + \dots)}{(r-l+1)(r-l)}\\ = & \frac{{c_0}^2 + {c_1}^2 + \dots - (r-l+1)}{(r-l+1)(r-l)} \end{align} \]

于是直接用莫队维护一下每个颜色的个数平方和即可。

#include <bits/stdc++.h>
using namespace std;
#define ll long long

const int N=1e6+5;
int n,m,a[N],c[N],bl[N],B,l,r;
ll ans;
struct node{
  int l,r,id;
  bool operator <(const node &rhs) const{
  	if(bl[l]!=bl[rhs.l]) return bl[l]<bl[rhs.l];
  	return r<rhs.r;
  }
}q[N];
struct Answer{ll a,b;}p[N];

ll gcd(ll x,ll y){
  if(y==0) return x;
  return gcd(y,x%y);
}

void chg(int x,int v){
  if(x==0) return;
  ans-=1ll*c[a[x]]*c[a[x]];
  c[a[x]]+=v;
  ans+=1ll*c[a[x]]*c[a[x]];
}

int main(){
  /*2023.12.5 H_W_Y P1494 [国家集训队] 小 Z 的袜子 fk*/
  ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
  cin>>n>>m;B=(int)floor(sqrt(n));
  for(int i=1;i<=n;i++) cin>>a[i],bl[i]=(i-1)/B+1;
  for(int i=1;i<=m;i++) cin>>q[i].l>>q[i].r,q[i].id=i;
  sort(q+1,q+m+1);l=r=0;ans=0;
  for(int i=1;i<=m;i++){
  	if(q[i].l==q[i].r){p[q[i].id]=(Answer){0,1};continue;};
  	while(r<q[i].r) chg(++r,1);
  	while(l>q[i].l) chg(--l,1);
  	while(r>q[i].r) chg(r--,-1);
  	while(l<q[i].l) chg(l++,-1);
  	ll g=gcd(ans-1ll*(q[i].r-q[i].l+1),1ll*(q[i].r-q[i].l+1)*(q[i].r-q[i].l));
  	p[q[i].id].a=(ans-1ll*(q[i].r-q[i].l+1))/g;
  	p[q[i].id].b=1ll*(q[i].r-q[i].l+1)*(q[i].r-q[i].l)/g;
  }
  for(int i=1;i<=m;i++) cout<<p[i].a<<'/'<<p[i].b<<'\n';
  return 0;
}

P4396 [AHOI2013] 作业

P4396 [AHOI2013] 作业

查询区间 \([l,r]\) 中值在 \([a,b]\) 内的不同数个数。

首先容易想到对于 \([l,r]\) 进行莫队,

而对于 \([a,b]\),我们可以用树状数组维护,但是这样会多一个 \(\log\)


所以我们考虑分块值域做到 \(\mathcal O(1)\) 修改。

于是就做完了,时间复杂度 \(\mathcal O(n \sqrt m)\)

#include <bits/stdc++.h>
using namespace std;

const int N=1e5+5;
int n,m,a[N],bl[N],B,c[N],b[N],l,r,B2;
struct fk{int num,s;}t[N],ans[N];
struct node{
  int l,r,a,b,id;
  bool operator <(const node &rhs) const{
  	if((l/B2)!=(rhs.l/B2)) return (l/B2)<(rhs.l/B2);
  	return r<rhs.r;
  }
}q[N];

void ins(int x){
  c[x]++;if(c[x]==1) t[bl[x]].num++;
  t[bl[x]].s++;
}

void del(int x){
  c[x]--;if(c[x]==0) t[bl[x]].num--;
  t[bl[x]].s--;
}

fk calc(int L,int R){
  fk res=(fk){0,0};
  for(int i=L;i<=min(R,bl[L]*B);++i) res.num+=(c[i]>0),res.s+=c[i];
  if(bl[L]==bl[R]) return res;
  for(int i=(bl[R]-1)*B+1;i<=R;++i) res.num+=(c[i]>0),res.s+=c[i];
  for(int i=bl[L]+1;i<bl[R];++i) res.num+=t[i].num,res.s+=t[i].s;
  return res;
}

int main(){
  /*2023.12.5 H_W_Y P4396 [AHOI2013] 作业 md*/
  ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
  cin>>n>>m;B=sqrt(100000),B2=(int)sqrt(n);
  for(int i=1;i<=100000;i++) bl[i]=(i-1)/B+1;
  
  for(int i=1;i<=n;i++) cin>>a[i],b[i]=a[i];
  
  for(int i=1;i<=m;i++) cin>>q[i].l>>q[i].r>>q[i].a>>q[i].b,q[i].id=i;
  sort(q+1,q+m+1);l=1,r=0;
  
  for(int i=1;i<=m;i++){
  	while(r<q[i].r) ins(a[++r]);
  	while(l>q[i].l) ins(a[--l]);
  	while(r>q[i].r) del(a[r--]);
  	while(l<q[i].l) del(a[l++]);
  	ans[q[i].id]=calc(q[i].a,q[i].b);
  }
  for(int i=1;i<=m;i++) cout<<ans[i].s<<' '<<ans[i].num<<'\n';
  return 0;
}

CF617E XOR and Favorite Number

CF617E XOR and Favorite Number

给定一个长度为 \(n\) 的序列 \(a\),然后再给一个数字 \(k\),再给出 \(m\) 组询问,每组询问给出一个区间,求这个区间里面有多少个子区间的异或值为 \(k\)

区间子区间 问题,我们用莫队处理。


首先做一个差分,也就是维护异或的前缀和。

于是每一次插入一个数的时候我们就加上 \(x \oplus k\) 的出现次数,于是直接用莫队维护就可以了。

#include <bits/stdc++.h>
using namespace std;
#define ll long long

const int N=1e5+5,M=1048577;
int n,m,k,a[N],c[M],B,l,r;
ll ans[N],res=0;
struct node{
  int l,r,id;
  bool operator <(const node &rhs) const{
  	if((l/B)!=(rhs.l/B)) return (l/B)<(rhs.l/B);
  	return r<rhs.r;
  }
}q[N];

void ins(int x){res+=1ll*c[x^k];c[x]++;}
void del(int x){c[x]--,res-=1ll*c[x^k];}

int main(){
  /*2023.12.5 H_W_Y CF617E XOR and Favorite Number md*/
  ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
  cin>>n>>m>>k;B=sqrt(n);
  for(int i=1;i<=n;i++) cin>>a[i],a[i]^=a[i-1];
  for(int i=1;i<=m;i++) cin>>q[i].l>>q[i].r,q[i].l--,q[i].id=i;
  sort(q+1,q+m+1);l=0,r=-1;
  for(int i=1;i<=m;i++){
  	while(r<q[i].r) ins(a[++r]);
  	while(l>q[i].l) ins(a[--l]);
  	while(r>q[i].r) del(a[r--]);
  	while(l<q[i].l) del(a[l++]);
  	ans[q[i].id]=res;
  }
  for(int i=1;i<=m;i++) cout<<ans[i]<<'\n';
  return 0;
}

P5268 [SNOI2017] 一个简单的询问

P5268 [SNOI2017] 一个简单的询问

给你一个长度为 \(N\) 的序列 \(a_i\)\(1\leq i\leq N\),和 \(q\) 组询问,每组询问读入 \(l_1,r_1,l_2,r_2\),需输出

\[\sum\limits_{x=0}^\infty \text{get}(l_1,r_1,x)\times \text{get}(l_2,r_2,x) \]

$ \text{get}(l,r,x)$ 表示计算区间 \([l,r]\) 中,数字 \(x\) 出现了多少次。

\(1 \le N \le 5 \times 10^4,1 \le a_i \le N\)

差分之后直接用莫队维护即可。


具体就是按照前缀差分一下——真没什么好讲的。

实现的时候分别维护 \(l,r\) 的前缀中每一个数的出现次数即可。

#include <bits/stdc++.h>
using namespace std;
#define ll long long

const int N=2e5+5;
int n,m,cnt=0,cl[N],cr[N],l,r,a[N],B;
ll ans[N],res=0;
struct node{
  int l,r,id,op;
  bool operator <(const node &rhs) const{
  	if((l/B)!=(rhs.l/B)) return (l/B)<(rhs.l/B);
  	return r<rhs.r;
  }
}q[N];

void ml(int x){
  if(x==1) ++l,cl[a[l]]++,res+=cr[a[l]];
  else cl[a[l]]--,res-=cr[a[l]],--l;
}
void mr(int x){
  if(x==1) ++r,cr[a[r]]++,res+=cl[a[r]];
  else cr[a[r]]--,res-=cl[a[r]],--r;
}

int main(){
  /*2023.12.5 H_W_Y P5268 [SNOI2017] 一个简单的询问 md*/
  ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
  cin>>n;B=sqrt(n);
  for(int i=1;i<=n;i++) cin>>a[i];
  cin>>m;
  for(int i=1,l1,l2,r1,r2;i<=m;i++){
  	cin>>l1>>r1>>l2>>r2;
  	q[++cnt]=(node){r1,r2,i,1};
  	q[++cnt]=(node){r1,l2-1,i,-1};
  	q[++cnt]=(node){l1-1,r2,i,-1};
  	q[++cnt]=(node){l1-1,l2-1,i,1};
  }
  for(int i=1;i<=cnt;i++) if(q[i].l>q[i].r) swap(q[i].l,q[i].r);
  sort(q+1,q+cnt+1);l=r=0;
  for(int i=1;i<=cnt;i++){
  	while(r<q[i].r) mr(1);
  	while(r>q[i].r) mr(-1);
  	while(l<q[i].l) ml(1);
  	while(l>q[i].l) ml(-1);
  	ans[q[i].id]+=1ll*q[i].op*res;
  }
  for(int i=1;i<=m;i++) cout<<ans[i]<<'\n';
  return 0;
}

P4689 [Ynoi2016] 这是我自己的发明

P4689 [Ynoi2016] 这是我自己的发明

给一个树,\(n\) 个点,有点权,初始根是 \(1\)

\(m\) 个操作,每次操作:

  1. 将树根换为 \(x\)
  2. 给出两个点 \(x,y\),从 \(x\) 的子树中选每一个点,\(y\) 的子树中选每一个点,如果两个点点权相等,\(ans++\),求 \(ans\)

\(1 \le n \le 10^5,1 \le m \le 5 \times 10^5\)

首先换根是假的。

可以直接在 dfs 序上面变成区间查询或者区间的补集。


于是我们再对询问进行一个差分,和上面那道题类似去维护即可。

注意 dep 的减法不要写反了!

#include <bits/stdc++.h>
using namespace std;
#define ll long long 
#define pb push_back
#define pii pair<int,int>
#define fi first
#define se second

const int N=1e5+5,M=2e6+5;
int n,m,a[N],col[N],dfn[N],idx=0,sz[N],dep[N],f[N][19],cl[N],cr[N],l,r,B,cnt=0,tim=0,b[N],rt;
ll ans[M],cur;
struct node{
  int l,r,id,op;
  bool operator <(const node &rhs) const{
  	if((l/B)!=(rhs.l/B)) return (l/B)<(rhs.l/B);
  	return r<rhs.r;
  }
}q[M];
struct edge{int v,nxt;}e[N<<1];
int head[N],tot=0;

void add(int u,int v){
  e[++tot]=(edge){v,head[u]};head[u]=tot;
  e[++tot]=(edge){u,head[v]};head[v]=tot;
}

void dfs(int u,int fa){
  dep[u]=dep[fa]+1;dfn[u]=++idx;sz[u]=1;
  f[u][0]=fa;
  for(int i=1;f[f[u][i-1]][i-1];++i) f[u][i]=f[f[u][i-1]][i-1];
  for(int i=head[u];i;i=e[i].nxt){
  	int v=e[i].v;
  	if(v==fa) continue;
  	dfs(v,u);sz[u]+=sz[v];
  }
}

int getk(int x,int k){
  for(int i=17;i>=0;i--) if(k>=(1<<i)) k-=(1<<i),x=f[x][i];
  return x;
}

void ins(int l1,int r1,int l2,int r2,int t){
  q[++cnt]=(node){l1-1,l2-1,t,1};
  q[++cnt]=(node){r1,r2,t,1};
  q[++cnt]=(node){r1,l2-1,t,-1};
  q[++cnt]=(node){l1-1,r2,t,-1};
}

vector<pii> calc(int x){
  vector<pii> res;res.resize(0);
  if(x==rt){res.pb({1,n});return res;}
  if(dfn[x]<=dfn[rt]&&dfn[x]+sz[x]-1>=dfn[rt]){
  	int u=getk(rt,dep[rt]-dep[x]-1);
  	if(dfn[u]-1>=1) res.pb({1,dfn[u]-1});
  	if(dfn[u]+sz[u]<=n) res.pb({dfn[u]+sz[u],n});
  }
  else res.pb({dfn[x],dfn[x]+sz[x]-1});
  return res;
}

void ml(int x){
  if(x==1) ++l,++cl[col[l]],cur+=cr[col[l]];
  else --cl[col[l]],cur-=cr[col[l]],--l;
}

void mr(int x){
  if(x==1) ++r,++cr[col[r]],cur+=cl[col[r]];
  else --cr[col[r]],cur-=cl[col[r]],--r;
}

int main(){
  /*2023.12.6 H_W_Y P4689 [Ynoi2016] 这是我自己的发明 md*/
  ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
  cin>>n>>m;rt=1;
  for(int i=1;i<=n;i++) cin>>a[i],b[i]=a[i];
  sort(b+1,b+n+1);int len=unique(b+1,b+n+1)-b-1;
  for(int i=1,u,v;i<n;i++) cin>>u>>v,add(u,v);
  dfs(1,0);
  
  for(int i=1;i<=n;i++) col[dfn[i]]=lower_bound(b+1,b+len+1,a[i])-b;
  
  for(int i=1,op,x,y;i<=m;i++){
  	cin>>op;
  	if(op==1) cin>>rt;
  	else{
  	  cin>>x>>y;++tim;
  	  vector<pii> px,py;
  	  px=calc(x);py=calc(y);
  	  for(auto j:px) for(auto k:py) ins(j.fi,j.se,k.fi,k.se,tim);
  	}
  }
  
  B=sqrt(n);
  for(int i=1;i<=cnt;i++) if(q[i].l>q[i].r) swap(q[i].l,q[i].r);
  sort(q+1,q+cnt+1);l=r=1;cur=0;
  
  for(int i=1;i<=cnt;i++){
  	while(r<q[i].r) mr(1);
  	while(r>q[i].r) mr(-1);
  	while(l<q[i].l) ml(1);
  	while(l>q[i].l) ml(-1);
  	ans[q[i].id]+=1ll*q[i].op*cur;
  }
  for(int i=1;i<=tim;i++) cout<<ans[i]<<'\n';
  return 0;
}

P3245 [HNOI2016] 大数

P3245 [HNOI2016] 大数

给一个数字串以及一个质数 \(p\)

多次查询这个数字串的一个子串里有多少个子串是 \(p\) 的倍数。

一道让我印象非常深刻的题目。


还是先考虑差分,

但是发现前缀做差分是很难做的,因为不同的区间长度会带来乘上不同的 \(10^i\)

所以我们考虑对于后缀做差分。


一个区间 \([l,r]\) 满足条件当且仅当 \(suf[r+1] \equiv suf[l] \pmod p\)

于是我们直接维护就好了。

离散化之后就变成小 Z 的袜子了。


而要注意我们需要对 \(p=2,5\) 进行特判,而这也是好处理的,

直接判断奇偶或者 \(0,5\) 即可。

#include <bits/stdc++.h>
using namespace std;
#define ll long long

const int N=2e5+5;
int n,m,a[N],b[N],B,c[N],l,r;
ll p,ans[N],res=0,nw=1ll;
string s;
struct node{
  int l,r,id;
  bool operator <(const node &rhs) const{
  	if((l/B)!=(rhs.l/B)) return (l/B)<(rhs.l/B);
  	return r<rhs.r;
  }
}q[N];

void ins(int x){res+=c[x];++c[x];}
void del(int x){--c[x];res-=c[x];}

bool chk(ll p,char ch){
  if(p==2&&(ch-'0')%2==0) return true;
  if(p==5&&(ch-'0')%5==0) return true;
  return false;
}

void sol(){
  n=s.size();
  for(int i=0;i<n;i++) if(chk(p,s[i])) ++c[i+1],ans[i+1]=1ll*(i+1);
  for(int i=1;i<=n;i++) c[i]+=c[i-1],ans[i]+=ans[i-1];
  cin>>m;
  for(int i=1;i<=m;i++){
  	cin>>l>>r;
  	cout<<(ans[r]-ans[l-1]-1ll*(c[r]-c[l-1])*(l-1))<<'\n';
  }
}

int main(){
  /*2023.12.5 H_W_Y P3245 [HNOI2016] 大数 md*/
  ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
  cin>>p>>s;
  if(p==2||p==5) sol(),exit(0);
  
  n=s.size();B=sqrt(n);nw=1ll;
  for(int i=n-1;i>=0;i--) a[i+1]=(1ll*a[i+2]+1ll*nw*(s[i]-'0')%p)%p,nw=10ll*nw%p;
  
  ++n;
  for(int i=1;i<=n;i++) b[i]=a[i];
  sort(b+1,b+n+1);
  for(int i=1;i<=n;i++) a[i]=lower_bound(b+1,b+n+1,a[i])-b;
  
  cin>>m;
  for(int i=1;i<=m;i++) cin>>q[i].l>>q[i].r,++q[i].r,q[i].id=i;
  sort(q+1,q+m+1);l=1,r=0;
  for(int i=1;i<=m;i++){
  	while(r<q[i].r) ins(a[++r]);
  	while(l>q[i].l) ins(a[--l]);
  	while(r>q[i].r) del(a[r--]);
  	while(l<q[i].l) del(a[l++]);
  	ans[q[i].id]=res;
  }
  for(int i=1;i<=m;i++) cout<<ans[i]<<'\n';
  return 0;
}

P3604 美好的每一天

P3604 美好的每一天

给一个小写字母的字符串。

每次查询区间有多少子区间可以重排成为一个回文串。

同样发现一个区间满足条件当且仅当只有最多一种字符出现了奇数次,

于是我们不难想到可以异或完成。


把这 \(26\) 个字符分别变成 \(2^i\)

于是满足条件就变成了区间的异或和为 \(0,2^0,2^1,\dots,2^{25}\)


这样同样就变成了一个简单的问题,

每次加入一个元素的时候我们枚举一下 \(26\) 种合法的值就可以了。

#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define pb push_back

const int N=1e5+5;
int n,m,a[N],p[27],B,l,r,c[N],b[N],len;
ll ans[N],res=0;
map<int,int> mp;
vector<int> g[N];
char ch;
struct node{
  int l,r,id;
  bool operator <(const node &rhs) const{
  	if((l/B)!=(rhs.l/B)) return (l/B)<(rhs.l/B);
  	return r<rhs.r;
  }
}q[N];

void init(){p[0]=1;for(int i=1;i<26;i++) p[i]=2*p[i-1];p[26]=0;}

void ins(int x){for(auto i:g[x]) res+=1ll*c[i];++c[x];}
void del(int x){--c[x];for(auto i:g[x]) res-=1ll*c[i];}

int main(){
  /*2023.12.5 H_W_Y P3604 美好的每一天 md*/
  ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
  cin>>n>>m;init();B=sqrt(n);
  memset(c,0,sizeof(c));
  for(int i=1;i<=n;i++){
  	cin>>ch;
  	while(!(ch>='a'&&ch<='z')) cin>>ch;
  	a[i]=a[i-1]^p[ch-'a'];b[i]=a[i];
  }
  b[++n]=0;
  sort(b+1,b+n+1);len=unique(b+1,b+n+1)-b-1;
  for(int i=0;i<=n;i++) a[i]=lower_bound(b+1,b+len+1,a[i])-b;
  for(int i=1;i<=len;i++) mp[b[i]]=i;
  for(int i=1;i<=len;i++){
  	for(int j=0;j<=26;j++) if(mp.count(b[i]^p[j])) g[i].pb(mp[b[i]^p[j]]); 
  }
  
  for(int i=1;i<=m;i++) cin>>q[i].l>>q[i].r,--q[i].l,q[i].id=i;
  sort(q+1,q+m+1);l=0,r=-1;
  for(int i=1;i<=m;i++){
  	while(r<q[i].r) ins(a[++r]);
  	while(l>q[i].l) ins(a[--l]);
  	while(r>q[i].r) del(a[r--]);
  	while(l<q[i].l) del(a[l++]);
  	ans[q[i].id]=res;
  }
  for(int i=1;i<=m;i++) cout<<ans[i]<<'\n';
  return 0;
}

回滚莫队

也叫不删除莫队/不插入莫队。

即当插入/删除操作不好操作时我们采取的办法,时间复杂度不变。


P5906 【模板】回滚莫队&不删除莫队

P5906 【模板】回滚莫队&不删除莫队

给定一个序列,多次询问一段区间 \([l,r]\),求区间中相同的数的最远间隔距离

序列中两个元素的间隔距离指的是两个元素下标差的绝对值

\(1\leq n,m\leq 2\cdot 10^5\)\(1\leq a_i\leq 2\cdot 10^9\)

回滚莫队的板子。


发现删除操作是不好做的,

于是我们用栈维护一下,把删除改成撤销去维护即可。

#include <bits/stdc++.h>
using namespace std;

int read(){
  int x=0,f=1;char ch=getchar();
  while(!isdigit(ch)){if(ch=='-') f=-1;ch=getchar();}
  while(isdigit(ch)){x=(x<<1)+(x<<3)+ch-'0';ch=getchar();}
  return x*f;
}

void print(int x){
  int p[15],tmp=0;
  if(x==0) putchar('0');
  if(x<0) putchar('-'),x=-x;
  while(x) p[++tmp]=x%10,x/=10;
  for(int i=tmp;i>=1;--i) putchar(p[i]+'0');
  putchar('\n');
}

inline int max(const int &x,const int &y){return x>y?x:y;}

const int N=2e5+5;
int n,m,a[N],b[N],lst[N],ans[N],num=0,bl[N],B,len,st[N],del[N],ct=0;
struct node{
  int l,r,id;
  bool operator <(const node &rhs) const{
  	if(bl[l]!=bl[rhs.l]) return bl[l]<bl[rhs.l];
  	return r<rhs.r;
  }
}q[N];

int calc(int l,int r){
  int res=0;
  for(int i=l;i<=r;++i) (!lst[a[i]])?lst[a[i]]=i:res=max(res,i-lst[a[i]]);
  for(int i=l;i<=r;++i) lst[a[i]]=0;
  return res;
}

int main(){
  /*2023.12.5 H_W_Y P5906 【模板】回滚莫队&不删除莫队 md*/
  n=read();B=sqrt(n);
  for(int i=1;i<=n;++i) a[i]=read(),b[i]=a[i],bl[i]=(i-1)/B+1;
  sort(b+1,b+n+1);len=unique(b+1,b+n+1)-b-1;num=bl[n];
  for(int i=1;i<=n;++i) a[i]=lower_bound(b+1,b+len+1,a[i])-b;
  
  m=read();
  for(int i=1;i<=m;++i) q[i].l=read(),q[i].r=read(),q[i].id=i;
  sort(q+1,q+m+1);
  
  for(int i=1,j=1;j<=num;++j){
  	int br=min(n,j*B),l=br+1,r=br,res=0;ct=0;
  	
  	for(;i<=m&&bl[q[i].l]==j;++i){
  	  if(bl[q[i].r]==j){ans[q[i].id]=calc(q[i].l,q[i].r);continue;}
  	  
  	  while(r<q[i].r){
  	  	++r;lst[a[r]]=r;
  	  	if(!st[a[r]]) st[a[r]]=r,del[++ct]=a[r];
  	    res=max(res,r-st[a[r]]);
  	  }
  	  
  	  int cur=res;
  	  
  	  while(l>q[i].l){
  	  	--l;
  	  	if(!lst[a[l]]) lst[a[l]]=l;
  	  	res=max(res,lst[a[l]]-l);
  	  }
  	  
  	  ans[q[i].id]=res;
  	  
  	  while(l<=br){
  	  	if(lst[a[l]]==l) lst[a[l]]=0;
  	  	l++;
  	  }
  	  
  	  res=cur;
  	}
  	while(ct) lst[del[ct]]=st[del[ct]]=0,--ct;
  }
  for(int i=1;i<=m;i++) print(ans[i]);
  return 0;
}

\(num\) 赋值成 \(b[n]\),虚空调试半个小时。


BZOJ 4358 premu

BZOJ 4358 premu

给一个长为 \(n\) 的排列。

\(m\) 次查询,每次查询给出 \([l,r]\),输出最大的 \((j-i)\) 满足:

\(i,i+1,\dots, j\) 这些值都在区间 \([l,r]\) 中出现过。

\(1 \le n,q \le 2 \times 10^5\)

我们用 \(01\) 去维护值域,

\(1\) 表示出现过的,于是我们只需要维护每一个 \(1\) 的段的左端点和右端点,左端点指向右端点,右端点指向左端点,

加入一个节点时直接 \(\mathcal O(1)\) 修改,用链表维护序列即可。


而由于我们需要取最大值,是不支持删除的,

所以用回滚莫队完成即可。


想是好想的,但是发现实现好难?!

可是一看代码?!怎么这么短,可能是对回滚莫队有一些无解吧。

具体实现中,我们用 \(u,d\) 两个数组分别记录第 \(i\) 个点往左往右能走到的地方即可。

#include <bits/stdc++.h>
using namespace std;
#define pii pair<int,int>
#define fi first
#define se second

const int N=2e5+5;
int n,a[N],u[N],d[N],B,m,tp=0,ans[N],res=0,cur=0,bl[N];
struct node{
  int l,r,id;
  bool operator <(const node &rhs) const{
    if(bl[l]!=bl[rhs.l]) return bl[l]<bl[rhs.l];
    return r<rhs.r;
  }
}q[N];
pii s[N];

void ins(int x){
  u[x]=u[x+1]+1;d[x]=d[x-1]+1;
  int len=u[x]+d[x]-1;
  s[++tp]={x+u[x]-1,d[x+u[x]-1]};
  s[++tp]={x-d[x]+1,u[x-d[x]+1]};
  d[x+u[x]-1]=u[x-d[x]+1]=len;
  res=max(res,len);
}

int main(){
  /*2023.12.6 H_W_Y bzoj4358 permu md*/
  ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
  cin>>n>>m;B=sqrt(n);
  for(int i=1;i<=n;i++) cin>>a[i],bl[i]=(i-1)/B+1;
  for(int i=1;i<=m;i++) cin>>q[i].l>>q[i].r,q[i].id=i;
  sort(q+1,q+m+1);
  for(int i=1,r=0,br=0;i<=m;i++){
  	if(bl[q[i].l]!=bl[q[i-1].l]){
  	  for(int j=0;j<=n;j++) u[j]=d[j]=0;
  	  cur=res=0;r=br=B*bl[q[i].l];
  	}
  	res=tp=0;
  	while(r<q[i].r) ins(a[++r]);
  	tp=0;cur=res=max(cur,res);
  	for(int j=q[i].l;j<=min(q[i].r,br);j++) ins(a[j]);
  	ans[q[i].id]=res;
  	for(int j=tp;j>=1;j--) (j&1)?d[s[j].fi]=s[j].se:u[s[j].fi]=s[j].se;
  	for(int j=q[i].l;j<=min(q[i].r,br);j++) u[a[j]]=d[a[j]]=0;
  }
  for(int i=1;i<=m;i++) cout<<ans[i]<<'\n';
  return 0;
}

P5386 [Cnoi2019] 数字游戏

P5386 [Cnoi2019] 数字游戏

给定一个排列,多次询问,求一个区间 \([l,r]\) 有多少个子区间的值都在区间 \([x,y]\) 内。

\(1 \le n,q \le 2 \times 10^5\)

我们对值域莫队,而用上一题类似的方法即可得到一个 \(01\) 串,

于是我们需要维护的就是最长的全 \(1\) 段的平方和。


这个东西我们可以对它进行分块,直接维护即可。

没看题解代码打出来了(虽然时间有点长。)

#include <bits/stdc++.h>
using namespace std;
#define ll long long

const int N=5e5+5;
int n,m,pos[N],bl[N],B,Bq,L[N],R[N],num,tp,tpn=0,u[N],d[N];
ll ans[N];
struct query{
  int l,r,id,a,b;
  bool operator <(const query &rhs)const{
  	if(((l-1)/Bq)!=((rhs.l-1)/Bq)) return ((l-1)/Bq)<((rhs.l-1)/Bq);
  	return r<rhs.r; 
  }
}q[N];
struct node{ll ans;int l,r;bool vis;}t[N];
struct stk{int op,id,val;}s[N];
struct stn{int id;node val;}st[N];

ll sq(int x){return 1ll*x*(x+1)/2ll;}

node merge(node a,node b){
  node res;res.vis=false;
  if(a.vis&&b.vis) return (node){0,a.l+b.l,a.r+b.r,1};
  if(a.vis) res.l=a.l+b.l,res.r=b.r,res.ans=b.ans;
  else if(b.vis) res.l=a.l,res.r=b.r+a.r,res.ans=a.ans;
  else res.l=a.l,res.r=b.r,res.ans=a.ans+b.ans+sq(a.r+b.l);
  return res;
}

void ins(int x){
  int nw=bl[x],len=0;
  st[++tpn]=(stn){nw,t[nw]};
  
  if(x==L[nw]){
  	d[x]=1;u[x]=len=u[x+1]+1;
  	s[++tp]=(stk){1,u[x]+x-1,d[u[x]+x-1]};
  	
  	if(len==R[nw]-L[nw]+1) t[nw].l=t[nw].r=len,t[nw].vis=true,t[nw].ans=0;
  	else t[nw].ans-=sq(u[x+1]),t[nw].l=len;
  	
  	d[u[x]+x-1]=len;
  }
  else if(x==R[nw]){
  	d[x]=len=d[x-1]+1,u[x]=1;
  	s[++tp]=(stk){0,x-d[x]+1,u[x-d[x]+1]};
  	
  	if(len==R[nw]-L[nw]+1) t[nw].l=t[nw].r=len,t[nw].vis=true,t[nw].ans=0;
  	else t[nw].ans-=sq(d[x-1]),t[nw].r=len;
  	
  	u[x-d[x]+1]=len;
  }
  else{
  	d[x]=d[x-1]+1,u[x]=u[x+1]+1;len=u[x]+d[x]-1;
    s[++tp]=(stk){0,x-d[x]+1,u[x-d[x]+1]};
    s[++tp]=(stk){1,x+u[x]-1,d[x+u[x]-1]};
    if(len==R[nw]-L[nw]+1) t[nw].l=t[nw].r=len,t[nw].vis=true,t[nw].ans=0;
    else if(x-d[x]+1==L[nw]) t[nw].ans-=sq(u[x+1]),t[nw].l=len;
    else if(x+u[x]-1==R[nw]) t[nw].ans-=sq(d[x-1]),t[nw].r=len;
    else t[nw].ans+=sq(len)-sq(d[x-1])-sq(u[x+1]);
    
    d[x+u[x]-1]=u[x-d[x]+1]=len;
  }
}

node g(int l,int r){
  int lf=0,rf=0,lst=0;
  node res;res.ans=res.vis=0;
  for(int i=r;i>=l;i--) if(!u[i]){rf=i,res.r=r-i;break;}
  if(!rf) return (node){0,r-l+1,r-l+1,1};
  for(int i=l;i<=r;i++) if(!u[i]){lf=i,res.l=i-l;break;}
  
  for(int i=lf;i<=rf;i++){
  	if(u[i]&&!lst) lst=i;
  	if(!u[i]&&lst) res.ans+=sq(i-lst),lst=0;
  }
  return res;
}

ll qry(int l,int r){
  if(bl[l]==bl[r]){
  	node res=g(l,r);
  	if(res.vis) return sq(res.l);
  	return res.ans+sq(res.l)+sq(res.r);
  }
  node res=g(l,R[bl[l]]);
  for(int i=bl[l]+1;i<bl[r];i++) res=merge(res,t[i]);
  res=merge(res,g(L[bl[r]],r));
  if(res.vis) return sq(res.l);
  return res.ans+sq(res.l)+sq(res.r);
}

int find(int i){return (i-1)/Bq+1;}

int main(){
  /*2023.12.6 H_W_Y P5386 [Cnoi2019] 数字游戏 回滚莫队*/
  ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
  cin>>n>>m;Bq=(int)1.0*n/sqrt(m);B=sqrt(n);num=(n-1)/B+1;
  for(int i=1,x;i<=n;i++) cin>>x,pos[x]=i,bl[i]=(i-1)/B+1;
  for(int i=1;i<=num;i++) L[i]=R[i-1]+1,R[i]=min(L[i]+B-1,n);
  for(int i=1;i<=m;i++) cin>>q[i].a>>q[i].b>>q[i].l>>q[i].r,q[i].id=i;
  sort(q+1,q+m+1);
  
  for(int i=1,br=0,r=0;i<=m;i++){
	if(i==1||find(q[i].l)!=find(q[i-1].l)){
  	  for(int j=0;j<=n;j++) u[j]=d[j]=0;
  	  for(int j=0;j<=num;j++) t[j].l=t[j].r=t[j].ans=t[j].vis=0;
  	  r=br=find(q[i].l)*Bq;
  	}
  	tp=tpn=0;
  	while(r<q[i].r) ins(pos[++r]);
  	tp=tpn=0;
  	for(int j=q[i].l;j<=min(q[i].r,br);j++) ins(pos[j]);
	ans[q[i].id]=qry(q[i].a,q[i].b);
	for(int j=tp;j>=1;j--) (s[j].op)?d[s[j].id]=s[j].val:u[s[j].id]=s[j].val;
  	for(int j=tpn;j>=1;j--) t[st[j].id]=st[j].val; 
  	for(int j=q[i].l;j<=min(q[i].r,br);j++) u[pos[j]]=d[pos[j]]=0;
  }
  
  for(int i=1;i<=m;i++) cout<<ans[i]<<'\n';
  return 0;
}

P8078 [WC2022] 秃子酋长

P8078 [WC2022] 秃子酋长

给一个长为 \(n\) 的排列 \(a_1,\dots, a_n\),有 \(m\) 次询问,每次询问区间 \([l, r]\) 内,排序后相邻的数在原序列中的位置的差的绝对值之和。

有很多种做法,这里介绍一种回滚莫队的做法。


好像想到回滚莫队你就赢了。

考虑莫队,发现删除操作可以 \(O(1)\) 完成,而插入是不行的——

所以我们考虑 不插入莫队!!!

然后做完了。

#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define pb push_back

inline int read(){}
inline void prt(ll x){}

const int N=5e5+5;
int n,m,a[N],pos[N],L,R,B;
ll ans[N],res=0;
inline int Bl(int x){return x/B;}
struct node{
  int p,s;
  node(int P=0,int S=0){p=P,s=S;}
}c[N];
struct Query{
  int l,r,id;
  Query(int L=0,int R=0,int Id=0){l=L,r=R,id=Id;}
  bool operator <(const Query &a) const{return Bl(l)==Bl(a.l)?r>a.r:Bl(l)<Bl(a.l);}
}Q[N];

inline void del(int x){
  int nw=a[x],P=c[nw].p,S=c[nw].s;c[P].s=S,c[S].p=P;
  res+=((P&&S<=n)?abs(pos[P]-pos[S]):0)-(S<=n?abs(pos[S]-pos[nw]):0)-(P?abs(pos[nw]-pos[P]):0);
}

inline void ins(int x){
  int nw=a[x],P=c[nw].p,S=c[nw].s;c[P].s=c[S].p=nw;
  res+=(S<=n?abs(pos[nw]-pos[S]):0)+(P?abs(pos[nw]-pos[P]):0)-((P&&S<=n)?abs(pos[S]-pos[P]):0);
}

int main(){
  /*2024.3.5 H_W_Y P8078 [WC2022] 秃子酋长 回滚莫队*/
  n=read(),m=read();B=1145;
  for(int i=0;i<n;++i) a[i]=read(),pos[a[i]]=i;
  for(int i=0,l,r;i<m;++i) l=read(),r=read(),Q[i]=Query(l-1,r-1,i);
  sort(Q,Q+m);
  
  pos[0]=pos[1],pos[n+1]=pos[n];L=0,R=n-1;
  for(int i=1;i<=n;++i) c[i]=node(i-1,i+1),res+=abs(pos[i]-pos[i-1]);
  
  for(int i=0,bl=0;i<m;++i){
  	bl=Bl(Q[i].l);
  	if(bl!=Bl(Q[i-1].l)){
  	  while(R<n-1) ins(++R);
  	  while(L<bl*B) del(L++);
  	}
  	while(R>Q[i].r) del(R--);
  	while(L<Q[i].l) del(L++);
  	ans[Q[i].id]=res;
  	while(L>bl*B) ins(--L);
  }
  
  for(int i=0;i<m;++i) prt(ans[i]);
  return 0;
}  

莫队 + bitset

信息不一定合并只能 \(O(n)\),有的题的信息可以 \(O(\frac n w)\) 使用 bitset 加速合并。

这些题我们可以用莫队维护出区间的 bitset,然后把多个区间的 bitset 暴力合并起来。


P4688 [Ynoi2016] 掉进兔子洞

P4688 [Ynoi2016] 掉进兔子洞

\(m\) 个询问,每次询问三个区间 \([l1,r1],[l2,r2],[l3,r3]\)

\(f(x,l,r)\) 表示区间 \([l,r]\)\(x\) 的出现次数。

查询:\(\sum_{i=1}^n \min \{ f(i,l1,r1),f(i,l2,r2),f(i,l3,r3)\}\)

好玩捏。


想办法转换一下,先离散化,假设数 \(x\) 离散化后的位置是 \(y\),然后 \(x\) 出现 \(z\) 次,那么 \(y,y+1,\cdots,y+z-1\) 这些位置都是 \(x\) 的。

于是用莫队维护 bitset 时,如果 \(x\) 出现了 \(w\) 次,那么 \([y,y+w-1]\) 都是 \(1\),后面的 \([y+w,y+z-1]\) 都是 \(0\)

这样处理之后直接与起来就是答案了。


总时间复杂度 \(O(n \sqrt m + \frac{nm}{w})\)代码

#include <bits/stdc++.h>
using namespace std;

const int N=1e5+5,B=320,M=2e4;
int n,m,a[N],b[N],sz[N],len[N],cnt=0;
bitset<N> c[M+3],nw;
bool vis[N];

int bl(int x){return (x-1)/B+1;}

struct Nod{
  int l,r,id;
  Nod(int X=0,int Y=0,int Z=0){l=X,r=Y,id=Z;}
  bool operator <(const Nod &a) const{
    if(bl(l)!=bl(a.l)) return bl(l)<bl(a.l);
    return r<a.r;
  } 
}Q[N];

void ins(int x){nw[x+(sz[x]++)]=1;}
void del(int x){nw[x+(--sz[x])]=0;}

void sol(int q){
  cnt=0;
  for(int i=1,l1,r1,l2,r2,l3,r3;i<=q;i++){
    cin>>l1>>r1>>l2>>r2>>l3>>r3;
    vis[i]=0;
    Q[++cnt]=Nod(l1,r1,i);
    Q[++cnt]=Nod(l2,r2,i);
    Q[++cnt]=Nod(l3,r3,i);
    len[i]=r1+r2+r3-l1-l2-l3+3;
  }
  
  memset(sz,0,sizeof(sz));
  nw.reset();
  sort(Q+1,Q+cnt+1);
  int l=1,r=0;
  
  for(int i=1;i<=cnt;i++){
    while(r<Q[i].r) ins(a[++r]);
    while(l>Q[i].l) ins(a[--l]);
    while(r>Q[i].r) del(a[r--]);
    while(l<Q[i].l) del(a[l++]);
    if(!vis[Q[i].id]) c[Q[i].id]=nw,vis[Q[i].id]=1;
    else c[Q[i].id]&=nw;
  }
  
  for(int i=1;i<=q;i++) cout<<(len[i]-3*c[i].count())<<'\n';
}

int main(){
  /*2024.3.28 H_W_Y P4688 [Ynoi2016] 掉进兔子洞 莫队 + bitset*/ 
  ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
  cin>>n>>m;
  for(int i=1;i<=n;i++) cin>>a[i],b[i]=a[i];
  sort(b+1,b+n+1);
  for(int i=1;i<=n;i++) a[i]=lower_bound(b+1,b+n+1,a[i])-b;
  
  while(m){
    if(m>=M) sol(M),m-=M;
    else sol(m),m=0;
  }
  return 0;
}

P5355 [Ynoi2017] 由乃的玉米田

P5355 [Ynoi2017] 由乃的玉米田

给你一个序列 \(a\),长度为 \(n\),有 \(m\) 次操作,每次询问一个区间是否可以选出两个数它们的差为 \(x\),或者询问一个区间是否可以选出两个数它们的和为 \(x\),或者询问一个区间是否可以选出两个数它们的乘积为 \(x\) ,或者询问一个区间是否可以选出两个数它们的商为 \(x\)(没有余数) ,这四个操作分别为操作 \(1,2,3,4\)

选出的这两个数可以是同一个位置的数,值域 \(10^5\)

P3674 小清新人渣的本愿 的加强版呀。


考虑用莫队维护 bitset,用 \(\operatorname{bs1}\)\(\operatorname{bs2}\) 分别维护 \(x\)\(100000-x\) 是否出现过,

那么操作 \(1\) 的答案就是 (bs1&(bs1<<x)).any(),操作 \(2\) 的答案是 (bs1&(bs2>>(100000-x))).any()


操作 \(3\),我们可以直接枚举 \(\sqrt x\) 个因数,用 \(O(m \sqrt n)\) 的时间完成。

而对于操作 \(4\),考虑根号分治:

  1. 对于 \(x \gt \sqrt n\) 的,可能的答案最多 \(\sqrt n\) 组,于是暴力枚举即可。
  2. 对于 \(x \le \sqrt n\) 的,我们可以枚举每一个 \(x\),单独扫一次序列,每次维护对于每一个 \(r\),满足条件的最大的 \(l\) 即可,时间复杂度 \(O(n \sqrt n)\)

总时间复杂度 \(O(n \sqrt m + m \sqrt n + \frac{mn} w + n \sqrt n)\)代码


存在理论复杂度更有的做法,但是常熟较大且难以实现。

const int N=1e5+5,M=1e5,B=320;
int n,m,a[N],c[N];
bool ans[N];
bitset<N> bs1,bs2;
vector<int> G[B+10];

int bl(int x){return (x-1)/B+1;}

struct Qry{
  int op,l,r,x,id;
  bool operator <(const Qry &a) const{
    if(bl(l)==bl(a.l)) return r<a.r;
    return bl(l)<bl(a.l);
  }
}Q[N];

void ins(int x){
  if(!c[x]) bs1[x]=bs2[M-x]=1;
  ++c[x];
}

void del(int x){
  --c[x];
  if(!c[x]) bs1[x]=bs2[M-x]=0;
}

bool qry(int op,int x){
  if(op==1) return (bs1&(bs1<<x)).any();
  if(op==2) return (bs1&(bs2>>(M-x))).any();
  if(op==3){
  	for(int i=1;i*i<=x;i++){
  	  if(x%i) continue;
	  if(bs1[i]&&bs1[x/i]) return true;
    }
	return false;
  }
  for(int i=1;i*x<N;i++)
    if(bs1[i*x]&&bs1[i]) return true;
  return false;
}

void sol(int x){
  sort(G[x].begin(),G[x].end(),[&](int x,int y){return Q[x].r<Q[y].r;});
  int L=0;
  for(int i=1,j=0;i<=n;i++){
  	c[a[i]]=i;
  	if(1ll*a[i]*x<1ll*N&&c[a[i]*x]!=0) L=max(L,c[a[i]*x]);
  	if(a[i]%x==0&&c[a[i]/x]!=0) L=max(L,c[a[i]/x]);
  	while(j<(int)G[x].size()&&Q[G[x][j]].r==i) ans[G[x][j]]=(L>=Q[G[x][j]].l),++j;
  }
  
  for(int i=1;i<=n;i++) c[a[i]]=0;
}

int main(){
  #ifdef H_W_Y
  freopen("P3674_2.in","r",stdin);
  freopen("1.out","w",stdout);
  #endif
  
  /*2024.3.29 H_W_Y P5355 [Ynoi2017] 由乃的玉米田 莫队 + bitset*/ 
  ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
  cin>>n>>m;
  for(int i=1;i<=n;i++) cin>>a[i];
  for(int i=1;i<=m;i++){
    cin>>Q[i].op>>Q[i].l>>Q[i].r>>Q[i].x,Q[i].id=i;
    if(Q[i].op==4&&Q[i].x<=B) G[Q[i].x].pb(i),Q[i].op=5;
  }
  
  for(int i=1;i<=B;i++){
  	if(!(int)G[i].size()) continue;
  	sol(i);
  }
  
  sort(Q+1,Q+m+1);
  int l=1,r=0;
  bs1.reset(),bs2.reset();
  
  for(int i=1;i<=m;i++){
    if(Q[i].op==5) continue;
  	while(r<Q[i].r) ins(a[++r]);
  	while(l>Q[i].l) ins(a[--l]);
  	while(r>Q[i].r) del(a[r--]);
  	while(l<Q[i].l) del(a[l++]);
  	ans[Q[i].id]=qry(Q[i].op,Q[i].x);
  }
  
  for(int i=1;i<=m;i++){
  	if(ans[i]) cout<<"yuno\n";
  	else cout<<"yumi\n";
  }
  return 0;
} 

莫队二次离线


根号分治

一个通过对两部分分开讨论的很好的东西。

做题的时候容易发现,其实本质上就和什么修改同余或者维护质因数有关的题目一起出现。


基础例题

经典问题。


P5901 [IOI2009] Regions

P5901 [IOI2009] Regions

\(N\) 个节点的树,有 \(R\) 种属性,每个点属于一种属性。

\(Q\) 次询问,每次询问 \(r1,r2\),回答有多少对 \((e1,e2)\) 满足 \(e1\) 属性是 \(r1\)\(e2\) 属性是 \(r2\)\(e1\)\(e2\) 的祖先。

\(1 \le N,Q \le 2 \times 10^5\)

将询问离线下来。


直接对属性进行根号分治即可。

具体来说,对于个数 \(\ge \sqrt n\) 的属性我们提前预处理出来它与其他属性的答案。

而对于其他的,我们直接用双指针跑就可以了。

双指针可以优化掉一个 \(\log\)!!!


自己写的东西自己都看不懂了。


判断祖孙关系是可以通过 dfs 序所对应的子树区间 \(O(1)\) 完成的。

考虑对属性进行根号分治。

对于个数 \(\ge \sqrt n\) 的属性,最多只会有 \(\sqrt n\) 个,于是我们用 dfs 直接预处理出它与其他属性的答案。

而对于个数 \(\lt n\) 的属性,我们在询问时直接暴力枚举每一个元素,在另外的那一个属性中找到答案。

直接二分会带 \(\log\),而这个 \(\log\) 可以用双指针优化掉,所以总时间复杂度 \(\mathcal O(n \sqrt n)\)

远古代码,比较臭。

int n,m,q,a[N],sz[N],dfn[N],ed[N],lim,idx=0,s[N],head[N],tot=0;
vector<int> g[N],rev[N],lg,ans[2][N];
bool vis[N];
struct edge{
  int v,nxt;
}e[N<<1];

void add(int u,int v){e[++tot]=(edge){v,head[u]};head[u]=tot;}

void dfs(int u){
  dfn[u]=++idx;
  for(int i=head[u];i;i=e[i].nxt) dfs(e[i].v);
  ed[u]=idx;
}
void dfs1(int u){for(int i=head[u];i;i=e[i].nxt) dfs1(e[i].v),s[u]+=s[e[i].v];}
void dfs2(int u){for(int i=head[u];i;i=e[i].nxt) s[e[i].v]+=s[u],dfs2(e[i].v);}

bool cmp1(int x,int y){return dfn[x]<dfn[y];}
bool cmp2(int x,int y){return ed[x]<ed[y];}

void init(){for(int i=0;i<=n;i++) s[i]=0;}

int main(){
  /*2023.12.6 H_W_Y P5901 [IOI2009] Regions ghfz*/
  ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
  cin>>n>>m>>q>>a[1];lim=sqrt(n);sz[a[1]]++;g[a[1]].pb(1);
  
  for(int i=2,x;i<=n;i++) cin>>x>>a[i],add(x,i),g[a[i]].pb(i),sz[a[i]]++;
  //dfs(1);
  for(int i=1;i<=m;i++){
  	if(sz[i]>=lim) lg.pb(i),vis[i]=true;
  	sort(g[i].begin(),g[i].end(),cmp1),rev[i]=g[i],sort(rev[i].begin(),rev[i].end(),cmp2);
  }
  
  for(int i=0;i<(int)lg.size();i++){
  	int nw=lg[i];
  	ans[0][nw].resize(m+1);ans[1][nw].resize(m+1);
  	init();for(auto j:g[nw]) ++s[j];dfs1(1);
    for(int j=1;j<=n;j++) ans[0][nw][a[j]]+=s[j];
    init();for(auto j:g[nw]) ++s[j];dfs2(1);
    for(int j=1;j<=n;j++) ans[1][nw][a[j]]+=s[j];
  }
  
  while(q--){
  	int x,y;cin>>x>>y;
  	if(vis[x]) cout<<ans[1][x][y]<<endl;
  	else if(vis[y]) cout<<ans[0][y][x]<<endl;
  	else{
  	  int cur=sz[x]*sz[y];
  	  for(int i=0,j=-1;i<(int)g[x].size();i++){
  	  	while(j+1<(int)g[y].size()&&dfn[g[y][j+1]]<dfn[g[x][i]]) ++j;
  	  	cur-=j+1;
  	  }
  	  for(int i=(int)g[x].size()-1,j=(int)g[y].size();i>=0;i--){
  	  	while(j-1>=0&&dfn[g[y][j-1]]>ed[rev[x][i]]) --j;
  	  	cur-=(int)g[y].size()-j;
  	  }
  	  cout<<cur<<endl;
  	}
  }
  return 0;
}

P9809 [SHOI2006] 作业 Homework

P9809 [SHOI2006] 作业 Homework

1 X : 在集合 \(S\) 中加入一个X,保证 X 在当前集合中不存在。

2 Y : 在当前的集合中询问所有 \(X \mod Y\) 最小的值。

\(X,Y \le 10^5\)

很明显直接对 \(Y\) 根号分治即可。


而对于 \(Y\) 较大的,我们对值域分块,最多只会有 \(\sqrt n\) 个块,

具体实现时直接用 set 维护一下即可。

int n,a[N],x;
set<int> s;

int main(){
  /*2023.12.5 H_W_Y P9809 [SHOI2006] 作业 Homework 根号分治*/
  ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
  cin>>n;memset(a,0x3f,sizeof(a));
  for(int i=1;i<=n;i++){
  	char ch;cin>>ch;
  	while(ch!='A'&&ch!='B') cin>>ch;
  	cin>>x;
  	if(ch=='A'){s.insert(x);for(int j=1;j<549;j++) a[j]=min(a[j],x%j);}
    else{
      if(x<549) cout<<a[x]<<'\n';
      else{
      	int nw=x,ans;auto it=s.lower_bound(0);
      	ans=(*it)%x;
      	while((it=s.lower_bound(nw))!=s.end())
      	  ans=min(ans,(*it)%x),nw+=x;
      	cout<<ans<<'\n';
      }
    }
  }
  return 0;
}

P5397 [Ynoi2018] 天降之物

P5397 [Ynoi2018] 天降之物

给定长度为 \(n\) 的序列 \(a\),支持以下操作:

  1. 把所有 \(x\) 变成 \(y\)

  2. 查询最小的 \(|i-j|\) 使得 \(a_i =x ,a_j = y\)

有些人看了一个晚上看不懂,第二天早上发现自己看错题了。

于是有了新的 idea:如果每次只修改一个 \(x\) 怎么做?


由于这时属于根号分治的板块,所以这里没有给出需要卡常的序列分块做法。

我们记 \(x\) 的出现次数为 \(sz_x\),按照根号分治通常的思路,考虑对 \(sz_x\) 根号分治。

容易发现,我们存在一种方法使得每次的修改保证 \(sz_x \le sz_y\),即对两个数都进行一个映射,交换它们即可。

所以下面的讨论基于 \(sz_x \le sz_y\)


设根号分治的点为 \(lim\)

对于 \(sz _ x \gt lim\) 的数,我们称之为大数,可以预处理出它的答案,总个数不超过 \(\frac n {lim}\)

反之称之为小数,这是可以在每次查询的时候用双指针直接找出来的。

结合上面的两种情况,我们就有了不修改的做法。


现在来考虑有修改。

我们给每个点设置一个附属集合 \(G_i\),保证附属集合里面的数的个数 \(\le lim\),对于每一个数,我们记录它出现的位置。

而每当这个附属集合达到 \(lim\) 个数时,我们就暴力重构一次,即按照大数的做法预处理出每一次答案。

修改时,有以下几种情况:

  • \(sz_x \le lim,sz_y \le lim\)
    • \(sz_x + sz_y \le lim\),合并之后还是小数,直接暴力合并即可。
    • \(sz_x + sz_y \gt lim\),暴力处理所有答案,重构。
  • \(sz_x \le lim,sz_y \gt lim\)
    • \(sz_x + G_y.size() \le lim\),合并之后附属集合不用重构,所以也是暴力合并。
    • \(sz_x + G_y.size() \gt lim\),暴力重构 \(y\) 的答案,附属集合清空。
  • \(sz_x \gt lim,sz_y \gt lim\),直接暴力重构。

容易发现,因为每一次我们暴力重构都是在附属集合凑上 \(lim\) 的时候,所以最多只会重构 \(\frac n {lim}\) 次。

\(lim=\sqrt n\) 时,时间复杂度是 \(O(n \sqrt n)\)


而这样处理之后查询也是简单的。

只要加上两者附属集合之间的贡献就可以了,而由于附属集合的大小至多为 \(lim\),可以被看成小数,于是查询的复杂度也是 \(O(n \sqrt n)\) 的。

总时间复杂度 \(\mathcal O(n \sqrt n)\)代码


\(j\) 打成 \(i\),调了一个小时。乐。

int n,m,id[N],val[N],ans[M][N],idx=0,sz[N],lim,lst=0,a[N];
vector<int> G[N];

void build(int x){
  if(!id[x]) id[x]=++idx;
  memset(ans[id[x]],0x3f,sizeof(ans[id[x]]));
  int dis=inf;
  for(int i=1;i<=n;i++){
    if(a[i]==x) dis=0;
    else ++dis;
    ans[id[x]][a[i]]=min(ans[id[x]][a[i]],dis);
  }
  dis=inf;
  for(int i=n;i>=1;i--){
    if(a[i]==x) dis=0;
    else ++dis;
    ans[id[x]][a[i]]=min(ans[id[x]][a[i]],dis);
  }
  G[x].clear();
}

void init(){
  lim=sqrt(n);
  for(int i=1;i<=n;i++) val[i]=n+1;
  for(int i=1;i<=n;i++) ++sz[a[i]],val[a[i]]=a[i],G[a[i]].pb(i);
  for(int i=1;i<=n;i++) if(sz[i]>lim) build(i);
}

void merge(int x,int y){
  vector<int> res;res.clear();
  for(int i=0,j=0;i<(int)G[x].size()||j<(int)G[y].size();){
    if(j>=(int)G[y].size()||(i<(int)G[x].size()&&G[x][i]<G[y][j])) res.pb(G[x][i++]);
    else res.pb(G[y][j++]);
  }
  G[y]=res;
}

void updA(int x,int y){
  for(int i=0;i<(int)G[x].size();i++) a[G[x][i]]=y;
  for(int i=1;i<=idx;i++) ans[i][y]=min(ans[i][y],ans[i][x]);
  merge(x,y);
}

void updB(int x,int y){
  for(int i=1;i<=n;i++) if(a[i]==x) a[i]=y;
  build(y);
}

void upd(int x,int y){
  if(!sz[val[x]]||val[x]==val[y]) return ;
  int px=val[x],py=val[y];
  
  if(sz[val[x]]>sz[val[y]]) val[y]=val[x],swap(px,py);
  val[x]=n+1,x=px,y=py;
  
  if(x==n+1||y==n+1) return;
  
  if(sz[x]<=lim&&sz[y]<=lim){
    if(sz[x]+sz[y]<=lim) updA(x,y);
    else updB(x,y);
  }
  
  else if(sz[x]<=lim){
    if(sz[x]+(int)G[y].size()<=lim) updA(x,y);
    else updB(x,y);
  }
  
  else updB(x,y);

  sz[y]+=sz[x],sz[x]=0,G[x].clear();
}

int calc(int x,int y){
  int res=inf;
  if(!sz[x]||!sz[y]) return inf;
  
  for(int i=0,j=0;i<(int)G[x].size()&&j<(int)G[y].size();){
    if(G[x][i]<G[y][j]) res=min(res,G[y][j]-G[x][i]),++i;
    else res=min(res,G[x][i]-G[y][j]),++j;
  }
  return res;
}

int qry(int x,int y){
  x=val[x],y=val[y];
  if(x==n+1||y==n+1||!sz[x]||!sz[y]) return -1;
  if(sz[x]>sz[y]) swap(x,y);
  
  if(sz[x]<=lim&&sz[y]<=lim) return calc(x,y);
  if(sz[x]<=lim) return min(ans[id[y]][x],calc(x,y));
  return min(min(ans[id[x]][y],ans[id[y]][x]),calc(x,y));
}

int main(){
  /*2024.3.25 H_W_Y P5397 [Ynoi2018] 天降之物 根号分治*/ 
  #ifdef H_W_Y
  freopen("1.in","r",stdin);
  freopen("1.out","w",stdout);
  #endif
  ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
  cin>>n>>m;
  for(int i=1;i<=n;i++) cin>>a[i];
  //init();
  while(m--){
    int op,x,y;cin>>op>>x>>y;
    x^=lst,y^=lst;
    if(op==1) upd(x,y);
    else{
      int res=qry(x,y);
      if(res==-1) lst=0,cout<<"Ikaros\n";
      else cout<<(lst=res)<<'\n';
    }
  }
  return 0;
}

P5309 [Ynoi2011] 初始化

P5309 [Ynoi2011] 初始化

Mayuri 有 \(n\) 颗星星,每颗星星都有一个明亮度 \(A_{i}\) 。Mayuri 时常想知道一个区间 \([l,r]\) 内所有星星的明亮度的总和是多少。但是星星是会眨眼的,所以星星的明亮度是会变化的。有的时候,下标为 \(y,y+x,y+2x,y+3x,\ldots,y+kx\) 的星星的明亮度会增加 \(z\)。保证 \(y\leq x\)

Mayuri 不怎么会数学,请回答她的询问。答案要对 \(10^{9}+7\) 取模。

用到了一些根号平衡的思想。


看到同余的东西,考虑对 \(x\) 进行根号分治,对于 \(x \gt \sqrt n\) 的情况,我们直接暴力做即可。

而对于 \(x \le \sqrt n\) 的情况,我们用数组一个二维数组维护 \(t[x][y]\) 表示模 \(x\)\(y\) 所加的东西。


由于是区间查询,我们考虑维护 \(O(1) - O(\sqrt n)\) 的分块维护 \(a\) 数组,

而对于 \(x \le \sqrt n\) 的修改的和是可以枚举每一个 \(x\),通过前缀和 \(O(1)\) 计算的,而这里的前缀和是可以在修改时处理好的。

所以总时间复杂度 \(\mathcal O(n \sqrt n)\)代码

const int B=1000,B1=200;
int n,m,bl[N],L[N],R[N];
ll t[B+3][B+3],a[N],s[N];

ll bf(int l,int r){
  ll res=0;
  for(int i=l;i<=r;i++) res+=a[i];
  return res;
}

ll qry(int l,int r){
  if(bl[l]==bl[r]) return bf(l,r);
  else{
    ll res=bf(l,R[bl[l]])+bf(L[bl[r]],r);
    for(int i=bl[l]+1;i<bl[r];i++) res+=s[i];
    return res;
  }
}

int main(){
  /*2024.3.25 H_W_Y P5309 [Ynoi2011] 初始化 根号分治 + 分块*/ 
  ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
  cin>>n>>m;
  for(int i=1;i<=n;i++) cin>>a[i],bl[i]=(i-1)/B+1;
  for(int i=1;i<=bl[n];i++){
    L[i]=R[i-1]+1,R[i]=min(R[i-1]+B,n);
    for(int j=L[i];j<=R[i];j++) s[i]+=a[j];
  }
  
  while(m--){
    int op,x,y,z;
    cin>>op>>x>>y;
    if(op==1){
      cin>>z;y%=x;
      if(x>B1) for(int i=y;i<=n;i+=x) a[i]+=z,s[bl[i]]+=z;
      else for(int i=y+1;i<=x;i++) t[x][i]+=z;
    }else{
      ll res=qry(x,y);
      for(int i=1;i<=B1;i++)
        res+=t[i][y%i+1]-t[i][x%i]+1ll*t[i][i]*(y/i-x/i);
      cout<<res%H<<'\n';
    }
  }
  return 0;
}

P7710 [Ynoi2077] stdmxeypz

P7710 [Ynoi2077] stdmxeypz

给你一棵边权为 \(1\),且以 \(1\) 为根的有根树,每个点有初始为 \(0\) 的点权值,定义两个点的距离为其在树上构成的简单路径的长度,需要支持两种操作:

1 a x y z:把 \(a\) 子树中所有与 \(a\) 的距离模 \(x\) 等于 \(y\) 的节点权值加 \(z\)

2 a:查询 \(a\) 节点的权值。

首先我们把树用 dfn 序拍到序列上面,那么就变成了一个区间的同余的修改。

这就和上一道题非常像了。


我们对 \(x\) 进行根号分治,同时对序列分块,

\(x \le \sqrt n\) 时,我们用一个 \(B \times B \times B\) 的数组维护每一个块模 \(x\)\(y\) 所操作的权值,

而对于 \(x \gt \sqrt n\),直接暴力维护即可。


查询时枚举以下那个模数即可。

总时间复杂度 \(\mathcal O(n \sqrt n)\)


const int B=1500,B1=200;
int n,s[N],t[N/B+3][B1+3][B1+3],m,bl[N],L[N],R[N],dfn[N],idx=0,sz[N],dep[N],t1[N][N/B+3];
vector<int> G[N];

void dfs(int u,int fa){
  dfn[u]=++idx,dep[idx]=dep[dfn[fa]]+1,sz[u]=1;
  for(auto v:G[u]) if(v!=fa) dfs(v,u),sz[u]+=sz[v];
}

void bf(int l,int r,int x,int y,int z){
  for(int i=l;i<=r;i++) if(dep[i]%x==y) s[i]+=z;
}

void upd(int l,int r,int x,int y,int z){
  y%=x;
  if(bl[l]==bl[r]) bf(l,r,x,y,z);
  else{
    bf(l,R[bl[l]],x,y,z);
    bf(L[bl[r]],r,x,y,z);
    if(x<=B1) for(int i=bl[l]+1;i<bl[r];i++) t[i][x][y]+=z;
    else{
      for(int i=y;i<=n;i+=x)
        t1[i][bl[l]+1]+=z,t1[i][bl[r]]-=z;
    }
  }
}

int qry(int x){
  int res=s[x];
  for(int i=1;i<=bl[x];i++) res+=t1[dep[x]][i];
  for(int i=1;i<=B1;i++) res+=t[bl[x]][i][dep[x]%i];
  return res;
}

int main(){
  /*2024.3.25 H_W_Y P7710 [Ynoi2077] stdmxeypz 根号分治 + 分块*/ 
  
  #ifdef H_W_Y
  freopen("1.in","r",stdin);
  freopen("1.out","w",stdout);
  #endif
  
  ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
  cin>>n>>m;
  for(int i=2,x;i<=n;i++) cin>>x,G[x].pb(i);
  for(int i=1;i<=n;i++) bl[i]=(i-1)/B+1;
  for(int i=1;i<=bl[n];i++) L[i]=R[i-1]+1,R[i]=min(R[i-1]+B,n);
  //dfs(1,0);
  
  while(m--){
    int op,x,y,z,a;
    cin>>op;
    if(op==1) cin>>a>>x>>y>>z,upd(dfn[a],dfn[a]+sz[a]-1,x,y+dep[dfn[a]],z);
    else cin>>a,cout<<qry(dfn[a])<<'\n';
  }
  return 0;
}

质因数相关

有时可以和莫队一起解决一些区间子区间的问题。


P5071 [Ynoi2015] 此时此刻的光辉

P5071 [Ynoi2015] 此时此刻的光辉

珂朵莉给你了一个长为 \(n\) 的序列,有 \(m\) 次查询,每次查询一段区间的乘积的约数个数 \(\mod 19260817\) 的值。

\(1 \le n,m \le 10^5,v \le 10^9\)

很容易发现我们只需要把每一个数进行质因数分解,

因数个数就是 \((c_1+1)(c_2+1) \dots\)(小学奥数知识)。


这样直接用分块维护,复杂度时带 \(\log\) 的,

在这道题中根本过不了。

所以我们需要考虑一种新的方法。


发现对于每一个数 \(\ge 1000\) 的质数只有可以有两个,

于是我们对于 \(\lt 1000\)\(168\) 个质数进行暴力的维护,而只对那两个大质数在莫队时维护即可。


具体实现的时候就需要先把 \(\lt 1000\) 的质数做一个前缀和即可。

#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define pb push_back

const ll mod=19260817;
const int N=1e5+5,M=1e6+5;
int n,m,x,s[N][169],c[M],cnt=0,p[M],bl[N],B,l,r,len=0;
vector<int> g[N];
ll inv[M],ans[M],res=0;
struct node{
  int l,r,id;
  bool operator <(const node &rhs) const{
  	if(bl[l]!=bl[rhs.l]) return bl[l]<bl[rhs.l];
  	return r<rhs.r;
  }
}q[M];
bool vis[M];

void init(){
  inv[1]=1;
  for(int i=2;i<M;i++) inv[i]=inv[mod%i]*(mod-mod/(1ll*i))%mod;
  for(int i=2;i<M;i++){
  	if(!vis[i]) p[++cnt]=i;
  	for(int j=1;j<=cnt&&p[j]*i<M;j++){
  	  vis[p[j]*i]=true;
  	  if(i%p[j]==0) break;
  	}
  }
}

void chg(int i,int v){
  for(auto j:g[i]){
  	res=res*inv[c[j]+1]%mod;
  	c[j]+=v;
  	res=1ll*res*(c[j]+1ll)%mod;
  }
}

int main(){
  /*2023.12.5 H_W_Y P5071 [Ynoi2015] 此时此刻的光辉 md*/
  ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
  init();
  cin>>n>>m;B=(int)floor(sqrt(n));
  for(int i=1;i<=n;i++){
  	cin>>x;bl[i]=(i-1)/B+1;
  	
  	for(int j=1;j<=168;j++)
  	  while(x%p[j]==0) s[i][j]++,x/=p[j];
  	  
  	for(int j=169;j<=cnt&&p[j]*p[j]<=x;j++)
  	  if(x%p[j]==0){
  	  	g[i].pb(p[j]),x/=p[j];
  	  	c[++len]=p[j];
  	  	break;
  	  }
  	if(x!=1) g[i].pb(x),c[++len]=x;
  }

  sort(c+1,c+len+1);
  len=unique(c+1,c+len+1)-c-1;
  
  for(int i=1;i<=n;i++){
    for(int j=0;j<(int)g[i].size();j++)
      g[i][j]=lower_bound(c+1,c+len+1,g[i][j])-c;
    for(int j=1;j<=168;j++)
      s[i][j]+=s[i-1][j];
  }
  
  memset(c,0,sizeof(c));
  
  for(int i=1;i<=m;i++){
  	cin>>q[i].l>>q[i].r;
  	ans[i]=1ll;q[i].id=i;
  	for(int j=1;j<=168;j++)
  	  ans[i]=1ll*ans[i]*(s[q[i].r][j]-s[q[i].l-1][j]+1ll)%mod;
  }
  sort(q+1,q+m+1);
  
  l=1;r=0;res=1ll;
  
  for(int i=1;i<=m;i++){
  	while(r<q[i].r) chg(++r,1);
  	while(l>q[i].l) chg(--l,1);
  	while(r>q[i].r) chg(r--,-1);
  	while(l<q[i].l) chg(l++,-1);
  	(ans[q[i].id]*=res)%=mod;
  }
  
  for(int i=1;i<=m;i++) cout<<ans[i]<<'\n';
  return 0;
}

P9060 [Ynoi2002] Goedel Machine

P9060 [Ynoi2002] Goedel Machine

给定一个长度为 \(n\) 的序列 \(a_1\cdots a_n\)。你需要回答 \(m\) 个询问,第 \(i\) 个询问给定一个区间 \([l_i,r_i]\),请你求出这个区间中所有非空子集的最大公约数的乘积。

由于答案可能很大,每次询问请你求出其对 \(998244353\) 取模的结果。

\(1 \le n,m,a_i \le 10^5\)

居然是 2023.8.5 的联考题,不愧是 CQYC 的。

当时记得在机场看的这道题,才办完托运,感觉真的根本不可做——那一场都不可做吧。

后面到了 London 看了后面一道题的题解就没有管这道题了。


看到区间子区间就不知道怎么下手了。

感觉可以用单调栈,对于每一个质数拆开算贡献,然后再用一个扫描线维护一下,

也就是扫描线的区间子区间问题,

而对于大的质数我们直接暴力?

可能还真可以,只不过常数比较大,可能写着也比较复杂。


考虑正常的做法。

首先按每一个质数分别算贡献是没有问题的,

那么如果这个质数在 \(x\) 个数中都出现了,那么它的贡献是 \(p^{2^x-1}\),这是好理解的。


但是现在发现了一个严重的问题——\(p\) 还有很多的幂,这是不好统计的。

于是我们考虑根号分治,按照 \(\sqrt w\) 分成两部分。


对于 \(\le \sqrt w\) 的部分,有大概 \(\frac{\sqrt n}{\log n}\) 个质数,于是加上它们的幂就有 \(\sqrt n\) 个数。

于是我们对这 \(\sqrt n\) 个数分别算出在每一个数中的出现次数,加上它们的贡献即可,这是可以提前预处理得到的。


对于 \(\gt \sqrt w\) 的部分,每一个数只会出现一次,就是说幂是 \(1\) 的。

所以我们直接用莫队维护一下出现次数即可。


代码中用到了算 \(2^k-1\) 的技巧,就每一次加上 \(2^i\) 就可以了。

注意空间问题,有可能会越界(\(t\) 数组),所以循环的时候只能枚举到 \(\lt s[i]\)

#include <bits/stdc++.h>
using namespace std;
#define ll long long

const ll mod=998244353;
const int N=1e5+5;
int lim,n,m,a[N],p[N],cnt=0,s[N],mx,B,len=0,c[N],t[N],l,r,pos[N];
ll ans[N],res_pos,res_inv;
bool vis[N];
struct node{
  int l,r,id;
  bool operator <(const node &rhs) const{
  	if((l/B)!=(rhs.l/B)) return (l/B)<(rhs.l/B);
  	return r<rhs.r;
  }
}q[N];

ll qpow(ll a,ll b){
  ll res=1ll;
  while(b){if(b&1) res=res*a%mod;a=a*a%mod;b>>=1;}
  return res;
}

void init(int lim){
  for(int i=2;i<=lim;++i){
    if(!vis[i]) p[++cnt]=i;
    for(int j=1;j<=cnt&&p[j]*i<=lim;++j){
      vis[p[j]*i]=true;
      if(i%p[j]==0) break;
    }
  }
}

void ins(int x){if(x>1) res_pos=1ll*res_pos*t[pos[x]+(c[x]++)]%mod;}
void del(int x){if(x>1) res_inv=1ll*res_inv*t[pos[x]+(--c[x])]%mod;}

int main(){
  /*2023.12.5 H_W_Y P9060 [Ynoi2002] Goedel Machine md*/
  ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
  cin>>n>>m;B=n/sqrt(m);
  
  for(int i=1;i<=n;++i) cin>>a[i],mx=max(mx,a[i]);
  init((lim=sqrt(mx)));
  
  for(int i=1;i<=m;i++) cin>>q[i].l>>q[i].r,q[i].id=i,ans[i]=1ll;
  
  for(int k=1;k<=cnt;k++){
  	int nw=1ll*p[k],inv=(int)qpow(nw,mod-2);t[0]=nw;
  	for(int i=1;i<=n;++i) t[i]=1ll*t[i-1]*t[i-1]%mod;
  	for(int j=nw;j<=mx;j*=nw){
  	  for(int i=1;i<=n;i++) s[i]=s[i-1]+(a[i]%j==0);
  	  for(int i=1;i<=m;i++) ans[i]=1ll*ans[i]*t[s[q[i].r]-s[q[i].l-1]]%mod*inv%mod;
  	}
  	for(int i=1;i<=n;i++) while(a[i]%nw==0) a[i]/=nw;
  }
  
  memset(s,0,sizeof(s));cnt=0;
  for(int i=1;i<=n;i++) if(a[i]>1) s[a[i]]++;
  
  for(int i=1;i<=mx;i++) if(s[i]){
  	t[pos[i]=++cnt]=i;
  	for(int j=1;j<s[i];j++) ++cnt,t[cnt]=1ll*t[cnt-1]*t[cnt-1]%mod;
  }
  
  memset(c,0,sizeof(c));
  
  sort(q+1,q+m+1);l=1,r=0;res_pos=res_inv=1ll;
  
  for(int i=1;i<=m;i++){
  	while(r<q[i].r) ins(a[++r]);
  	while(l>q[i].l) ins(a[--l]);
  	while(r>q[i].r) del(a[r--]);
  	while(l<q[i].l) del(a[l++]);
  	ans[q[i].id]=1ll*ans[q[i].id]*res_pos%mod*qpow(res_inv,mod-2)%mod;
  }
  
  for(int i=1;i<=m;i++) cout<<ans[i]<<'\n';
  return 0;
}

根号重构

这是 PPT 上面没有的问题,但是个人认为还是非常有意思。


Conclusion

一些补题时得到的东西。

  1. 对于区间子区间的复杂标记下传问题一定要想好顺序和具体合并时的方法,我们把它变成一个标记组一个标记组去合并。
  2. 区间子区间问题很多只是争对最小值的累加,所以处理的时候直接维护最小值的累加次数,下传时注意。
  3. 分块 可以对值域和下标进行同时的处理,将每一个块排序即可。(P3863 序列)
  4. 树剖跳 \(k\) 级祖先最后在一条重链上的情况不要暴力跳,直接返回 \(rev[dfn[u]-k]\) 即可!!!。(P6779 rla1rmdq)
  5. 分块的 \(L[i],R[i]\) 一定要在做所有操作之前单独处理出来。

后记

数据结构是一个很庞大的东西,由于 PPT 上面的篇幅有限,就根号数据结构的所有知识点也没有办法全部讲完,例如树分块之类的东西,大家可以自行了解。

由于笔者水平非常菜,所以这里就写这么多了,若有问题,欢迎大家提出。

posted @ 2024-03-22 09:45  H_W_Y  阅读(126)  评论(2编辑  收藏  举报