根号分治
根号分治:
引入:
有这样一类问题:有 \(n\) 个序列,\(m\) 个询问,存在两种做法:\(O(n^2)\) 预处理和 \(O(mn)\) 的不预处理.
显然,两种方法的复杂度都无法接受,因此考虑一种方法是否能平衡这种复杂度。
然后,就拥有了 根号分治 这种方法,思路和 分块的整块处理块和枚举处理 类似
一般来说,根号分治的题目可以分为 预处理阶段 和 枚举阶段
分析:
根据一道题目引入:P3396 哈希冲突
题意:
给定 \(n\) 长序列,\(m\) 个操作:
A x y
询问在序列下标模 \(x\) 时,余数为 \(y\) 的下标的对应的值的加和
C x y
把序列第 \(x\) 个数的值替换成 \(y\)
解决:
我们有两种想法:
-
\(O(n^2)\) 处理 模 \(i\) 意义下值为 \(j\) 的答案,每次修改为 \(O(n)\).
-
每次询问暴力求 \(O(mn)\) ,修改为 \(O(1)\)
这两种肯定都不行。考虑优化:
对于模数小于 \(\sqrt{n}\) 的情况,按照第一种方法做,预处理为 \(O(n\sqrt{n})\), 查询为 \(O(1)\)
对于模数大于 \(\sqrt{n}\) 的情况,模 \(\sqrt{n}\) 的结果为 \(i\) 的情况最多只有 \(\sqrt{n}\) 个数产生贡献。因此用第二种方法:查询修改复杂度为 \(O(\sqrt{n})\)
代码:
#include<bits/stdc++.h>
using namespace std;
const int N=1.5e5+5,M=405;
char ch[5];
int n,m,x,y;
int dp[M][M],a[N],mod;
int main(){
cin>>n>>m; mod=sqrt(n);
for(int i=1;i<=n;i++) scanf("%d",&a[i]);
for(int i=1;i<=n;i++) for(int j=1;j<=mod;j++) dp[j][i%j]+=a[i];
while(m--){
scanf("%s%d%d",ch,&x,&y);
if(ch[0]=='A'){
if(x<=mod) printf("%d\n",dp[x][y]);
else{
int res=0; for(int i=y;i<=n;i+=x) res+=a[i]; printf("%d\n",res);
}
}
else{ for(int i=1;i<=mod;i++) dp[i][x%i]+=(y-a[x]); a[x]=y;}
}
return 0;
}
例题:
CF797E Array Queries
题意:
给定一个长度为 \(n\) 的序列 \(a\) 有 \(m\) 次询问:
p k
要求不断操作 \(p=p+a_p+k\) 直到 \(p>n\) ,求操作次数。
分析:
这道题和之前的题分析过程差不多,但是要预处理。
设 \(dp[i][j]\) 表示 \(i=p,j=k\) 的情况,此时的 根号情况设置成 \(k\). 因为如果刨除 \(a[i]\) 的值的影响, 整个递推过程的时间复杂度只跟 \(k\) 的大小有关.
下标从大到小 递推预处理: 通过 \(dp[i+a_i+j][j]\) 的情况推理出 \(dp[i][j]\) 的值,同时记得预处理 \(i+a_i+j>n\) 的情况,则有转移方程:
if(i+a[i]+j>n) dp[i][j]=1;
else dp[i][j]=dp[i+a[i]+j][j]+1;
-
\(k\leq \sqrt{n}\) 直接输出 \(dp[p][k]\)
-
\(k>\sqrt{n}\),递推查询 \(p=a[p]+k+p>n\) 的需要加的次数即可.
代码:
#include<bits/stdc++.h>
using namespace std;
const int N=405,M=1e5+5;
int n,m;
int dp[M+1000][N],a[M];
int p,k,num;
int main(){
cin>>n; num=sqrt(n);
for(int i=1;i<=n;i++) scanf("%d",&a[i]);
for(int i=n;i>=1;i--)
for(int j=1;j<=num;j++){
if(i+a[i]+j>n) dp[i][j]=1;
else dp[i][j]=dp[i+a[i]+j][j]+1;
}
cin>>m;
while(m--){
scanf("%d%d",&p,&k);
if(k<=num) printf("%d\n",dp[p][k]);
else{
int res=0;
while(p<=n) res++,p=a[p]+k+p;
printf("%d\n",res);
}
}
return 0;
}
CF1039D You Are Given a Tree
题意:
给定一棵树,求树上拥有 \([1,n]\) 个节点的链的个数(一个点只能处于一条链上).
分析:
看数据范围,我们又可以用这个根号分治的方法了!
这里求链上的节点个数可以使用 贪心 的方法:
首先预处理出来整个图的 逆 \(dfs\) 序和每个点的父亲. 通过儿子更新父亲, 无递归过程而且不重复枚举(优美的卡常).
如何判断这个节点所在的链是否包含 \(num\) 个节点的链:
这个节点两个不同儿子对应子链包含点数加和 \(\geq num\)
-
可行,那么就把答案增加 \(1\) ,并且标记此点不能再用.
-
否则,更新这个点 儿子对应子链节点数最大值
进行两部分的时间复杂度分析:
设预处理次数为 \(T\) , 而且答案对于节点数的增加是单调递减的,因此 枚举阶段 可以二分答案计算.
预处理: \(O(nT)\) , 枚举: \(P(\frac\large{n^2 \log n}{T})\) (二分和搜索)
然后根据数学知识,这俩值相同时,加和最小. 所以 \(T=\sqrt{n\log n}\)
二分时注意事项:
我们判断的是: \([i,lim]\) 区间的答案,所以下一个区间 \(i=lim+1\) 然后接下来的左端限制为 \(i\).... 可能语言读起来比较奇怪,还是看代码吧,这一点写的时候改了好几回.
代码:
#include<bits/stdc++.h>
#define pii pair<int,int>
#define mk make_pair
using namespace std;
const int N=1e5+5;
int n,m,cnt,res,lim;
vector<int> g[N];
int f[N],dfn[N],ans[N],fa[N];
void dfs(int x,int last){
for(auto y:g[x]){
if(y==last) continue;
fa[y]=x; dfs(y,x);
}
dfn[++cnt]=x;
}
int solve(int num){//判断是否包含 num 个节点的链: 两个不同儿子对应子链包含点数加和>=num
int res=0; for(int i=1;i<=n;i++) f[i]=1;
for(int i=1;i<=n;i++){//根据dfs序,不可能情况重复
int y=dfn[i],x=fa[y];
if(!x||f[y]==-1||f[x]==-1) continue;
if(f[x]+f[y]>=num) res++,f[x]=-1;//标记此点不能再用
else f[x]=max(f[x],f[y]+1);
}
return res;
}
int main(){
cin>>n; m=sqrt(n*log2(n));//小优化
for(int i=1,x,y;i<n;i++){
scanf("%d%d",&x,&y); g[x].push_back(y); g[y].push_back(x);
}
dfs(1,0); ans[1]=n;
for(int i=2;i<=m;i++) ans[i]=solve(i);
for(int i=m+1;i<=n;i=lim+1){//答案是单调的
int now=solve(i),l=i,r=n;
while(l<=r){
int mid=l+r>>1;
if(solve(mid)==now) l=mid+1,lim=mid;
else r=mid-1;
}
for(int j=i;j<=lim;j++) ans[j]=now;
}
for(int i=1;i<=n;i++) printf("%d\n",ans[i]);
// system("pause");
return 0;
}