分块思想的应用

##分块思想

利用分块的思想设计出的算法其复杂度往往与根号相关。

OI 中常见的数据结构题目:

  • 区间修改……
  • 区间查询……

这类题目往往可以用线段树、平衡树等经典的数据结构来解决,但传统的数据结构有代码难度大、常数大的劣势,对一些这类问题我们可以考虑对时间分治,但并不是所有问题都容易实现区间合并,这时就需要利用分块的思想,

常见的套路是:

假设有 $n$ 个元素,将连续的 $m$ 个元素分成一块,那么每次查询或修改时的区间一定包含最多 $\left\lfloor\frac{n}{m}\right\rfloor$ 个完整的段,和最多 $2m-2$ 个单点,我们对完整的段统一处理,对剩余的单点暴力处理。

那么只要选取合适的 $m$ ,在本例中就是 $\sqrt n$ ,设单点修改的复杂度为 $f(n)$ , 那么总复杂度就是 $O(nf(n)\sqrt n)$

[CodeChef] Chef and Churu

给定一个长度为 $n$ 的数字序列 $a$ ,和一个长度为 $n$ 的函数序列 $f$ ,每个函数都是 $f_i=\sum_{j=l_i}^{r_i}a_j$ 形式。

  • 询问 $\sum_{i=l}^rf_i$ 的值;

  • 修改 $a_i$ 的值。

    $n\leq10^5,a_i\leq10^9$

首先,我们要对函数分块,记录每一块的初始答案,那么每次询问时求整块的答案,再加上两边不超过 $2\sqrt n$ 个函数的值,若能做到 $O(1)$ 查询函数值,那么只需要 $O(\sqrt n)$ 的时间复杂度。

那么问题就变成了如何$O(1)$ 查询函数值,其实不难:对数字序列也分块,维护每一块的块内前缀和,和每一块的之前的块的一个前缀和。

每次修改时,需要修改:

  • 数字序列的两个前缀和,直接 $O(\sqrt n)$ 更新,以保证单个函数查询无误;
  • 函数块的答案,需要预处理出没个函数快中的函数包含数字序列中的各个元素次数,共需要 $O(\sqrt n)$ 的时间复杂度,这样保证了整块函数值查询无误。

综上所述,查询和修改的时间复杂度都是 $O(\sqrt n)$ ,那么整个算法的时间复杂度就是 $O(n\sqrt n)$ .

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
#include<iostream>
#include<cstdio>
#include<cmath>
using std::min;
typedef unsigned long long ull;

const int MAXN=1e5+5,SN=355;

int N,Q;
int bnum,bsz;

int a[MAXN];
ull sum1[SN],sum2[SN][SN];
void initA()
{
int i,j;
for(i=1;i<=bnum;i++)
{
int la=(i-1)*bsz;
for(j=1;j<=bsz&&la+j<=N;j++) sum2[i][j]=sum2[i][j-1]+a[la+j];
sum1[i+1]=sum1[i]+sum2[i][j-1];
}
}
ull calSum(int x) {return sum1[(x-1)/bsz+1]+sum2[(x-1)/bsz+1][(x-1)%bsz+1];}
ull calSum(int l,int r){return calSum(r)-calSum(l-1);}

int fl[MAXN],fr[MAXN];
int cont[SN][MAXN];
ull sum[SN];
void initF()
{
int i,j;
for(i=1;i<=bnum;i++)
{
int la=(i-1)*bsz;
for(j=1;j<=bsz&&la+j<=N;j++)
sum[i]+=calSum(fl[la+j],fr[la+j]),
cont[i][fl[la+j]]++,cont[i][fr[la+j]+1]--;
}
for(i=1;i<=bnum;i++)
for(j=1;j<=N;j++) cont[i][j]+=cont[i][j-1];
}

