Ynoi 大分块学习笔记
最初分块
看题解了,迷迷糊糊的,目前不会实现。
第二分块
做这个题还是比较有感触的,这是不是我第一道黑题来着,当时还是抄了天波老师的卡常题解过的。
Descr
给定长为 \(n\) 的数列 \(a\),支持以下两种操作:
- 将区间 \([l,r]\) 内大于 \(x\) 的数减去 \(x\)。
- 查询区间 \([l,r]\) 内有多少个 \(x\)。
Sol
首先肯定是要用分块的。。。先给它序列分块了再说。
我们先讨论整块修改,这通常是比较困难的。
首先我们注意到一个不难注意到的事实,不同数的个数只会减少不会增多。所以我们做修改操作的时候可以使用只支持快速合并而不支持分裂的数据结构,比如说并查集。
其次我们注意到最大值一定是不增的,这启发我们把最大值当作一种势能来处理。如果我们直接模拟修改操作那么会被一直减一卡爆,那么我们可以考虑把小于等于 \(x\) 的数都加上 \(x\) 再打上整体减 tag。
接下来是本题的精华,考虑被减一卡爆的本质是什么。本质是我们对于一个值操作了很多次减一操作,而没有很好利用最大值不增这一性质。也就是说,我们期望对每个值操作完减 \(x\) 后,这些值永远都不会再出现,否则会重复造成贡献。
考虑当前最大值 \(mx\) 与操作的 \(x\) 之间的关系,我们要规定一个阈值来界定是暴力减还是先加再整体减。到了这一步,很多题解直接讨论 \(2x\) 和 \(mx\) 的关系,但是并没有说明白我们到底为何选择 \(2x\) 当做阈值。
暴力减操作是遍历值域 \((x,mx]\) 的数并让他们往下合并。因为要保证对每个值只操作一次,所以有 \(mx-x< x\),否则再操作一次 \(x\) 会让 \(mx-x\) 再减去一次 \(x\)。
先加再减是遍历值域 \([1,x]\) 的数并让他们往上合并,同理需要保证 \(x+x< mx\),否则加上的数超过了 \(mx\) 会产生新的数。
发现正好是 \(2x>mx\) 是暴力减,\(2x<m\) 时先减后加。
我们再来讨论散块修改,肯定是考虑直接重构的。当时我们已经用并查集把值相同的位置缩到一块了,我们只需要查询一遍所有的位置再应用上整体减 tag 即可。注意路径压缩并查集全部查一遍的复杂度是 \(O(n)\) 的。
综上所述,我们在 \(O((q+V)\sqrt n)\) 的时间内解决了上述问题。
关于空间怎么卡,我们直接离线操作然后对每个块做一次所有询问把答案累加上即可。
Code
说一些实现上的问题。
rt[x] 记录所有值为 \(x\) 的位置的一个代表,siz[x] 记录多少个数值为 \(x\),fa[i] 记录的是位置 \(i\) 的 father,val[i] 记录的是位置 \(i\) 的值。
举个例子我们最后散块暴力重构时要查询位置 \(i\) 的值可以写 val[find(i)]。
写 Ynoi 题还想用 cin 和 #define int long long?
#include <bits/stdc++.h>
using namespace std;
//#define int long long
typedef pair<int,int> pii;
#define fi first
#define se second
#define mp make_pair
#define pb push_back
const int N=1e6+10,T=1e3+10,M=5e5+10,V=1e5+10,INF=0x3f3f3f3f,mod=1e4+7;
int n,m,a[N];
int op[M],l[M],r[M],x[N];
struct Block{
int rt[N],fa[N],val[N],siz[N];
int mx,tm,l,r;
int find(int x){return x==fa[x]?x:fa[x]=find(fa[x]);}
void merge(int x,int y){
if(rt[y]){
fa[rt[x]]=rt[y];
}else{
rt[y]=rt[x];
val[rt[y]]=y;
}
siz[y]+=siz[x];
rt[x]=siz[x]=0;
}
void build(){
mx=0,tm=0;
for(int i=l;i<=r;i++){
if(rt[a[i]])fa[i]=rt[a[i]];
else rt[a[i]]=i,fa[i]=i,val[i]=a[i];
siz[a[i]]++;
mx=max(mx,a[i]);
}
}
void destroy(){
for(int i=l;i<=r;i++){
a[i]=val[find(i)];
rt[a[i]]=siz[a[i]]=0;
a[i]-=tm;
}
for(int i=l;i<=r;i++)fa[i]=0;
tm=0;
}
void updateAll(int x){
if(2*x<=mx-tm){
for(int i=tm+1;i<=tm+x;i++)
if(rt[i])merge(i,i+x);
tm+=x;
}else{
for(int i=mx;i>tm+x;i--)
if(rt[i])merge(i,i-x);
mx=min(mx,tm+x);
}
}
void update(int l,int r,int x){
if(r<l)return;
destroy();
for(int i=l;i<=r;i++)
if(a[i]>x)a[i]-=x;
build();
}
int queryAll(int x){
return x+tm>=N?0:siz[x+tm];
}
int query(int l,int r,int x){
if(r<l)return 0;
int ans=0;
for(int i=l;i<=r;i++)
if(val[find(i)]-tm==x)ans++;
return ans;
}
}b;
int ans[N];
int rd(){
char ch=getchar();
while(!isdigit(ch))ch=getchar();
int x=0;
while(isdigit(ch))x=(x<<1)+(x<<3)+(ch^'0'),ch=getchar();
return x;
}
signed main(){
ios::sync_with_stdio(0);
cin.tie(0),cout.tie(0);
n=rd(),m=rd();
for(int i=1;i<=n;i++)a[i]=rd();
for(int i=1;i<=m;i++)op[i]=rd(),l[i]=rd(),r[i]=rd(),x[i]=rd();
int t=1000;
for(int L=1,R=t;L<=n;L=R+1,R=min(L+t-1,n)){
b.l=L,b.r=R;
b.build();
for(int j=1;j<=m;j++){
if(op[j]==1){
if(l[j]<=L&&R<=r[j])b.updateAll(x[j]);
else b.update(max(L,l[j]),min(R,r[j]),x[j]);
}
if(op[j]==2){
if(l[j]<=L&&R<=r[j])ans[j]+=b.queryAll(x[j]);
else ans[j]+=b.query(max(L,l[j]),min(R,r[j]),x[j]);
}
}
b.destroy();
}
for(int i=1;i<=m;i++){
if(op[i]==2)cout<<ans[i]<<'\n';
}
return 0;
}
第四分块
有点想法,先把想法写在这。
首先肯定考虑序列分块,每个块内记录每个 \(x\) 第一次出现的位置和最后一次出现的位置,以及块内任何两对 \((x,y)\) 之间的最小距离。注意到一个块内不同元素数量是 \(O(\sqrt n)\) 的,所以构建上面的那个块需要先离散化,然后就可以 \(O(n)\) 建。
然后这个修改操作比较典,讨论一下 \(x\) 修改到的 \(y\) 存不存在。如果不存在,直接改标记是 \(O(1)\) 的。如果存在直接暴力 \(O(\sqrt n)\) 合并,散块直接半暴力重构。注意到这样做是对的。定义块内的势能为其不同元素的个数,初始时所有块的势能都是 \(O(\sqrt n)\),一共有 \(O(\sqrt n)\) 个块,所以这个 DS 的总势能是 \(O(n)\) 的。每次修改操作会使左右 \(O(1)\) 个散块的势能增加 \(O(1)\),所以最终势能最大才只有 \(O(n+q)\),然而我们只需要 \(O(\sqrt n)\) 的时间就能将势能减小 \(1\),所以暴力做这个是 \(O((n+q)\sqrt n)\) 的。
然后你发现就做完了,具体实现细节有点像最初分块,维护一个并查集,重构的时候查询,均摊 \(O(1)\) 的。
我擦,我咋会的???写写试试(不过听说要卡常。。。)
哦原来修改和查询都是全局的,那这应该也能做吧(
19:00 开写。
19:34 写完了,开始调。
19:47 过阳历了,4pts。
20:21 MLE。
23:04 24pts。26pts。32pts。34pts。
10:56 模拟赛弃赛来卡常。36pts。40pts。56pts。
@Kketchup 帮我卡常,58pts。
@arrow_king 帮我卡常,60pts。

浙公网安备 33010602011771号