操作分块
操作分块:有若干修改,询问组成的操作序列,考虑把这个操作序列分块,然后维护块内和块间的贡献,并根据实际情况平衡两种维护方式的复杂度。
当遇到大量操作并且难以用普通数据结构维护的时候,可以考虑操作分块,并进行如下思考:
\(1\):当仅有\(\sqrt n\)次操作时,如何在\(O(\sqrt n^2)\)内维护。
\(2\):如何在单块\(O(\sqrt n)内维护前面\)O(n)$长度对于当前块的贡献。
\(3\):是否存在根号分治可以平衡的暴力做法。
要注意往往操作分块并不是正解,并且其复杂度较劣,运用时慎重考虑。
通过几道例题深入了解:
\(P5443\)
https://www.luogu.com.cn/problem/P5443
题解:先考虑暴力做法。
暴力\(1\):对每个修改直接暴力修改,对每个询问,将所有权值大于等于\(w\)的边加入并查集,最后回答\(s\)所在集合大小即可。
修改\(O(1)\),查询\(O(m)\)。
暴力\(2\):考虑对所有边,所有的询问从大到小排序。
对于边,分为带修边和不带修边,对于不带修边直接随着询问动态加入并查集即可。
而对于有修改的边,每次询问时,暴力枚举每条边对应的修改操作,选择时间小于等于当前询问时间里,时间最大的一条边。若这条边重量大于等于询问重量,则将这条边加入到可撤销并查集中,每次询问完,撤销本次加入的带修边。
发现对于每次询问,暴力\(1\)枚举了所有边,而暴力\(2\)枚举了所有操作,考虑平衡。
将\(S\)个操作放在一个块里,对于处理一个块,先将所有边和块内询问按重量排序,然后根据是否存在块内的修改操作分为带修边和不带修边。对于带修边和不带修边的处理,都和暴力\(2\)中相同。处理完一个块后,对块内的修改统一直接修改即可。
注意根据操作复杂度平衡块长,本题取\(\sqrt{mlogn}\)较为优秀。
#include <bits/stdc++.h>
using namespace std;
typedef pair<int,int> PII;
const int N=1e5+10;
int n,m,k;
int ans[N];
vector<PII> q[N];
struct Edge{
int x,y,z;
bool operator <(Edge t){
return z<t.z;
}
}e[N];
struct Operator{
int tp,x,y;
}op[N];
struct Change{
int tm,x,y;
bool operator <(Change t){
return y<t.y;
}
};
struct DisjointSetUnion{
int p[N],sz[N];
int top;
PII stk[N];
void init(){
top=0;
for(int i=1; i<=n; i++) p[i]=i;
for(int i=1; i<=n; i++) sz[i]=1;
}
int find(int x){
if(x==p[x]) return x;
return find(p[x]);
}
void merge(int x,int y){
x=find(x),y=find(y);
if(x==y) return;
if(sz[x]>sz[y]) swap(x,y);
p[x]=y,sz[y]+=sz[x];
stk[++top]={x,y};
}
void del(){
PII t=stk[top--];
int x=t.first,y=t.second;
sz[y]-=sz[x],p[x]=x;
}
}dsu;
int main(){
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
cin >> n >> m;
for(int i=1; i<=m; i++){
int x,y,z;
cin >> x >> y >> z;
e[i]={x,y,z};
}
cin >> k;
for(int i=1; i<=k; i++){
int tp,x,y;
cin >> tp >> x >> y;
op[i]={tp,x,y};
}
int len=sqrt(k*log2(n));
for(int L=1; L<=k; L+=len){
int R=min(k,L+len-1);
for(int i=1; i<=m; i++) q[i].clear();
vector<Change> task;
vector<Edge> e1;
vector<int> e2;
for(int i=L; i<=R; i++)
if(op[i].tp==1) q[op[i].x].push_back({i,op[i].y});
else task.push_back({i,op[i].x,op[i].y});
for(int i=1; i<=m; i++)
if(!q[i].size()) e1.push_back({e[i].x,e[i].y,e[i].z});
else e2.push_back(i);
sort(e1.begin(),e1.end());
reverse(e1.begin(),e1.end());
sort(task.begin(),task.end());
reverse(task.begin(),task.end());
dsu.init();
for(int i=0,j=0; i<task.size(); i++){
while(j<e1.size()&&e1[j].z>=task[i].y)
dsu.merge(e1[j].x,e1[j].y),j++;
int tmp=dsu.top;
for(int x=0; x<e2.size(); x++){
int id=e2[x],l=0,r=q[id].size()-1;
while(l<r){
int mid=(l+r+1)/2;
if(q[id][mid].first<=task[i].tm) l=mid;
else r=mid-1;
}
if(q[id][l].first>task[i].tm&&e[id].z>=task[i].y)
dsu.merge(e[id].x,e[id].y);
if(q[id][l].first<=task[i].tm&&q[id][l].second>=task[i].y)
dsu.merge(e[id].x,e[id].y);
}
ans[task[i].tm]=dsu.sz[dsu.find(task[i].x)];
while(dsu.top>tmp) dsu.del();
}
for(int i=L; i<=R; i++)
if(op[i].tp==1) e[op[i].x].z=op[i].y;
}
for(int i=1; i<=k; i++)
if(op[i].tp==2) cout << ans[i] << endl;
return 0;
}
\(P3247\)
https://www.luogu.com.cn/problem/P3247
题解:对于每个询问,将所有满足\(x\leq a,y\leq b\),的边\((u,v,x,y)\)加入到并查集中,若\(u,v\)处于一个联通块,且该联通块两维度的最大值分别为\(a,b\),则询问正确。
考虑优化,将边按第一维度从小到大排序,并将询问放入块中。
考虑先前块对当前块的影响,只需要将先前块内边,和当前块内询问都按第二维度排序,针对第二维度双指针即可。
考虑当前块对当前块的影响,直接暴力判断边是否满足条件并加入即可,再撤销。
#include <bits/stdc++.h>
using namespace std;
const int N=2e5+10;
int n,m,k;
bool ans[N];
struct Edge{
int x,y;
int a,b;
}e[N];
bool cmp1(Edge t1,Edge t2){
return t1.a<t2.a;
}
bool cmp2(Edge t1,Edge t2){
return t1.b<t2.b;
}
struct Query{
int id;
int x,y;
int a,b;
bool operator <(Query t){
return b<t.b;
}
};
vector<Query> q[N];
struct DisjointSetUnion{
int p[N],sz[N];
int mx_a[N],mx_b[N];
int top;
Edge stk[N];
void init(){
top=0;
for(int i=1; i<=n; i++) p[i]=i;
for(int i=1; i<=n; i++) sz[i]=1;
memset(mx_a,-1,sizeof mx_a);
memset(mx_b,-1,sizeof mx_b);
}
int find(int x){
if(x==p[x]) return x;
return find(p[x]);
}
void merge(int x,int y,int a,int b){
x=find(x),y=find(y);
if(x==y){
stk[++top]={x,y,mx_a[x],mx_b[x]};
mx_a[x]=max(mx_a[x],a);
mx_b[x]=max(mx_b[x],b);
return;
}
if(sz[x]>sz[y]) swap(x,y);
p[x]=y,sz[y]+=sz[x];
stk[++top]={x,y,mx_a[y],mx_b[y]};
mx_a[y]=max(mx_a[y],max(mx_a[x],a));
mx_b[y]=max(mx_b[y],max(mx_b[x],b));
}
void del(){
Edge t=stk[top--];
int x=t.x,y=t.y,t_a=t.a,t_b=t.b;
mx_a[y]=t_a,mx_b[y]=t_b;
if(x!=y) sz[y]-=sz[x],p[x]=x;
}
}dsu;
int main(){
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
cin >> n >> m;
for(int i=1; i<=m; i++){
int x,y,a,b;
cin >> x >> y >> a >> b;
e[i]={x,y,a,b};
}
sort(e+1,e+m+1,cmp1);
int len=sqrt(m),cnt=(m+len-1)/len+1;
cin >> k;
for(int i=1; i<=k; i++){
int u,v,a,b;
cin >> u >> v >> a >> b;
int l=0,r=cnt-1;
while(l<r){
int mid=(l+r+1)/2;
if(e[min(mid*len,m)].a<=a) l=mid;
else r=mid-1;
}
q[l+1].push_back({i,u,v,a,b});
}
for(int i=1; i<=cnt; i++){
int R=min((i-1)*len,m);
sort(e+1,e+R+1,cmp2);
sort(q[i].begin(),q[i].end());
dsu.init();
for(int j=0,now=1; j<q[i].size(); j++){
Query t=q[i][j];
while(now<=R&&e[now].b<=t.b)
dsu.merge(e[now].x,e[now].y,e[now].a,e[now].b),now++;
int tmp=dsu.top;
for(int x=(i-1)*len+1; x<=i*len; x++){
if(x>m) break;
if(e[x].a<=t.a&&e[x].b<=t.b)
dsu.merge(e[x].x,e[x].y,e[x].a,e[x].b);
}
bool flag=1;
int x=dsu.find(t.x),y=dsu.find(t.y);
if(x!=y) flag=0;
if(dsu.mx_a[x]!=t.a||dsu.mx_b[x]!=t.b) flag=0;
ans[t.id]=flag;
while(dsu.top>tmp) dsu.del();
}
}
for(int i=1; i<=k; i++)
if(ans[i]) cout << "Yes" << endl;
else cout << "No" << endl;
return 0;
}