void mdf(int p,int v)
{
int i,pB=(p-1)/bsz+1;
for(i=p;i<=pB*bsz&&i<=N;i++)
sum2[pB][i-(pB-1)*bsz]+=v-a[p];
for(i=pB+1;i<=bnum;i++) sum1[i]+=v-a[p];

for(i=1;i<=bnum;i++) sum[i]+=(ull)(v-a[p])*cont[i][p];

a[p]=v;
}
ull qry(int l,int r)
{
int i,lB=(l-1)/bsz+1,rB=(r-1)/bsz+1;ull res=0;
if(lB==rB) for(i=l;i<=r;i++) res+=calSum(fl[i],fr[i]);
else
{
for(i=l;i<=lB*bsz;i++) res+=calSum(fl[i],fr[i]);
for(i=lB+1;i<=rB-1;i++) res+=sum[i];
for(i=(rB-1)*bsz+1;i<=r;i++) res+=calSum(fl[i],fr[i]);
}
return res;
}

int main()
{
int i;
scanf("%d",&N);
bsz=std::sqrt(N);bnum=(N+bsz-1)/bsz;
for(i=1;i<=N;i++) scanf("%d",a+i);initA();
for(i=1;i<=N;i++) scanf("%d%d",fl+i,fr+i);initF();
scanf("%d",&Q);
while(Q--)
{
int opt;scanf("%d",&opt);
int x,y;scanf("%d%d",&x,&y);
if(opt==1) mdf(x,y);
else printf("%llu\n",qry(x,y));
}
return 0;
}

##分类讨论

分块的思想本质上就是分类讨论:大块的个数少,小点修改起来块。

其实在很多应用分块思想的题目中并不是直接分块,而是应用了分块的分类讨论思想

###[TC SRM589 Div1] Flipping Bits

给定一个长度为 $n$ 的 01 字符串,每次操作可以将一个位置取反,或是将前 $k*m$ 个位置取反,问最少要多少次操作才能将原串变成一个长度为 $n-m$ 的前缀和长度为 $n-m$ 的后缀长度相等的串。

$n\leq300$

首先,题目中所谓”长度为 $n-m$ 的前缀和长度为 $n-m$ 的后缀长度相等的串“其实就是循环节为 $m$ 的循环串。

然后虽然不会证明,但不难发现这种题是不会有线性解法的(至少我不会)。所以只能枚举暴力,但是数据范围对 $O(2^n)$ 复杂度来说还是太大了,于是我们遵循题目中的暗示:分块。

其实就是分两种情况讨论:

  • 当 $m\leq\sqrt n$ 时,直接枚举答案(循环节),这样的话再 DP 一下整段的翻转,转移的时候就加上需要的单点修改次数即可。
  • 当 $m>\sqrt n$ 时,每一块都很长,但总数就少了,那么枚举每一段是否被翻转。注意到在每各个块中相对位置相同的元素一定要相同,那么就统计一下这些元素的 01 的数量,把少的改成多的就好。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
#include<iostream>
#include<cstdio>
#include<vector>
#include<string>
using std::min;
using std::max;
using std::vector;
using std::string;

const int INF=1061109567;

unsigned popcount(unsigned u)
{
u = (u & 0x55555555) + ((u >> 1) & 0x55555555);
u = (u & 0x33333333) + ((u >> 2) & 0x33333333);
u = (u & 0x0F0F0F0F) + ((u >> 4) & 0x0F0F0F0F);
u = (u & 0x00FF00FF) + ((u >> 8) & 0x00FF00FF);
u = (u & 0x0000FFFF) + ((u >> 16) & 0x0000FFFF);
return u;
}

class FlippingBitsDiv1{public:int getmin(vector<string> Sv,int M);};

