http://hzwer.com/8053.html 先上大神博客。
https://loj.ac/problems/tag/207 题目链接
分块,是一种能过题的暴力,一般复杂度为n^(1/2),可以处理很多区间的问题的数据结构,因此很有必要去学习分块这里有入门级别的九道题,我觉得做题要自己去摸索,而不是一味得去读别人的题解,否则到时候就算做过,也有可能wa成狗...
1.给出一个长为n的数列,以及n个操作,操作涉及区间加法,单点查值。
分块的基本操作,将元素分为m块,每一块有n/m个元素,如果涉及的区间有不完整块的,那么直接对不完整块进行暴力遍历,对于完整块,用一个标记去维护,例如这里区间加法,利用一个atag去标记这个块增加了多少,初始化操作为将每一个元素进行块的分类blo[i]=(i-1)/m+1,blo[i]意味着i属于哪一块,单点查询直接v[i]+atag[blo[i]]就好了
#include <iostream>
#include <cstdio>
#include <cstring>
#include <string>
#include <queue>
#include <algorithm>
#include <vector>
#include <cmath>
#include <set>
#define INF 99999999
typedef long long ll;
using namespace std;
int v[50999];//原数值
int blo[50999];//块的数组
int atag[50999];//每一个块的标记数组
int n,m;
void add(int a,int b,int c)
{
int i;
for(i=a;i<=min(b,blo[a]*m);i++)
{
v[i]+=c;
}
if(blo[a]!=blo[b])
{
for(i=(blo[b]-1)*m+1;i<=b;i++)
{
v[i]+=c;
}
}
for(i=blo[a]+1;i<=blo[b]-1;i++)
{
atag[i]+=c;
}
}
int main()
{
// freopen("date.txt","r",stdin);
// freopen("ans.txt","w",stdout);
int i;
scanf("%d",&n);
m=sqrt(n);
for(i=1;i<=n;i++)
{
scanf("%d",&v[i]);
blo[i]=(i-1)/m+1;
}
for(i=1;i<=n;i++)
{
int op,a,b,c;
scanf("%d%d%d%d",&op,&a,&b,&c);
if(op==0) add(a,b,c);
else printf("%d\n",v[b]+atag[blo[b]]);
}
}
2.给出一个长为n的数列,以及n个操作,操作涉及区间加法,询问区间内小于某个值x的元素个数。
区间加法和上题一样,这里求小于某个x的值,所以要对元素进行排序,所以要用到容器,这里使用vector,下面的一道题使用set,其实都一样,而且如果参入了set,那么对于增加删除操作很更加方便,把每一块的元素加入相应的vector中,那么最后一定要对所有的vector进行一次sort排序,接着是暴力修改,暴力修改完后需要对该块的所有元素重新放入,重新排序,所以利用reset()(这里要注意!!!!你传入函数的参数到底是什么!!!!),那么如果对于整个块修改呢,那么就维护atag就好了,接着查询的时候,不完整的块就直接暴力,完整的块就先把要查询的数c-atag[i],然后进行lower_bound(c)(容器的二分函数,lower_bound()返回第一个大于等于c的元素,upper_bound()返回第一个大于c的元素),作累加即可(其实这道题用multiset更合适,这是后面想到的)
#include <iostream>
#include <cstdio>
#include <cstring>
#include <string>
#include <queue>
#include <algorithm>
#include <vector>
#include <map>
#include <cmath>
#define INF 99999999
typedef long long ll;
using namespace std;
int v[50999];//原数值
int blo[50999];//块的数组
int atag[50999];//每一个块的标记数组
int n,m;
vector<int >ve[509];
void reset(int x)
{
int i;
ve[x].clear();
for(i=(x-1)*m+1;i<=min(x*m,n);i++)
{
ve[x].push_back(v[i]);
}
sort(ve[x].begin(),ve[x].end());
}
void add(int a,int b,int c)
{
int i;
for(i=a;i<=min(blo[a]*m,b);i++)
{
v[i]+=c;
}
reset(blo[a]);
if(blo[a]!=blo[b])
{
for(i=(blo[b]-1)*m+1;i<=b;i++)
{
v[i]+=c;
}
reset(blo[b]);
}
for(i=blo[a]+1;i<=blo[b]-1;i++)
{
atag[i]+=c;
}
}
int query(int a,int b,int c)
{
int i,ans=0;
for(i=a;i<=min(blo[a]*m,b);i++)
{
if(v[i]+atag[blo[a]]<c) ans++;
}
if(blo[a]!=blo[b])
{
for(i=(blo[b]-1)*m+1;i<=b;i++)
{
if(v[i]+atag[blo[b]]<c) ans++;
}
}
for(i=blo[a]+1;i<=blo[b]-1;i++)
{
int x=c-atag[i];
ans+=lower_bound(ve[i].begin(),ve[i].end(),x)-ve[i].begin();
}
return ans;
}
int main()
{
int i;
scanf("%d",&n);
m=sqrt(n);
for(i=1;i<=n;i++)
{
scanf("%d",&v[i]);
blo[i]=(i-1)/m+1;
ve[blo[i]].push_back(v[i]);
}
for(i=1;i<=blo[n];i++)//第n块!!!!
{
sort(ve[i].begin(),ve[i].end());
}
for(i=1;i<=n;i++)
{
int op,a,b,c;
scanf("%d%d%d%d",&op,&a,&b,&c);
if(op==0) add(a,b,c);
else printf("%d\n",query(a,b,c*c));
}
}
给出一个长为n的数列,以及n个操作,操作涉及区间加法,询问区间内小于某个值x的前驱(比其小的最大元素)。
这题目和题2差不多,不过这里用了set,要注意的是大神博客里面是之间分了1000个块,我这里是分类sqrt(n)个块,所以我用了reset(),还要注意的是!!!!!!边界条件,边界条件,边界条件!!!!例如说,set在lower_bound()中如果第一个元素就大于等于c了怎么办?所以it=s[i].begin(),那么就不能进行it--了!!!所以要特判一下。还要注意对块进行查找的时候,要减去atag[i] !!
#include <iostream>
#include <cstdio>
#include <cstring>
#include <string>
#include <queue>
#include <algorithm>
#include <vector>
#include <cmath>
#include <set>
#define INF 99999999
typedef long long ll;
using namespace std;
int v[100999];//原数值
int blo[100999];//块的数组
int atag[100999];//每一个块的标记数组
int n,m;
set<int>s[1009];
void reset(int x)
{
int i;
s[x].clear();
for(i=(x-1)*m+1;i<=x*m;i++)
{
s[x].insert(v[i]);
}
}
void add(int a,int b,int c)
{
int i;
for(i=a;i<=min(b,blo[a]*m);i++)
{
v[i]+=c;
}
reset(blo[a]);
if(blo[a]!=blo[b])
{
for(i=(blo[b]-1)*m+1;i<=b;i++)
{
v[i]+=c;
}
reset(blo[b]);
}
for(i=blo[a]+1;i<=blo[b]-1;i++)
{
atag[i]+=c;
}
}
int query(int a,int b,int c)
{
int ans=-1,i;
for(i=a;i<=min(b,blo[a]*m);i++)
{
if(v[i]+atag[blo[a]]<c) ans=max(ans,v[i]+atag[blo[a]]);
}
if(blo[a]!=blo[b])
{
for(i=(blo[b]-1)*m+1;i<=b;i++)
{
if(v[i]+atag[blo[b]]<c) ans=max(ans,v[i]+atag[blo[b]]);
}
}
for(i=blo[a]+1;i<=blo[b]-1;i++)
{
int x=c-atag[i];
set<int >::iterator it;
it=s[i].lower_bound(x);
if(it==s[i].begin()) continue;
it--;
ans=max(ans,(*it)+atag[i]);
}
return ans;
}
int main()
{
int i;
scanf("%d",&n);
m=sqrt(n);
for(i=1;i<=n;i++)
{
scanf("%d",&v[i]);
blo[i]=(i-1)/m+1;
s[blo[i]].insert(v[i]);
}
for(i=1;i<=n;i++)
{
int op,a,b,c;
scanf("%d%d%d%d",&op,&a,&b,&c);
if(op==0) add(a,b,c);
else printf("%d\n",query(a,b,c));
}
}
4.给出一个长为n的数列,以及n个操作,操作涉及区间加法,区间求和。
数据结构经典操作,树状数组,线段树都可以做,这里用分块,和问题1一样,加一个sum数组,注意溢出问题就好了
#include <iostream>
#include <cstdio>
#include <cstring>
#include <string>
#include <queue>
#include <algorithm>
#include <vector>
#include <cmath>
#include <set>
#define INF 99999999
typedef long long ll;
using namespace std;
ll v[50999];//原数值
int blo[50999];//块的数组
ll atag[50999];//每一个块的标记数组
int n,m;
set<int>s[1009];
ll sum[50009];
void add(int a, int b,int c)
{
int i;
for(i=a;i<=min(b,blo[a]*m);i++)
{
v[i]+=c;
sum[blo[a]]+=c;
}
if(blo[a]!=blo[b])
{
for(i=(blo[b]-1)*m+1;i<=b;i++)
{
v[i]+=c;
sum[blo[b]]+=c;
}
}
for(i=blo[a]+1;i<=blo[b]-1;i++)
{
atag[i]+=c;
}
}
ll query(int a,int b)
{
int i;
ll ans=0;
for(i=a;i<=min(b,blo[a]*m);i++)
{
ans+=v[i]+atag[blo[a]];
}
if(blo[a]!=blo[b])
{
for(i=(blo[b]-1)*m+1;i<=b;i++)
{
ans+=v[i]+atag[blo[b]];
}
}
for(i=blo[a]+1;i<=blo[b]-1;i++)
{
ans+=sum[i]+atag[i]*m;
}
return ans;
}
int main()
{
int i;
scanf("%d",&n);
m=sqrt(n);
for(i=1;i<=n;i++)
{
scanf("%lld",&v[i]);
blo[i]=(i-1)/m+1;
sum[blo[i]]+=v[i];
}
for(i=1;i<=n;i++)
{
int op,a,b,c;
scanf("%d%d%d%d",&op,&a,&b,&c);
if(op==0) add(a,b,c);
else printf("%lld\n",query(a,b)%(c+1));
}
}
5.给出一个长为n的数列,以及n个操作,操作涉及区间开方,区间求和。
区间求和就和上面的一样利用一个sum数组去做,而区间开方就要去思考一下了,50000最多也就开方4次就到1了,到了1之后,无论怎么开方都还是1,因此我们要标记这个块是否还可以开方就好了,atag==1表示还可以开方,所以一开始要初始化的所有块都是1(wa了好久才发现),其它和上面的题目差不多
#include <iostream>
#include <cstdio>
#include <cstring>
#include <string>
#include <queue>
#include <algorithm>
#include <vector>
#include <cmath>
#include <set>
#define INF 99999999
typedef long long ll;
using namespace std;
ll v[50999];//原数值
int blo[50999];//块的数组
int atag[50999];//每一个块的标记数组
int n,m;
set<int>s[1009];
ll sum[50009];
void reset(int x)
{
int flag=0,i;
sum[x]=0;
for(i=(x-1)*m+1;i<=x*m;i++)
{
v[i]=sqrt(v[i]);
sum[x]+=v[i];
if(v[i]>1) flag=1;
}
atag[x]=flag;
}
void sq(int a,int b)
{
int i;
for(i=a;i<=min(b,blo[a]*m);i++)
{
sum[blo[a]]-=v[i];
v[i]=sqrt(v[i]);
sum[blo[a]]+=v[i];
}
if(blo[a]!=blo[b])
{
for(i=(blo[b]-1)*m+1;i<=b;i++)
{
sum[blo[b]]-=v[i];
v[i]=sqrt(v[i]);
sum[blo[b]]+=v[i];
}
}
for(i=blo[a]+1;i<=blo[b]-1;i++)
{
if(atag[i])
{
reset(i);
}
}
}
ll query(int a,int b)
{
int i;
ll ans=0;
for(i=a; i<=min(b,blo[a]*m); i++)
{
ans+=v[i];
}
if(blo[a]!=blo[b])
{
for(i=(blo[b]-1)*m+1; i<=b; i++)
{
ans+=v[i];
}
}
for(i=blo[a]+1; i<=blo[b]-1; i++)
{
ans+=sum[i];
}
return ans;
}
int main()
{
int i;
scanf("%d",&n);
m=sqrt(n);
for(i=1; i<=n; i++)
{
scanf("%lld",&v[i]);
blo[i]=(i-1)/m+1;
sum[blo[i]]+=v[i];
}
for(i=1;i<=blo[n];i++)
{
atag[i]=1;
}
for(i=1; i<=n; i++)
{
int op,a,b,c;
scanf("%d%d%d%d",&op,&a,&b,&c);
if(op==0) sq(a,b);
else printf("%lld\n",query(a,b));
}
}
6.给出一个长为 nnn 的数列,以及 nnn 个操作,操作涉及单点插入,单点询问,数据随机生成。
分块插入操作,这里学会用pair的用法。
#include <iostream>
#include <cstdio>
#include <cstring>
#include <string>
#include <queue>
#include <algorithm>
#include <vector>
#include <cmath>
#include <set>
#include <map>
#define INF 99999999
typedef long long ll;
using namespace std;
int v[100999];//原数值
int blo[100999];//块的数组
int atag[100999];//每一个块的标记数组
int n,m,last;
int temp[200099];
vector<int>ve[1009];
pair<int ,int>query(int a)
{
int i=1;
while(a>ve[i].size())
{
a-=ve[i].size();
i++;
}
return make_pair(i,a-1);
}
void reset()
{
int top=1,i;
for(i=1;i<=last;i++)
{
for(vector<int>::iterator it=ve[i].begin();it!=ve[i].end();it++)
{
temp[top++]=*it;
}
ve[i].clear();
}
m=sqrt(top-1);
for(i=1;i<=top-1;i++)
{
blo[i]=(i-1)/m+1;
ve[blo[i]].push_back(temp[i]);
}
m=blo[top-1];
}
void insert(int a,int b)
{
pair<int,int>t=query(a);//
ve[t.first].insert(ve[t.first].begin()+t.second,b);
if(ve[t.first].size()>m*m) reset();
}
int main()
{
int i;
scanf("%d",&n);
m=sqrt(n);
for(i=1;i<=n;i++)
{
scanf("%d",&v[i]);
blo[i]=(i-1)/m+1;
ve[blo[i]].push_back(v[i]);
}
last=blo[n];
for(i=1;i<=n;i++)
{
int op,a,b,c;
scanf("%d%d%d%d",&op,&a,&b,&c);
if(op==0) insert(a,b);
else
{
pair<int,int>t=query(b);
printf("%d\n",ve[t.first][t.second]);
}
}
}
7.给出一个长为n的数列,以及n个操作,操作涉及区间乘法,区间加法,单点询问。
莫名其妙出bug,最后发现自己b写成了blo[b]...这里学会维护两个标记,应该是乘法优先,如果加法优先的话,会被覆盖
#include <iostream>
#include <cstdio>
#include <cstring>
#include <string>
#include <queue>
#include <algorithm>
#include <vector>
#include <cmath>
#include <set>
#include <map>
#define INF 99999999
#define mod 10007
typedef long long ll;
using namespace std;
int v[100999];//原数值
int blo[100999];//块的数组
int atag[100999];//每一个块的标记数组
int mtag[100999];
int n,m,last;
void reset(int x)
{
int i;
for(i=(x-1)*m+1;i<=min(n,x*m);i++)
{
v[i]=(v[i]*mtag[x]+atag[x])%mod;
}
atag[x]=0;
mtag[x]=1;
}
void add(int a,int b,int c,int op)
{
int i;
reset(blo[a]);
for(i=a;i<=min(b,blo[a]*m);i++)
{
if(op==0) v[i]+=c;
else v[i]*=c;
v[i]%=mod;
}
if(blo[a]!=blo[b])
{
reset(blo[b]);
for(i=(blo[b]-1)*m+1;i<=b;i++)
{
if(op==0) v[i]+=c;
else v[i]*=c;
v[i]%=mod;
}
}
for(i=blo[a]+1;i<=blo[b]-1;i++)
{
if(op==0)
{
atag[i]=(atag[i]+c)%mod;
}
else
{
atag[i]=(atag[i]*c)%mod;
mtag[i]=(mtag[i]*c)%mod;
}
}
}
int main()
{
int i;
scanf("%d",&n);
m=sqrt(n);
for(i=1;i<=n;i++)
{
scanf("%d",&v[i]);
blo[i]=(i-1)/m+1;
mtag[i]=1;
}
for(i=1;i<=n;i++)
{
int op,a,b,c;
scanf("%d%d%d%d",&op,&a,&b,&c);
if(op==0||op==1) add(a,b,c,op);
else printf("%d\n",(v[b]*mtag[blo[b]]+atag[blo[b]])%mod);
}
}
8.给出一个长为n的数列,以及n个操作,操作涉及区间询问等于一个数c的元素,并将这个区间的所有元素改为c。
如果区间是不完整块,那么先把这个块全部变成标记的值,并且将标记变为-1,证明这个块是有不同值得,暴力去比较并且修改,对于完整块,如果c和标记不同,一个个去比较,然后改变值,如果相等直接加上块元素的数量。
#include <iostream>
#include <cstdio>
#include <cstring>
#include <string>
#include <queue>
#include <algorithm>
#include <vector>
#include <cmath>
#include <set>
#include <map>
#define INF 99999999
#define mod 10007
typedef long long ll;
using namespace std;
int v[100999];//原数值
int blo[100999];//块的数组
int atag[100999];//每一个块的标记数组
int mtag[100999];
int n,m,last;
void reset(int x)
{
int i;
for(i=(x-1)*m+1; i<=min(n,x*m); i++)
{
v[i]=atag[x];
}
atag[x]=-1;
}
int add(int a,int b,int c)
{
int i;
int ans=0;
if(atag[blo[a]]!=-1)reset(blo[a]);
for(i=a; i<=min(b,blo[a]*m); i++)
{
if(v[i]==c) ans++;
else v[i]=c;
}
if(blo[a]!=blo[b])
{
if(atag[blo[b]]!=-1)reset(blo[b]);
for(i=(blo[b]-1)*m+1; i<=b; i++)
{
if(v[i]==c) ans++;
else v[i]=c;
}
}
for(i=blo[a]+1; i<=blo[b]-1; i++)
{
if(atag[i]!=-1)
{
if(atag[i]==c) ans+=m;
else atag[i]=c;
}
else
{
for(int j=(i-1)*m+1;j<=i*m;j++)
{
if(v[j]==c) ans++;
else v[j]=c;
}
atag[i]=c;
}
}
return ans;
}
int main()
{
int i;
scanf("%d",&n);
m=sqrt(n);
for(i=1; i<=n; i++)
{
scanf("%d",&v[i]);
blo[i]=(i-1)/m+1;
atag[i]=-1;
}
for(i=1; i<=n; i++)
{
int op,a,b,c;
scanf("%d%d%d",&a,&b,&c);
printf("%d\n",add(a,b,c));
}
}
9.给出一个长为n的数列,以及n个操作,操作涉及询问区间的最小众数。
这是一道经典的难题(我不知道,我也是听大牛说的,反正我不会...)还有区间修改的操作,以后有实力在去搞搞吧...题目先对没一个元素去映射一下,然后把每一个值都标记上序号,这就是v[i]了,然后val[i]表示该序号的值,然后把相同序号的元素放到一个vector中,接着就是预处理了,这个预处理是以块为单位,[1,blo[n]] [2,blo[n]] [3,blo[n]]...这样就把所有的区间都覆盖了f[i][j]表示第i块到第j块的最小众数。然后对于不完整块进行暴力查询,查询的方法就是在该序号里面找到第一个大于等于l,和第一个大r的元素,然后两个作作差就是这个数在[l,r]区间的个数了。
#include <iostream>
#include <cstdio>
#include <cstring>
#include <string>
#include <queue>
#include <algorithm>
#include <vector>
#include <cmath>
#include <set>
#include <map>
#define INF 0x3f3f3f3f
#define mod 10007
#define maxn 100009
#define L rt<<1
#define R rt<<1|1
typedef long long ll;
using namespace std;
int n,m;
int val[maxn];
int v[maxn];
int blo[maxn];
int cnt[maxn];
vector<int>ve[1009];
map<int,int>mp;
int f[1005][1005];
void pre(int x)
{
memset(cnt,0,sizeof(cnt));
int i,ans=0,mx=0;
for(i=(x-1)*m+1; i<=n; i++)
{
cnt[v[i]]++;
if(cnt[v[i]]>mx||cnt[v[i]]==mx&&val[v[i]]<val[ans])
{
ans=v[i];
mx=cnt[v[i]];
}
f[x][blo[i]]=ans;
}
}
int query(int l,int r,int x)
{
int t=upper_bound(ve[x].begin(),ve[x].end(),r)-lower_bound(ve[x].begin(),ve[x].end(),l);
return t;
}
int Q(int a,int b)
{
int ans=f[blo[a]+1][blo[b]-1],mx=query(a,b,ans),i;
for(i=a; i<=min(b,blo[a]*m); i++)
{
int temp=query(a,b,v[i]);
if(temp>mx||temp==mx&&val[v[i]]<val[ans])
{
mx=temp;
ans=v[i];
}
}
if(blo[a]!=blo[b])
{
for(i=(blo[b]-1)*m+1; i<=b; i++)
{
int temp=query(a,b,v[i]);
if(temp>mx||temp==mx&&val[v[i]]<val[ans])
{
mx=temp;
ans=v[i];
}
}
}
return val[ans];
}
int main()
{
scanf("%d",&n);
m=sqrt(n);
int id=0,i;
for(i=1; i<=n; i++)
{
scanf("%d",&v[i]);
blo[i]=(i-1)/m+1;
if(!mp[v[i]])
{
mp[v[i]]=++id;
val[id]=v[i];//该编号的值
}
v[i]=mp[v[i]];//将该值变成编号
ve[v[i]].push_back(i);//这是id那个点的序号
}
for(i=1; i<=blo[n]; i++) pre(i); //这是将所有块区间的众数都遍历一遍
//如[1,blo[n]] [2,3]表示第1-blo[n],2-3的众数
for(i=1;i<=n;i++)
{
int a,b;
scanf("%d%d",&a,&b);
printf("%d\n",Q(a,b));
}
}
、
最后说一下感想吧,大牛的代码风格真的很好,很值得我去学习,我要是能学到他一半我就已经知足了...最近思考了很多,也很迷茫,今年发生了很多事情,这些事情足以改变我的人生了,对于acm,之前有一段时间是比较厌恶反感的,因为有的人思想太过功利了,弄得好像二进制一样不是0就是1,世界哪有分的那么清的,突然我又喜欢上了acm,喜欢上了学算法,是单纯的想要去学习,而并不是想要拿奖拿奖云云,可能以后会打一堆破铜烂铁,但又如何?当心胸放宽广了,好像这些也不什么大事了。