根号分治

前言

根号分治,其实就是以根号级别的阈值分界,用两种不同的方法(大部分是暴力)并在正确的复杂度内求解问题的方法。

简而言之,就是把两种暴力放一块,各自求解各自擅长的数据范围。

详解几道例题说明。

直接根号分治

CF1207F Remainder Problem

题干

有两种暴力思路:

维护当前下标模\(x\)\(y\)之和,但局限于空间,仅能处理模数较小的情况。

每次询问直接暴力找所有符合条件的下标对应的值,复杂度为\(O(n/k)\),模数越小越劣。

我们发现两种方法针对不同情况,因此我们各自让他们做他们自己擅长的事就好了。

设阈值为\(t\),当询问的模数小于\(t\)时,用第一种方法,时间复杂度\(O(1)\),空间复杂度\(O(t^2)\)
当模数大于\(t\)时,用第二种方法,时间复杂度\(O(n/t)\)(最坏情况)。

因此当阈值取\(\sqrt{n}\)时,空间时间达到最优。

code
#include<iostream>
#include<string.h>
#include<string>
#include<vector>
#include<bitset>
#include<set>
#include<unordered_map>
#include<queue>
#include<math.h>
#include<algorithm>
#include<iomanip>
using namespace std;
#define ll long long
const int maxn=5e5+5;
const int B=710;
ll a[maxn],b[B+2][B+2];
int q;
signed main()
{
    ios::sync_with_stdio(0);
    cin.tie(0),cout.tie(0);
    cin>>q;
    for(int i=1;i<=q;i++)
    {
        int op,x,y;
        cin>>op>>x>>y;
        if(op==1)
        {
            a[x]+=y;
            for(int j=1;j<=B;j++) b[j][(x%j)]+=y;
        }
        else
        {
            if(x<=B) cout<<b[x][y]<<'\n';
            else
            {
                ll ret=0,sum=y;
                while(sum<=5e5)
                {
                    ret+=a[sum];
                    sum+=x;
                }
                cout<<ret<<'\n';
            }
        }
    }
    return 0;
}

根号分治与数论

CF710D Two Arithmetic Progressions

题干

这道题当然可以直接用\(\mathcal{exgcd}\)求解,这里我们换一种思路,采用根号分治方法解决。

\(a_1\)为题目给出的\(a_1,a_2\)的较大值,设阈值\(t\)\(\sqrt{2e9}\)

\(a_1<t\)时,此时\(a_2<t\)也成立,我们对于第一个等差数列的\(k\)进行枚举,从\(0\)\(a_2\)(我们要求解一个 \(a_1*k-b \equiv 0\ mod\ a_2\) ,所以每\(a_2\)一个循环节),找不到则无解,否则对于找到的\(x\)进行加减\(lcm(a_1,a_2)\)的操作,使得\(l,k>0,x>=L,x<=R\)\(x\)最小,若找不到则无解,否则再以\(lcm(a_1,a_2)\)为循环节,循环节个数\(+1\)(最开始找到的解),为答案。

\(a_1>=t\)时,显然枚举所有的\(k\)的数量超不过\(\sqrt{n}\),所以直接枚举,再在第二个式子判断成立即可。

code
#include<iostream>
#include<string.h>
#include<string>
#include<vector>
#include<bitset>
#include<set>
#include<unordered_map>
#include<queue>
#include<math.h>
#include<algorithm>
#include<iomanip>
using namespace std;
#define int long long
const int B=45000;
int a1,b1,a2,b2,l,r;
int gcd(int x,int y){return y?gcd(y,x%y):x;}
signed main()
{
    ios::sync_with_stdio(0);
    cin.tie(0),cout.tie(0);
    cin>>a1>>b1>>a2>>b2>>l>>r;
    if(a1<a2) swap(a1,a2),swap(b1,b2);
    if(a1>B)
    {
        int cnt=0;
        for(int i=b1;i<=r;i+=a1)
        {
            if(i>=l&&i>=b2)
            {
                if((i-b2)%a2==0) cnt++;
            }
        }
        cout<<cnt;
    }
    else
    {
        for(int i=0;i<=a2;i++)
        {
            int p=a1*i+b1;
            if(abs(p-b2)%a2==0)
            {
                int gdd=gcd(a1,a2);
                int lcm=a1/gdd*a2;
                if(p-b2<0)
                {
                    int cnt=ceil((double)(b2-p)/lcm);
                    p+=cnt*lcm;
                }
                if(p<l)
                {
                    int cnt=ceil((double(l-p)/lcm));
                    p+=cnt*lcm;
                }
                if(p>r) cout<<"0\n";
                else cout<<(r-p)/lcm+1<<'\n';
                return 0;
            }
        }
        cout<<"0\n";
    }
    return 0;
}