int FlippingBitsDiv1::getmin(vector<string> Sv,int M)
{
string s=std::accumulate(Sv.begin(),Sv.end(),string());
int N=s.length();

int i,ans=INF;
if(M<=17)
{
for(int S=0;S<(1<<M);S++)
{
int f[2];f[0]=f[1]=0;
for(int op=0;op<N;op+=M)
{
int l=min(M,N-op);
int oS=0;for(i=0;i<l;i++) if(s[op+i]-'0') oS|=1<<i;
int msk=(1<<l)-1;
int cst0=f[0]+popcount((S^oS)&msk),
cst1=f[1]+popcount((~S^oS)&msk);
f[0]=min(cst0,cst1+1);
f[1]=min(cst1,cst0+1);
}
ans=min(ans,min(f[0],f[1]));
}
}else
{
int gnum=(N+M-1)/M;
for(int fS=0;fS<(1<<gnum);fS++)
{
int res=0;bool o,la=false;
for(i=0;i<gnum;i++)
{
o=fS&(1<<i);
if(o^la) res++;
la=o;
}
int g,cnt[2];
for(int op=0;op<M;op++)
{
cnt[0]=cnt[1]=0;
for(i=op,g=0;i<N;i+=M,g++)
cnt[(s[i]-'0')^bool(fS&(1<<g))]++;
res+=min(cnt[0],cnt[1]);
}
ans=min(ans,res);
}
}
return ans;
}

定期(延时)重构

延时重构的思想很简单,就是每次询问单独处理复杂度太高,于是我们把一些询问分成一块,每次处理一个快内的询问。

##莫队算法

数论中的分块

由于数论的一些性质,许多题目可以用类似分块的做法处理。

[[51nod 1386] 双马尾机器人]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
#include<iostream>
#include<cstdio>
#include<cstring>
#include<vector>
using std::min;
using std::max;
inline void umn(int &a,int b){a=min(a,b);}
inline void umx(int &a,int b){a=max(a,b);}

const int MAXN=1005,PR[11]={2,3,5,7,11,13,17,19,23,29,31},PSN=200,MAXS=(1<<11)+5;

int mxPF[MAXN],stt[MAXN];
int mnPF[MAXN],pri[MAXN],pcnt;
inline void preTab()
{
int i,j;
for(i=2;i<MAXN;i++)
{
if(!mnPF[i]) mnPF[i]=pri[++pcnt]=i;
for(j=1;j<=pcnt&&pri[j]*i<MAXN;j++)
{
mnPF[pri[j]*i]=pri[j];
if(i%pri[j]==0) break;
}
}
for(i=2;i<MAXN;i++)
{
for(j=0;j<=10;j++) if(i%PR[j]==0) stt[i]|=1<<j;
for(int x=i;x>1;x/=mnPF[x]) umx(mxPF[i],mnPF[x]);
}
}

int gid[MAXN],gcnt;std::vector<int> gr[PSN];

int f[PSN][MAXS];
int solve(int n,int k)
{
int i,aS=0;for(i=2;i<=n;i++) if(i!=k) aS|=stt[i];
memset(f,0x3f,sizeof f);
f[0][0]=0;

gcnt=0;memset(gid,0,sizeof gid);
for(i=2;i<=n;i++) if(i!=k)
{
if(mxPF[i]>31)
{
if(!gid[mxPF[i]]) gr[gid[mxPF[i]]=++gcnt].clear();
gr[gid[mxPF[i]]].push_back(stt[i]);
}
else
{
umn(f[0][stt[i]],1);
for(int laS=aS;laS;laS=(laS-1)&aS)
umn(f[0][stt[i]|laS],f[0][laS]+1);
}
}

unsigned p;
for(i=1;i<=gcnt;i++)
for(int oS=gr[i][p=0];p<gr[i].size();oS=gr[i][++p])
{
umn(f[i][oS],1);
for(int laS=aS;laS;laS=(laS-1)&aS)
umn(f[i][oS|laS],f[i-1][laS]+1);
}
return f[gcnt][aS]+int(k!=1);
}

int main()
{
preTab();
int Case,T;scanf("%d",&T);
for(Case=1;Case<=T;Case++)
{
int n,k;scanf("%d%d",&n,&k);
printf("Case #%d: %d\n",Case,solve(n,k));
}
return 0;
}