分块-优雅的暴力

发布时间 2023-07-28 12:08:05作者: exut

前言

某人:线段树好难,学不会,树状数组感觉用途少好多,怎么办啊
Ben:入我分块神教!

ps:作者不认为分块是数据结构,而是一种思想。本文代码来自作者不同时期,马蜂习惯存在差别

前置芝士:循环,数组,没了

一 序列分块

对于给定序列要求增删改查类问题,一般最常用线段树和BIT,毕竟是高贵的log

但是假如对复杂度没有那么高要求,存不存在一种更好理解、更不容易写错的东西来代替呢?

现在介绍--分块

我们可以把一个数列划分成若干个等长的块

设块长为 \(B\),则我们可以整体化处理一些信息

以区间加法、求和为例(假设 \(B=2\) )

考虑对于数列\(a={1,1,4,5,1,4,1}\)

可以划分为\(a={1,1|4,5|1,4|1}\)

最后落单了个1怎么办?不影响不用管它

修改大概可以分为三个部分:

中间整块、两边不完整部分

考虑整块处理,预处理时处理好每个下标所在的块(块的左右端点可记可不记)和块内初始和。发现我们可以参考线段树,我们在块上打个标记记录增加量

整块最多有\(\frac{n}{B}\)量级个,复杂度\(O(\frac{n}{B})\)

散块呢?我们可以暴力处理(毕竟只有两个),\(O(B)\)

查询同理,对于每个整块将答案加上(初始和+长度\(\times\)标记),散块暴力累加(记得加上标记)

那么 \(B\)应该取多少呢?

我们肯定希望两种操作操作和尽可能小

由均值不等式,\(B+\frac{n}{B}>=2\sqrt n\),当左边两项相等时,即\(B=\sqrt n\)时最小。对于两个操作复杂度可以用\(B\)表示时,当两式相等\(B\)最佳,不一定是\(\sqrt n\)(不过你一般写\(\sqrt n\)人家也不会卡除了ynoi)

例题:线段树1
讲解见上,代码参考:

#include<bits/stdc++.h>
using namespace std;
inline long long read(){
	int x=0,f=1;char ch=getchar();
	while (ch<'0'||ch>'9'){if (ch=='-') f=-1;ch=getchar();}
	while (ch>='0'&&ch<='9'){x=x*10+ch-48;ch=getchar();}
	return x*f;
}
long long k[1114514],a[1114514],mx[1114514],mark[114514];
long long n,m;
int main(){
	n=read();m=read();
	int bl=sqrt(n);
	for(int i=1;i<=n;i++){
		cin>>a[i];
		k[i]=(i-1)/bl+1;
		mx[k[i]]+=a[i];
	}
	int la=k[n];
	long long l,r,z,kk;
	while(m--){
		z=read();l=read(),r=read();
		long long ans;
		ans=0;
		if(z==1){
			kk=read();
			if(k[l]==k[r] or k[l]+1==k[r]){
				for(int i=l;i<=r;i++){
					a[i]+=kk,mx[k[i]]+=kk;
				}
			}
			else{
				int ll=k[l],rr=k[r];
				for(int i=l;k[i]==ll;i++){
					a[i]+=kk,mx[k[i]]+=kk;
				}
				for(int i=r;k[i]==rr;i--){
					a[i]+=kk,mx[k[i]]+=kk;
				}
				for(int i=ll+1;i<rr;i++){
					mark[i]+=kk; 
				}
			}
		}
		else{
			if(k[l]==k[r] or k[l]+1==k[r]){
				for(int i=l;i<=r;i++){
					ans+=a[i]+mark[k[i]];
				}
			}
			else{
				int ll=k[l],rr=k[r];
				for(int i=l;k[i]==ll;i++){
					ans+=a[i]+mark[k[i]];
				}
				for(int i=r;k[i]==rr;i--){
					ans+=a[i]+mark[k[i]];
				}
				for(int i=ll+1;i<rr;i++){
					ans+=mx[i]+bl*mark[i];
				}	
			}
			cout<<ans<<"\n";
		}
	}
	return 0;
}

例二:开关
即区间取反,同样打标记,设块内开\(W\)个灯,整块取反时区间开灯数变成 \(len-W\),散块直接暴力取反记录
code:

#include<iostream>
#include<cstdio>
#include<cmath>

using namespace std;

inline int read(){register int x = 0, f = 1;register char c = getchar();while (c < '0' || c > '9'){if (c == '-') f = -1;c = getchar();}while (c >= '0' && c <= '9'){x = (x << 1) + (x << 3) + (c ^ 48);c = getchar();}return x * f;}
inline void write(int x){if (x < 0) putchar('-'), x = -x;if (x > 9) write(x / 10);putchar(x % 10 + '0');}

const int N = 1e5 + 10;
int n, m;
int block, b[N];
int a[N], sum[N], tag[N];

void init(){
	for (int i = 1; i <= n; i++){
		b[i] = (i - 1) / block + 1;
	}
}

