经典分治算法

RT,主要介绍一些经典的分治算法

CDQ 分治

经典人类智慧算法。

三维偏序问题

三维偏序是 CDQ 分治的一个经典应用,搭配树状数组可以在 O(nlog2n) 的时间复杂度内解决问题。如果我们枚举每一个元素,然后枚举其他的元素的话,可以在 O(n2) 的时间复杂度解决这个问题,但显然无法满足需求。

我们可以先按照 ai 排序,这样可以保证第 i 个元素前的所有元素均满足第一维限制。这样问题就转换为了二维偏序问题,如果此时再按 bi 排序,就会打乱第一维的顺序,显然不可取,为此,我们引出 CDQ 分治的一个大致模板。

定义 solve(l,r):求出所有 i[l,r] 的元素所受 [l,r] 内其他元素的影响值。显然可以将 solve(l,r) 分为三部分(定义 mid=l+r2)。

  • solve(l,mid)

  • solve(mid+1,r)

  • 求解 [l,mid] 内的元素对于 [mid+1,r] 的元素的影响。

重复上述过程,递归边界是 l=r

根据定义很好解释。我们发现可以下手优化的地方主要在第三部分,按照暴力做法,对于第三部分,我们枚举所有 i[mid+1,r] 内的元素,在 [l,mid] 中查找,定义 m=rl+1,时间复杂度 O(m2),有继续优化的空间。

我们发现,对于任何 i[l,mid],j[mid+1,r] 都始终满足 aiaj,因为序列最初是有序的,分治只在每一个区间内进行,所以不影响整体的单调性。所以说,即使将两个区间分别按照 bi 的值升序排序,上述性质依然成立。

我们定义两个指针 i=l,j=mid+1,每当 jj+1i 就不断移动直到第一次移动到 bi>bj,因为我们已经按照 bi 的值升序排序,所以 i,j 的移动均是单调的,且每次两个指针移动后满足任何 k[l,i1] 均满足 akaj,bkbj。此时只差第三维限制。

我们思考平时解决二维偏序问题主要依靠排序和树状数组,利用树状数组限制第二维,而此时我们也可以在三维偏序中使用树状数组来限制第三维。具体做法i 指针每移动一次,就将一个新的元素加入树状数组,在 ci 的位置上加 1,每当查询时,就访问 [0,cj] 的和。这样我们就解决这个问题了。

时间复杂度:CDQ 分治 一般是子问题的时间复杂度乘上 O(logn) ,就上面三维偏序问题来说,定义 m=rl+1,排序为 O(mlogm),指针扫描加树状数组为 O(mlogm),所以最终时间复杂度为 O(nlog2n)

同时,还存在几个小问题。

  • CDQ 分治的一个特点是:在每次分治求解一个小区间时,不会涉及该区间外的部分,仅仅与当前小区间长度有关,所以在使用完树状数组后,不可以暴力清空,而应该还原原本的过程,以保证时间复杂度。

  • 对于重复元素之间有贡献的,CDQ 分治无法直接解决,一个技巧是可以将元素离散化再求解。

  • CDQ 分治的分治过程类似并归排序,所以在将序列按照 bi 排序时,可以用并归排序替代。

贴出核心代码:

void merge(int l,int mid,int r){
    int i=l,j=mid+1,t=l;
    while(i<=mid||j<=r){
        if(j>r||(i<=mid&&cmp2(q[i],q[j])))book[t++]=q[i++];//cmp2指按b[i]排序
        else book[t++]=q[j++];
    }
    for(int i=l;i<=r;i++)q[i]=book[i];
    return;
}//并归排序
void solve(int l,int r){
    if(l==r)return;//递归边界,直接返回
    int mid=(l+r)>>1,t=l;
    solve(l,mid),solve(mid+1,r);//分治求解
    for(int i=mid+1;i<=r;i++){//指针扫描
        while(q[t].y<=q[i].y&&t<=mid)add(q[t].z,q[t].cnt),t++;//移动指针的同时,注意指针移动的范围,q[t].cnt指离散化前这种元素的数量
        q[i].ans+=ask(q[i].z);//更新答案
    }
    for(int i=l;i<t;i++)add(q[i].z,-q[i].cnt);//还原现场,注意是 i<t,以免多减
    return merge(l,mid,r),void();//并归排序
}

整体二分

没学不会。

点分治

点分治(又称淀粉质),通常用于统计树上路径。先看一道典题。

Tree

本题要求统计路径长度不超过 k 的路径数量,如果暴力统计,时间复杂度就会 TLE,思考优化的策略。

我们先选一个树中的节点 R 为这棵树的根节点,显然所有符合条件的路径可分为两类。

  • 经过点 R,两端位于其两个不同的子节点的子树内。

  • 整条路径R 的某个子节点的子树内。

我们发现这种路径很适合分治统计,很简单的就可以设计出分治算法的雏形。

  1. 首先先选择树中一点 R,然后统计经过 R 的第二类路径。

  2. 接着将 R 删除,使原本的一棵树变为若干棵树,再递归统计这些树中「路径长度不超过 k 的路径数量」。

注意在代码中的实现删除一般会打上标记,且在后续的统计中不会经过打了标记的节点

我们先探讨第一步,给定一棵树,给定其根节点 R,如何快速统计出经过 R 的第二类路径?我们假定存在一条符合上述条件的路径 xy,则他们必然满足:

  • x,y 分别属于 R 的不同子节点的子树

  • Rx,Ry距离和不超过 k

