(简记)根号分治
Intro
你需要知道,这是一种辅助优化暴力的技巧,通常通过设置一个阈值 \(b\) 选择两种暴力的其中一种,因为最终 \(b\) 一般取 \(\sqrt{n}\) 最优故有根号分治之称。
例题
P8572 [JRKSJ R6] Eltaw
这题的根号是全局的,给出了行数列数 \(nk \le 5\times 10^5\) 的条件,则必然有 \(n,k\) 其中之一 \(\le \sqrt{nk}\le \sqrt{5\times 10^5}\),分别考虑。
对于 \(k\le \sqrt{nk}\),直接暴力扫每行,前缀和 \(O(1)\) 查询即可,时间 \(O(nk+q\sqrt{nk})\)。
对于 \(n\le \sqrt{nk}\),我们发现不同的区间总共只有 \(n^2\le nk\le 5\times 10^5\) 个,那么就说明这 \(q\) 次询问中有挺多应该是重复的,那么对于 \(k\) 行,每行用其前缀和更新 \(f_{l,r}\) 表示询问 \([l,r]\) 的答案,预处理只需要 \(O(n^2k)\)。
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N=5e5+5;
int n,k,q;
vector<LL>sum[N];
LL res[710][710];
int main(){
ios::sync_with_stdio(0);
cin.tie(0);cout.tie(0);
cin>>k>>n>>q;
for(int i=0;i<n;i++){
LL pre=0;
for(int j=0;j<k;j++){
LL x;cin>>x;pre+=x;
sum[i].push_back(pre);
}
}
if(n<=k){
while(q--){
int l,r;
LL mx=0;
cin>>l>>r;
l--,r--;
for(int i=0;i<n;i++)
mx=max(mx,sum[i][r]-(l>0?sum[i][l-1]:0));
cout<<mx<<'\n';
}
}
else {
for(int i=0;i<k;i++)
for(int j=i;j<k;j++){
for(int ID=0;ID<n;ID++){
res[i][j]=max(res[i][j],sum[ID][j]-(i>0?sum[ID][i-1]:0));
}
}
while(q--){
int l,r;
cin>>l>>r;
l--,r--;
cout<<res[l][r]<<'\n';
}
}
return 0;
}
P3396 哈希冲突
如题,该题有两种暴力的方案。
- \(O(1)\) 修改 \(val_i\),然后 \(O(\frac{n}{x})\) 查询。
- \(O(b)\) 修改一个代表 \(i\bmod x=y\) 的位置的和的 \(c_{x,y}\),其中 \(x\) 是自变量,\(y\) 是因变量,然后 \(O(1)\) 查询。
对于方案 1,我们希望 \(\frac{n}{x}\) 尽量小,对于方案 2,我们又希望 \(O(b)\) 尽量小。我们可以把两种暴力混搭,如果 \(x\le b\) 就使用方案 2,否则使用方案 1。那么每次修改就需要修改 \(O(b)\) 次,把 \(\le b\) 的 \(c\) 全部修改,然后改掉 \(val_i\)。每次查询时如果 \(x\le b\) 就用方案 2 \(O(1)\) 查询,否则用方案 \(1\) \(O(\frac{n}{x})\) 查询。
可以发现,由于用 2 时 \(x>b\implies \frac{n}{x}<\frac{n}{b}\),意味着 \(O(\frac{n}{x})\) 其实就是 \(O(\frac{n}{b})\) 的,这样我们就通过一个阈值 \(b\) 把每次操作次数上限变成了 \(\max(\frac{n}{b},b)\) 的,显然在两者相等时取最小,\(b=\sqrt{n}\),时间 \(O(m\sqrt{n})\)。
#include<bits/stdc++.h>
using namespace std;
const int sN=1e3+5,N=1.5e5+5;
int n,m,b,c[sN][sN],val[N];
int main(){
scanf("%d%d",&n,&m);b=sqrt(n);
for(int i=1;i<=n;i++){
scanf("%d",&val[i]);
for(int j=1;j<=b;j++)
c[j][i%j]+=val[i];
}
while(m--){
char ch[1];
scanf("%s",ch);
int x,y;
scanf("%d%d",&x,&y);
if(ch[0]=='A'){
if(x<=b)printf("%d\n",c[x][y]);
else {
int sum=0;
for(int i=y;i<=n;i+=x)
sum+=val[i];
printf("%d\n",sum);
}
}
else {
for(int j=1;j<=b;j++)
c[j][x%j]-=val[x];
val[x]=y;
for(int j=1;j<=b;j++)
c[j][x%j]+=val[x];
}
}
return 0;
}
P5309 [Ynoi2011] 初始化
根据 trick,我们在修改时,当 \(x>b\),选择暴力修改,先分块(块长可取 \(\sqrt{n}\)),然后对于每个块记录 \(sum\),修改的时候同时改单个值和块的 \(sum\)。这样查询的时候就可以 \([l,r]\) 中整块直接累加,散块暴力处理。
当 \(x\le b\),我们选择用一个 \(c_{i,j}\) 记录修改时 \(i=x,j=y\) 的 \(z\) 的总和。这似乎不是很够,那么我们对 \(j\) 这一维做一个前后缀和,这样修改的时候要修改最多 \(x\) 次,查询的时候将序列视为关于 \(x\) 块长的分块,整块一起处理散块前后缀累加即可。
这样的时间理论上可以达到 \(O(m(\sqrt{n}+b+\frac{n}{b}))\),然而由于查询操作中逐个访问 \(1\) 到 \(b\) 常数有点大,所以 \(b\) 取 \(\sqrt{n}\) 可能有点小问题,根据讨论区适当卡常。(?
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const LL MOD=1e9+7;
const int sN=1e3+5,N=2e5+5;
int n,m,bs;
LL pre[sN][sN],suf[sN][sN],a[N],sum[sN];
int b[N],L[sN],R[sN];
inline void Mo(LL &a){a>MOD?a-=MOD:1;}
int main(){
ios::sync_with_stdio(0);
cin.tie(0);cout.tie(0);
cin>>n>>m;
bs=300;
for(register int i=1;i<=n;i++){
cin>>a[i];
a[i]%=MOD;
b[i]=(i-1)/bs+1;
if(!L[b[i]])L[b[i]]=i;
R[b[i]]=i;
sum[b[i]]+=a[i];
Mo(sum[b[i]]);
}
bs=160;
while(m--){
int op;
cin>>op;
if(op==1){
int x,y;
LL z;
cin>>x>>y>>z;
y%=x;z%=MOD;
if(x>bs){
for(register int i=y;i<=n;i+=x){
a[i]+=z,Mo(a[i]);
sum[b[i]]+=z,Mo(sum[b[i]]);
}
}
else {
for(register int i=0;i<=y;i++)
suf[x][i]+=z,Mo(suf[x][i]);
for(register int i=y;i<x;i++)
pre[x][i]+=z,Mo(pre[x][i]);
}
}
else {
int l,r;
cin>>l>>r;
LL res=0;
if(b[l]==b[r])
for(register int i=l;i<=r;i++)
res+=a[i];
else {
for(register int i=b[l]+1;i<=b[r]-1;i++)
res+=sum[i];
for(register int i=l;i<=R[b[l]];i++)
res+=a[i];
for(register int i=L[b[r]];i<=r;i++)
res+=a[i];
}
res%=MOD;
for(register int x=1;x<=bs;x++){
LL sum=suf[x][0];
res+=max(0,r/x-l/x-1)*sum;
if(l/x==r/x)
res+=pre[x][r%x]-(l%x?pre[x][l%x-1]:0);
else {
res+=suf[x][l%x];
res+=pre[x][r%x];
}
}
cout<<(res%MOD+MOD)%MOD<<'\n';
}
}
return 0;
}

浙公网安备 33010602011771号