void modify(int l,int r){
	for(int i=l;i<=min(b[l]*block,r);i++) a[i]^=1,sum[b[i]]+=(a[i]^tag[b[i]]?1:-1);
	if(b[l]!=b[r])	
		for(int i=(b[r]-1)*block+1;i<=r;i++) 
			a[i]^=1,sum[b[i]]+=(a[i]^tag[b[i]]?1:-1);
	for(int i=b[l]+1;i<=b[r]-1;i++){
		tag[i]^=1;
		sum[i]=block-sum[i];
	} 
}

int query(int l, int r){
	int ans = 0;
	for(int i=l;i<=min(b[l]*block,r);i++) ans+=(a[i]^tag[b[l]]);
	if(b[l]!=b[r])
		for(int i=(b[r]-1)*block+1;i<=r;i++) 
			ans+=(a[i]^tag[b[r]]);
	for(int i=b[l]+1;i<=b[r]-1;i++) ans+=sum[i];
	return ans;
}

int main(){
	n = read(), m = read();
	block = sqrt(n);
	init();
	for (int i = 1; i <= m; i++){
		int opt = read(), l = read(), r = read();
		if (opt == 0){
			modify(l, r);
		}else{
			printf("%d\n", query(l, r));
		}
	}
	return 0;
}

其他例题:LOJ数列分块1~9,题解参考hzwer大佬官方,但注意三中官方题解代码存在锅,用set如果块内存在多个元素会误杀,建议和二一样写

前面太简单了对吧,来上强度(选看)
由乃打扑克(ynoi)
求一个数据结构,可以区间加和区间kth

看完上面数列分块第三题再来这里

考虑查询(操作姑且放一边)

考虑二分一个值,显然这个数的排名满足单调性,可二分

how to check?显然可以用分块

操作直接暴力操作后重构即可

好水啊!这也叫上强度?顶多有点难写

啊确实,这么写完你应该TLE

优化1:二分不需要从\(INTMAXMIN\)开始,从区间最大值最小值开始即可

优化2:一个整块若最大值都小于mid可以直接把排名+块长,最小值都大于可以
跳过
优化3:玄学块长,我是200左右比较优

加这三就可以过

优化四:对于两边散块修改后可以和原块归并做到严格的修改

其余优化见tj

code:(优化123)

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=1e5+5;
//#define int long long
inline int qread() {
	register char c = getchar();
	register int x = 0, f = 1;
	while (c < '0' || c > '9') {
		if (c == '-') f = -1;
		c = getchar();
	}
	while (c >= '0' && c <= '9') {
		x = (x << 3) + (x << 1) + c - 48;
		c = getchar();
	}
	return x * f;
}
int n,m,k[1005],bk,cg=1;
ll a[N],ad[10005],sum[10005],mx[10005],mi[10005];
vector<ll> B[20005];
int bl(int x){return (x-1)/bk+1;}
void remake(int k){
    while(!B[k].empty())B[k].pop_back();
    mx[k]=INT_MIN;
    mi[k]=INT_MAX;
    for(int i=(k-1)*bk+1;i<=min(n,k*(bk));i++) a[i]+=ad[k],B[k].push_back(a[i]),mx[k]=max(mx[k],a[i]),mi[k]=min(a[i],mi[k]);
    sort(B[k].begin(),B[k].end());ad[k]=0;
}
void add(int l,int r,ll k){
    for(int i=l;i<=min(bl(l)*bk,r);i++){a[i]+=k,sum[bl(i)]+=k;}
    remake(bl(l));
    if(bl(l)!=bl(r)){
        for(int i=r;i>bk*(bl(r)-1);i--)
            a[i]+=k,sum[bl(i)]+=k;
        remake(bl(r));
    }
    for(int i=bl(l)+1;i<bl(r);i++) ad[i]+=k;
}
ll query(int l,int r,ll c){
    ll ans=0;
    for(int i=l;i<=min(bl(l)*bk,r);i++){ans+=(a[i]+ad[bl(i)]<c);}
    if(bl(l)!=bl(r)){
        for(int i=r;i>bk*(bl(r)-1);i--)
            ans+=(a[i]+ad[bl(i)]<c);
    }
    for(int i=bl(l)+1;i<bl(r);i++){
        int x=c-ad[i];
        if(mx[i]<x){ans+=bk;continue;}
        if(mi[i]>x){continue;}
        ans+=lower_bound(B[i].begin(),B[i].end(),x)-B[i].begin();
    }
    return ans;
}

ll ask(int l,int r,ll k){
	if(r-l+1<k or k<1) return -1;
	ll L=-2e4*cg-5000,R=-L;
	ll ans=-1;
	while(L<=R){
		ll mid=(L+R)>>1;
		int yzy=query(l,r,mid);
		if(yzy<k){
			L=mid+1;
		}
		else R=mid-1,ans=mid;
	}
	return ans-1;
}

signed main(){
//	freopen("data.in","r",stdin);
//	freopen("ans.txt","w",stdout);
    n=qread(),m=qread();
    bk=65;
    for(int i=1;i<=n;i++) a[i]=qread();
    for(int i=1;i<=bl(n);i++) remake(i);
	cg=1;
    while(m--){
        int f,l,r;ll k;
        f=qread(),l=qread(),r=qread(),k=qread();
        if(f==2) add(l,r,k),cg++;
        else printf("%lld\n",ask(l,r,k));
    }
}

非常好题目,恨来自中国