根号分治做题记录
对于块长 \(B=\sqrt n\),对 \(\le B\) 和 \(>B\) 采用其中一种使用暴力,另一种利用 \(\frac n B\le B\) 使用另一种策略。这种方法叫根号分治。
利用基本不等式等等,对 \(B\) 的取值进行平衡,是复杂度最优。
【例1】Train Maintenance
【例2】[POI2015]ODW
对步长 \(k\) 分类讨论。
当 \(k>B\) 时,跳的次数不会超过 \(n/B\),暴力跳,用倍增实现跳 \(k\) 条边。总 \(O(n\cdot n/B\log n)\)
当 \(k\le B\) 时,考虑对于所有 \(k\) 相同的一起考虑。这个东西显然是树上差分类似东西,所以记 \(w_{u,val}\) 为 \(u\) 的满足 \(dep\bmod k=val\) 的前缀和则对一组 \((s,t,k)\) 的答案等于 \(w_{s,dep_s\bmod k}+w_{t,dep_t\bmod k}-w_{fa[lca(s,t)][0],dep_s\bmod k}-w_{fa[lca(s,t)][0],dep_t\bmod k}\),如果 \(lca\) 被算了2次就还要再减去 \(lca\) 的不赘述。这个总共 \(O(B\cdot n)\)。
理论最优 \(B=\sqrt{n\log n}\),实测最优 \(B=20\),前者 TLE。标算使用长链剖分降低复杂度,然而我不会。
#include <bits/stdc++.h>
using namespace std;
const int N=5e4+5;
int n,num,a[N],b[N],c[N],res[N],dep[N],fa[N][17],cnt[225];
vector<int>G[N],want[N];
map<pair<int,int>,int>U;
struct J{
int s,t,k,id;
}q[N];
inline int read(){
register char ch=getchar();register int x=0;
while(ch<'0'||ch>'9')ch=getchar();
while(ch>='0'&&ch<='9')x=(x<<1)+(x<<3)+(ch^48),ch=getchar();
return x;
}
void dfs(int x,int p){
dep[x]=dep[p]+1;
fa[x][0]=p;
for(int i=1;i<=15;i++)fa[x][i]=fa[fa[x][i-1]][i-1];
for(int i=0;i<G[x].size();i++){
int y=G[x][i];
if(y^p)dfs(y,x);
}
}
int glca(int u,int v){
if(u==v)return u;
if(dep[u]>dep[v])swap(u,v);
for(int i=15;~i;i--)if(dep[fa[v][i]]>=dep[u])v=fa[v][i];
if(u==v)return u;
for(int i=15;~i;i--)if(fa[u][i]!=fa[v][i])u=fa[u][i],v=fa[v][i];
return fa[u][0];
}
void dfsk(int x,int p,int k){
cnt[dep[x]%k]+=a[x];
for(int i=0;i<want[x].size();i++)U[make_pair(x,want[x][i])]=cnt[want[x][i]];
for(int i=0;i<G[x].size();i++){
int y=G[x][i];
if(y^p)dfsk(y,x,k);
}
cnt[dep[x]%k]-=a[x];
}
inline int tok(int u,int k){
for(int i=15;(~i)&&u;i--)if((k>>i)&1)u=fa[u][i];
return u;
}
int solve(int s,int t,int k){
int lca=glca(s,t),sum=0;bool fl=0;
for(;dep[s]>=dep[lca];s=tok(s,k)){sum+=a[s];if(s==lca)fl=1;}
for(;dep[t]>=dep[lca];t=tok(t,k)){if(t==lca&&fl)break;sum+=a[t];}
return sum;
}
void print(int x){
if(x/10)print(x/10);
putchar(x%10+48);
}
int main(){
n=read();
for(int i=1;i<=n;i++)a[i]=read();
for(int i=1,u,v;i<n;i++)u=read(),v=read(),G[u].push_back(v),G[v].push_back(u);
for(int i=1;i<=n;i++)b[i]=read();
for(int i=1;i<n;i++)c[i]=read();
dfs(1,0);
int B=20;
for(int i=1;i<n;i++){
if(c[i]<=B)q[++num]={b[i],b[i+1],c[i],i};
else res[i]=solve(b[i],b[i+1],c[i]);
}
sort(q+1,q+num+1,[](J a,J b){return a.k<b.k;});
for(int i=1,las=1;i<=num;i++){
if(i==num||q[i].k!=q[i+1].k){
U.clear();
for(int j=1;j<=n;j++)want[j].clear();
for(int j=las;j<=i;j++){
want[q[j].s].push_back(dep[q[j].s]%q[i].k);
want[q[j].t].push_back(dep[q[j].t]%q[i].k);
int lca=fa[glca(q[j].s,q[j].t)][0];
if(!lca)continue;
want[lca].push_back(dep[q[j].s]%q[i].k);
want[lca].push_back(dep[q[j].t]%q[i].k);
}
dfsk(1,0,q[i].k);
for(int j=las;j<=i;j++){
want[q[j].s].push_back(dep[q[j].s]%q[i].k);
want[q[j].t].push_back(dep[q[j].t]%q[i].k);
int lca2=glca(q[j].s,q[j].t),lca=fa[lca2][0];
res[q[j].id]=U[make_pair(q[j].s,dep[q[j].s]%q[i].k)]+
U[make_pair(q[j].t,dep[q[j].t]%q[i].k)]-
U[make_pair(lca,dep[q[j].s]%q[i].k)]-
U[make_pair(lca,dep[q[j].t]%q[i].k)];
if(dep[lca2]%q[i].k==dep[q[j].s]%q[i].k&&
dep[lca2]%q[i].k==dep[q[j].t]%q[i].k)
res[q[j].id]-=a[lca2];
}
las=i+1;
}
}
for(int i=1;i<n;i++)print(res[i]),puts("");
}
【例3】CF1654E Arithmetic Operations
每次可以把数列 \(a_n\) 选任一位置改成任意整数,最少几次变成等差数列?\(n,a_i\le 10^5\)
很显然题目可以化成最多多少个点共线,最后答案是 \(n-ans\)。
写一下 \(a_i=ki+b\),移个项:\(b=a_i-ki\)。可以想到暴力思路是枚举公差 \(k\),\(O(n)\) 遍历 \(a_i\) 并将 \(b\) 放入桶,后来统计最多的 \(b\)。
根据公差(值域为 \(M=\max a_i\))根号分治。设块长为 \(B\)。
不妨设斜率(公差) \(k\ge 0\),因为只需要翻转 \(a\) 就是 \(k\le 0\)。
- 当 \(d<B\) 时,上头的暴力思路可以使用。——\(O(nB)\)。
- 当 \(d\ge B\) 时,需要探求一些性质。首先可以考虑 dp,设 \(f[i][k]\) 表示前 \(i\) 个数公差(斜率)为 \(k\)(显然是个整数)最多有多少个不修改,则 \(f[i][k]=\max\{f[j][k]\}\)。考虑降低转移复杂度,则需探究相邻两个不被修改的数之间满足什么性质。对于当前的 \(i\) 和 \(j<i\),假若 \(i-j>M/B\),则 \(a_i-a_j=(i-j)k>M/B\cdot k\ge M\),这显然不可能,所以 \(j\) 只能 \(\ge M/B\)。dp 数组可以用 map 存,块长取 70 时很快。