根号分治与图论

交友问题

P8250

分析题目,实际上是在求两点的公共邻点的个数,用\(u\)的总邻点个数减去它即为答案。

考虑用\(bitset\)高效维护交并问题,但数据范围达\(2e5\),即使\(bitset\)不怎么占空间,但面对\(4e10\)大小还是开不下的。

所以考虑根号分治,我们发现点的度总和为\(min(2m,n^2)\),度数大于\(\sqrt{n}\)的点不过就\(\sqrt{n}\)个,所以只对这些点维护\(bitset\),而对于其他点直接遍历邻点即可。

时间复杂度为\(O(n\sqrt{n})\)

code
#include<iostream>
#include<string.h>
#include<string>
#include<vector>
#include<bitset>
#include<math.h>
#include<algorithm>
using namespace std;
int n,m,q;
const int B=450;
vector<int> e[(int)2e5+5];
bitset<(int)2e5+2> a[B+2];
int in[(int)2e5+5],pos[(int)2e5+5],cnt;
signed main()
{
    ios::sync_with_stdio(0);
    cin.tie(0),cout.tie(0);
    cin>>n>>m>>q;
    for(int i=1;i<=m;i++)
    {
        int l,r;
        cin>>l>>r;
        e[l].push_back(r);
        e[r].push_back(l);
        in[r]++;
        in[l]++;
    }
    for(int i=1;i<=n;i++)
    {
        if(in[i]>B)
        {
            pos[i]=++cnt;
            for(int j:e[i]) a[cnt][j]=1;
        }
    }
    for(int i=1;i<=q;i++)
    {
        int x,y;
        cin>>x>>y;
        if(in[x]>B&&in[y]>B)
        {
        	a[pos[y]][y]=1;
            int ret=(a[pos[x]]^(a[pos[x]]&a[pos[y]])).count();
            a[pos[y]][y]=0;
            cout<<ret<<'\n';
        }
        else if(in[x]>B)
        {
            bitset<(int)2e5+2> op;
            for(int j:e[y]) op[j]=1;
            op[y]=1;
            int ret=(a[pos[x]]^(a[pos[x]]&op)).count();
            cout<<ret<<'\n';
        }
        else if(in[y]>B)
        {
            bitset<(int)2e5+2> op;
            for(int j:e[x]) op[j]=1;
            a[pos[y]][y]=1;
            int ret=(op^(op&a[pos[y]])).count();
            a[pos[y]][y]=0;
            cout<<ret<<'\n';
        }
        else
        {
            bitset<(int)2e5+2> op1,op2;
            for(int j:e[x]) op1[j]=1;
            for(int j:e[y]) op2[j]=1;
            op2[y]=1;
            int ret=(op1^(op1&op2)).count();
            cout<<ret<<'\n';
        }
    }
    return 0;
}

P5901 [IOI 2009] Regions

P5910

询问对于每个颜色为\(r_1\)的点的子树里有多少个颜色为\(r_2\)的点,答案累加输出。

我们发现可以按各个颜色出现的数量将颜色分为重颜色与轻颜色,重颜色出现次数大于\(\sqrt{n}\),因此重颜色个数也就在\(\sqrt{n}\)范围内。

所以我们可以预处理出所有关于重颜色的贡献。

\(down_{i,j}\)表示颜色\(i\)的子树中有多少重颜色\(j\)
与此对应的\(up_{i,j}\)表示颜色\(i\)的父链上有多少重颜色\(j\)

