根号分治

根号分治:

引入:

有这样一类问题:有 \(n\) 个序列,\(m\) 个询问,存在两种做法:\(O(n^2)\) 预处理和 \(O(mn)\) 的不预处理.

显然,两种方法的复杂度都无法接受,因此考虑一种方法是否能平衡这种复杂度。

然后,就拥有了 根号分治 这种方法,思路和 分块的整块处理块和枚举处理 类似

一般来说,根号分治的题目可以分为 预处理阶段枚举阶段

分析:

根据一道题目引入:P3396 哈希冲突

题意:

给定 \(n\) 长序列,\(m\) 个操作:

A x y 询问在序列下标模 \(x\) 时,余数为 \(y\) 的下标的对应的值的加和
C x y 把序列第 \(x\) 个数的值替换成 \(y\)

解决:

我们有两种想法:

  1. \(O(n^2)\) 处理 模 \(i\) 意义下值为 \(j\) 的答案,每次修改为 \(O(n)\).

  2. 每次询问暴力求 \(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; 
  1. \(k\leq \sqrt{n}\) 直接输出 \(dp[p][k]\)

  2. \(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. 可行,那么就把答案增加 \(1\) ,并且标记此点不能再用.

  2. 否则,更新这个点 儿子对应子链节点数最大值

进行两部分的时间复杂度分析:

设预处理次数为 \(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;
}
posted @ 2021-11-14 21:03  Evitagen  阅读(1254)  评论(1编辑  收藏  举报