我们遍历整棵树,求出树中每个节点 x 的两个信息,分别是:输入根节点的那个儿子的子树(记为 bx),到根节点的距离(记为 dx)。合法路径条数其实就是在统计 bxby,dx+dyk 的无序点对 (x,y) 的数量。

下面给出一种目前比较受欢迎的做法。


我们先不考虑 bxby 的限制条件,只求出 dx+dyk 的对数,我们记其为 calc(R)。我们可以先以 R 为根遍历整棵树,把所有的 dx 求出来,然后用树状数组或双指针快速求解。

接下来做的就是要减去不合法的路径,设 Rh 个儿子,分别是 s1,s2,,sh,如果路径的两个端点 (x,y) 都在 si 的子树中,则他们必然满足 dis(si,x)+dis(si,y)k2×dis(R,si)dis(u,v) 表示两点之间的距离)。

示意

如上图,我们发现,统计中类似蓝色路径(所要求的不合法路径)的数量其实就是统计中类似黄色路径的数量,唯一不同的是蓝色路径统计的是在 R 中, dx+dyk 的数量,而黄色路径则统计的是在 si 中,dx+dyk2×dis(R,si),我们只需要先令答案加上 calc(R),再挨个访问一遍 si,令答案减小 calc(si) 即可。具体的一些小问题看代码解决。

这种做法基于容斥原理,优势是易拓展,可以延伸到三维限制的统计中。劣势是时间复杂度稍差,理论上常数翻倍,实际时间上影响并不是很大。可以对比这一份这一份的差异,前者是其他做法,没有用到容斥原理。

注意在执行 calc(R) 时,需注意 R 为合法路径端点时的情况。


在统计完第二类路径后,我们将节点 R 打上删除的标记,然后分别递归处理 R 各个子节点的子树。对于每颗需要统计的树,我们在分治时,都会选择一个分治中心,围绕这个中心统计路径,分治求解。

假设我们在执行完 calc(R)后,选择了 Ri 作为处理 si 子树的新一轮分治中心。那么 Ri 应当是子树中的那个点?如果单纯的选择 si 当做处理 si 子树的分治中心,那么恭喜,仍然喜提 TLE。当树退化成一条链时,时间复杂度会卡到 O(n2)

对于这个问题,我们可以选择子树的重心作为树的分治点,树重心有一个很好性质,就是删除重心后,原本的树分裂形成的新的树大小均不超过原树的一半,这相当于把分治的规模对半砍了一刀。

画出分治时的递归树,对于递归树中同一层深度分治函数所执行的 calc() 的扫描总共覆盖了整棵树一遍,所以递归树中同一层深度的分治函数总时间复杂度为 O(nlogn),因为选择树的重心使得每一层分治和上一层相比规模减半,因此树高控制在 logn 这个级别,总时间复杂度 O(nlog2n)

搞定了这个,我们就完美的把这个问题解决了。

#include <iostream>
#include <cstdio>
#include <algorithm>
using namespace std;
const int N=4e4+10;
int n,idx,ver[N],to[2*N],nxt[2*N],val[2*N],k,siz[N],maxn[N],root,vis[N],cnt,b[N],ans,a[N];
void add(int x,int y,int z){
    to[++idx]=y,nxt[idx]=ver[x],ver[x]=idx,val[idx]=z;
}
int dfs_root(int x,int fa,int sum){
    siz[x]=1,maxn[x]=0;
    for(int i=ver[x];i;i=nxt[i]){
        if(vis[to[i]]||to[i]==fa)continue;
        siz[x]+=(siz[to[i]]=dfs_root(to[i],x,sum));
        maxn[x]=max(maxn[x],siz[to[i]]);
    }
    maxn[x]=max(maxn[x],sum-siz[x]);
    if(maxn[x]<maxn[root])root=x;
    return siz[x];
}//找树的重心
void dfs_dis(int x,int fa,int from,int len){
    a[++cnt]=len;
    for(int i=ver[x];i;i=nxt[i]){
        if(vis[to[i]]||to[i]==fa)continue;
        dfs_dis(to[i],x,from,len+val[i]);
    }
}//获得每个节点的信息
int calc(int u,int dist){
    a[cnt=1]=dist;//初始化以u为端点
    for(int i=ver[u];i;i=nxt[i]){
        if(vis[to[i]])continue;
        dfs_dis(to[i],u,to[i],dist+val[i]);
    }
    sort(a+1,a+1+cnt);
    int l=1,r=cnt,sum=0;
    while(l<r){
        while(a[l]+a[r]>k&&l<r)r--;
        sum+=(r-l),l++;
    }
    return sum;
}
void solve(int u){
    vis[u]=1;
    ans+=calc(u,0);
    for(int i=ver[u];i;i=nxt[i]){
        if(vis[to[i]])continue;
        ans-=calc(to[i],val[i]);//容斥
        root=0,dfs_root(to[i],u,siz[to[i]]);//递归分治
        solve(root);
    }
    return;
}
int main(){
    scanf("%d",&n);
    for(int i=1,u,v,w;i<n;i++){
        scanf("%d %d %d",&u,&v,&w);
        add(u,v,w),add(v,u,w);
    }
    scanf("%d",&k);
    maxn[0]=N;//注意初始化
    dfs_root(1,-1,n);
    solve(root);
    printf("%d\n",ans);
    return 0;
}
posted @   zuoqingyuan111  阅读(11)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 使用C#创建一个MCP客户端
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现
点击右上角即可分享
微信分享提示