考虑如何预处理,进行\(dfs\)时对以上两个数组维护两个计数器。

求父链的贡献,进入节点更新计数器,离开时撤销,这样计数器始终维护链上信息。

求子树贡献,只需更新计数器,不撤销,进入节点前与离开节点时的计数器之差即为子树贡献。

预处理时间\(O(n\sqrt{n})\)

再考虑询问两个轻颜色间的贡献。(设父颜色点集为\(i\),子颜色点集为\(j\)
只需要满足\(dfn_i<=dfn_j<=out_j\)即可,拆成两段:\(dfn_j<=out_i\)的个数减去\(dfn_i>dfn_j\)的数量即可。

二维偏序,可以归并排序,但不用,只需要在\(dfs\)过程中将各点放到一个\(vector\)中,就可以自然维护大小关系,求解双指针即可。(可结合代码理解)

code
#include<iostream>
#include<string.h>
#include<string>
#include<vector>
#include<bitset>
#include<math.h>
#include<algorithm>
#include<ctime>
using namespace std;
const int maxn=2e5+5;
const int maxr=2.5e4+5;
const int B=450;
int n,r,q;

// bool ST;
vector<int> e[maxn];//边
vector<int> col[maxr];//各颜色有谁
int a[maxr],cnt;//重颜色对应下标
int down[maxr][B+2],c1[B+2];//各颜色子树有多少重颜色,及计数器
int up[maxr][B+2],c2[B+2];//各颜色父链有多少重颜色,及计数器
int c[maxn];//每点颜色
int dfn[maxn],out[maxn],tot;//dfn序
vector<int> df[maxr],ou[maxr];
// bool ED;

void dfs(int x)
{
    for(int i=1;i<=cnt;i++) up[c[x]][i]+=c2[i];
    if(a[c[x]])
    {
        c2[a[c[x]]]++;
        c1[a[c[x]]]++;
    }
    dfn[x]=++tot;
    df[c[x]].push_back(x);
    for(int j=1;j<=cnt;j++) down[c[x]][j]-=c1[j];
    for(int i:e[x])
    {
        dfs(i);
    }
    for(int j=1;j<=cnt;j++) down[c[x]][j]+=c1[j];
    if(a[c[x]]) c2[a[c[x]]]--;
    out[x]=tot;
    ou[c[x]].push_back(x);
}
signed main()
{
    // clock_t start=clock();
    ios::sync_with_stdio(0);
    cin.tie(0),cout.tie(0);
    // freopen("P5901_26.in","r",stdin);
    // freopen("P5901_26.out","w",stdout);
    cin>>n>>r>>q;
    cin>>c[1];
    col[c[1]].push_back(1);
    for(int i=2;i<=n;i++)
    {
        int op;
        cin>>op;
        e[op].push_back(i);
        cin>>op;
        c[i]=op;
        col[op].push_back(i);
    }
    for(int i=1;i<=r;i++)
    {
        if(col[i].size()>B) a[i]=++cnt;
    }
    dfs(1);
    while(q--)
    {
        int x,y;
        cin>>x>>y;
        if(a[y]) cout<<down[x][a[y]]<<endl;
        else if(a[x]) cout<<up[y][a[x]]<<endl;
        else
        {
            long long ret=0;
            for(int i=0,j=0;i<(int)ou[x].size();i++)
            {
                while(j<(int)df[y].size()&&dfn[df[y][j]]<=out[ou[x][i]]) j++;
                ret+=j;
            }
            for(int i=0,j=0;i<(int)df[x].size();i++)
            {
                while(j<(int)df[y].size()&&dfn[df[y][j]]<dfn[df[x][i]]) j++;
                ret-=j;
            }
            cout<<ret<<endl;
        }
    }
    // clock_t end=clock();
    // cerr<<static_cast<double>(end-start)/CLOCKS_PER_SEC;
    // cerr<<(&ED-&ST)/1024.0/1024.0;
    return 0;
}
posted @ 2026-08-25 20:01  wang7979  阅读(11)  评论(0)    收藏  举报