首页 > 编程语言 >经典分治算法

经典分治算法

时间:2024-08-19 11:41:43浏览次数:5  
标签:路径 int text 复杂度 分治 mid 算法 经典

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

CDQ 分治

经典人类智慧算法。

三维偏序问题

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

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

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

  • \(\text{solve}(l,mid)\)

  • \(\text{solve}(mid+1,r)\)

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

重复上述过程,递归边界是 \(l=r\)。

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

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

我们定义两个指针 \(i=l,j=mid+1\),每当 \(j\to j+1\),\(i\) 就不断移动直到第一次移动到 \(b_i>b_j\),因为我们已经按照 \(b_i\) 的值升序排序,所以 \(i,j\) 的移动均是单调的,且每次两个指针移动后满足任何 \(k\in[l,i-1]\) 均满足 \(a_k\le a_j,b_k\le b_j\)。此时只差第三维限制。

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

时间复杂度:CDQ 分治 一般是子问题的时间复杂度乘上 \(O(\log n)\) ,就上面三维偏序问题来说,定义 \(m=r-l+1\),排序为 \(O(m\log m)\),指针扫描加树状数组为 \(O(m\log m)\),所以最终时间复杂度为 \(O(n\log^2 n)\)。

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

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

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

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

贴出核心代码:

点击查看代码
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\) 的第二类路径?我们假定存在一条符合上述条件的路径 \(x\to y\),则他们必然满足:

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

  • \(R\to x,R\to y\) 的距离和不超过 \(k\)。

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

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


我们先不考虑 \(b_x\ne b_y\) 的限制条件,只求出 \(d_x+d_y\le k\) 的对数,我们记其为 \(\text{calc}(R)\)。我们可以先以 \(R\) 为根遍历整棵树,把所有的 \(d_x\) 求出来,然后用树状数组或双指针快速求解。

接下来做的就是要减去不合法的路径,设 \(R\) 有 \(h\) 个儿子,分别是 \(s_1,s_2,\dots,s_h\),如果路径的两个端点 \((x,y)\) 都在 \(s_i\) 的子树中,则他们必然满足 \(\text{dis}(s_i,x)+\text{dis}(s_i,y)\le k-2\times \text{dis}(R,s_i)\)(\(\text{dis}(u,v)\) 表示两点之间的距离)。

示意

如上图,我们发现,统计中类似蓝色路径(所要求的不合法路径)的数量其实就是统计中类似黄色路径的数量,唯一不同的是蓝色路径统计的是在 \(R\) 中, $d_x+d_y\le k $ 的数量,而黄色路径则统计的是在 \(s_i\) 中,\(d_x+d_y\le k-2\times \text{dis}(R,s_i)\),我们只需要先令答案加上 \(\text{calc}(R)\),再挨个访问一遍 \(s_i\),令答案减小 \(\text{calc}(s_i)\) 即可。具体的一些小问题看代码解决。

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

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


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

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

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

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

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

点击查看代码
#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;
}

标签:路径,int,text,复杂度,分治,mid,算法,经典
From: https://www.cnblogs.com/zuoqingyuan/p/18366991

相关文章

  • 粒子群算法和引力搜索算法的混合算法(PSOGSA)优化BP神经网络原理及matlab代码
    目录0引言1数学模型2模型对比3matlab代码3.1伪代码示意图3.2PSOGSA-BP4视频讲解0引言基于已发表智能算法文献研究,SeyedaliMirjalili等人在发现PSO的开发能力与GSA的探索能力有者较好结合性能,因此基于二者算法优势点提出混合算法PSOGSA。该算法主要利用PSO鸟......
  • 粒子群算法和引力搜索算法的混合算法(PSOGSA)优化长短期记忆神经网络原理及matlab代码
    目录0引言1数学模型2模型对比3matlab代码3.1伪代码示意图3.2PSOGSA-LSTM4视频讲解0引言基于已发表智能算法文献研究,SeyedaliMirjalili等人在发现PSO的开发能力与GSA的探索能力有者较好结合性能,因此基于二者算法优势点提出混合算法PSOGSA。该算法主要利用PSO......
  • 粒子群算法和引力搜索算法的混合算法(PSOGSA)优化支持向量机原理及matlab代码
    目录0引言1数学模型2模型对比3matlab代码3.1伪代码示意图3.2PSOGSA-SVM4视频讲解0引言基于已发表智能算法文献研究,SeyedaliMirjalili等人在发现PSO的开发能力与GSA的探索能力有者较好结合性能,因此基于二者算法优势点提出混合算法PSOGSA。该算法主要利用PSO......
  • 莫队算法
    莫队是一种优美的暴力,分为普通莫队、树上莫队、带修莫队、回滚莫队等,但是这个人太菜了导致只会普通莫队。首先,%%%莫涛大神。传奇人物就直接放图吧:stostostostostostostostostostostostostostostoMoTaoorzorzorzorzorzorzorzorzorzorzorzorzorzo......
  • 合成数的高效算法
    数的合成指将多个数字合成一个整数,比如将9、5、2、7合成9527。本文主要讨论的是整数的合成,附带提一下字符型数的合成。一、整数的合成整数合成指的是输入的数字和输出的整数都是以数的形式(即数据类型全部为int型)存储,而不用字符串。有两种算法:1.位值法(看似简单实则复杂低......
  • 叠Buff!经典麻雀优化算法+多重双向深度学习!SSA-BiTCN-BiGRU-Attention多输入单输出回
    叠Buff!经典麻雀优化算法+多重双向深度学习!SSA-BiTCN-BiGRU-Attention多输入单输出回归预测目录叠Buff!经典麻雀优化算法+多重双向深度学习!SSA-BiTCN-BiGRU-Attention多输入单输出回归预测效果一览基本介绍程序设计参考资料效果一览基本介绍1.Matlab实现SS......
  • 【智能算法】回溯算法
    目录一、回溯算法概述二、回溯算法分类2.1组合问题2.2排列问题2.3切割问题2.4子集和问题2.5图论问题2.6其他问题三、回溯算法C语言实现3.1组合问题3.2排列问题3.3切割问题3.4子集和问题3.5图论问题四、回溯算法应用一、回溯算法概述      ......
  • 机器学习:线性回归算法(一元和多元回归代码)
    1、线性回归         1、数据准备:描述如何获取和准备数据。    2、图像预处理:包括图像读取。    3、将数据划分为训练集和测试集。    4、计算数据的相关系数矩阵。    5、模型训练:详细说明如何使用线性回归算法训练模型,包括......
  • 代码随想录算法训练营第11天|二叉树part01
    理论基础需要了解二叉树的种类,存储方式,遍历方式以及二叉树的定义二叉树纯理论方面还是比较简单,以前都学过,没什么可讲的。满二叉树就是满了,完全二叉树就是层满了(而且是左边)。平衡二叉搜索树就是左右深度绝对值差1。一般采用链式存储方式,顺序存储结构如果父节点的数组......
  • 代码随想录算法训练营第10天|栈与队列part02
    150.逆波兰表达式求值本题不难,但第一次做的话,会很难想到,所以先看视频,了解思路再去做题classSolution{public:intevalRPN(vector<string>&tokens){stack<longlong>st;for(conststring&token:tokens){if(token=="+......