首页 > 其他分享 >树分治

树分治

时间:2024-09-29 21:24:05浏览次数:1  
标签:rt 路径 int siz 分治 dp dis

点分治

点分治适合处理大规模的树上路径信息问题。

本质思想是选择一点作为分治中心,将原问题划分为几个相同的子树上的问题,进行递归解决。

常见的用于统计树上有关路径的信息。假设当前选定根节点为 \(rt\),则符合条件的路径必然是以下两种之一:

  1. 经过 \(rt\) 或一段在 \(rt\) 上;
  2. 在 \(rt\) 的子树上。

P3806

P3806【模板】点分治 1

给定一棵树,边有权值。

多次询问,查询树上是否有长度为 \(k\) 的路径。

先随意取一个点作为根节点 \(rt\)。对于经过 \(rt\) 的路径,先枚举其所有子节点 \(v\),以 \(v\) 为根计算 \(v\) 子树中所有节点到 \(rt\) 的距离。

即 \(dis_i\) 为当前节点 \(i\) 到 \(rt\) 的距离,\(pd_{key}\) 为之前处理过的子树中是否存在一个节点 \(v\) 使得 \(dis_v=key\)。

若一个询问的 \(k\) 满足 \(pd_{k-dis_i}=\text{true}\) 则存在一个长度为 \(k\) 的路径。

在计算完 \(v\) 子树中所连的边能否成为答案后,将这些新的距离加入到 \(pd\) 中。

注意清空 \(pd\) 时应将之前占用过的 \(pd\) 位置加入到一个队列中清空,依此来保证时间复杂度。

点分治过程中,每一层的所有递归过程合计对每个点处理一次,假设共递归 \(h\) 层,则总时间复杂度为 \(\mathcal O(hn)\)。

若每次选择子树的重心作为根节点,可以保证递归层数最少,时间复杂度为 \(\mathcal O(n\log n)\)。

const int N=1e4+5;
const int M=1e7+5;
int n,m,q[N],rt,sum,siz[N],dp[N],dis[N],pd[M],ret[N],vis[N],d[N],tot;
vector<tuple<int,int>>G[N];
queue<int>tag;

inline void getroot(int u,int f){
    siz[u]=1;dp[u]=0;
    for(auto[v,w]:G[u])
        if(v!=f and !vis[v]){
            getroot(v,u);
            siz[u]+=siz[v];
            dp[u]=max(dp[u],siz[v]);
        }
    dp[u]=max(dp[u],sum-siz[u]);
    if(dp[u]<dp[rt])
        rt=u;
}

inline void getdis(int u,int f){
    d[++tot]=dis[u];
    for(auto[v,w]:G[u])
        if(v!=f and !vis[v]){
            dis[v]=dis[u]+w;
            getdis(v,u);
        }
}

inline void solve(int u,int f){
    pd[0]=vis[u]=1;
    tag.push(0);
    for(auto[v,w]:G[u])
        if(v!=f and !vis[v]){
            dis[v]=w;
            getdis(v,u);
            for(int k=1;k<=tot;k++)
                for(int i=1;i<=m;i++)
                    if(q[i]>=d[k])
                        ret[i]|=pd[q[i]-d[k]];
            for(int k=1;k<=tot;k++)
                if(d[k]<M)
                    tag.push(d[k]),pd[d[k]]=1;
            tot=0;
        }
    while(tag.size())
        pd[tag.front()]=0,tag.pop();
    for(auto[v,w]:G[u])
        if(v!=f and !vis[v]){
            sum=siz[v];
            dp[rt=0]=inf;
            getroot(v,u);
            solve(rt,u);
        }
}

signed main(){
    sum=n=read();m=read();
    for(int i=1;i<n;i++){
        int u=read(),v=read(),w=read();
        G[u].eb(mt(v,w));G[v].eb(mt(u,w));
    }
    for(int i=1;i<=m;i++)
        q[i]=read();
    dp[rt=0]=inf;
    getroot(1,0);
    solve(rt,0);
    for(int i=1;i<=m;i++)
        puts(ret[i]?"AYE":"NAY");
    return 0;
}

P4178

P4178 Tree

给定一棵树,边有权值。

查询树上长度小于等于 \(k\) 的路径数量。

递归时,对于当前根节点 \(rt\),把每一个子节点到 \(rt\) 的距离排序,然后用类似双指针的方法来求小于等于 \(k\) 的边的数量。

在同一子树内的路径是不合法的,但是同样也会算进答案中。

所以对 \(rt\) 的每一条边进行遍历,利用容斥的思想减去不合法的路径,把经过了从重心到新遍历的点的边两次的路径剪掉,统计答案。

const int N=4e4+5;
int n,k,q[N],rt,sum,siz[N],dp[N],dis[N],ret[N],vis[N],d[N],tot,ans;
vector<tuple<int,int>>G[N];
queue<int>tag;

inline void getroot(int u,int f){
    siz[u]=1;dp[u]=0;
    for(auto[v,w]:G[u])
        if(v!=f and !vis[v]){
            getroot(v,u);
            siz[u]+=siz[v];
            dp[u]=max(dp[u],siz[v]);
        }
    dp[u]=max(dp[u],sum-siz[u]);
    if(dp[u]<dp[rt])
        rt=u;
}

inline void getdis(int u,int f){
    d[++tot]=dis[u];
    for(auto[v,w]:G[u])
        if(v!=f and !vis[v]){
            dis[v]=dis[u]+w;
            getdis(v,u);
        }
}

inline int work(int u,int w){
    tot=0;dis[u]=w;
    getdis(u,0);
    sort(d+1,d+1+tot);
    int l=1,r=tot,res=0;
    while(l<=r)
        if(d[l]+d[r]<=k)
            res+=r-l,l++;
        else
            r--;
    return res;
}

inline void solve(int u,int f){
    vis[u]=1;ans+=work(u,0);
    for(auto[v,w]:G[u])
        if(v!=f and !vis[v]){
            ans-=work(v,w);
            sum=siz[v];
            dp[0]=n,rt=0;
            getroot(v,u);
            solve(rt,u);
        }
}

signed main(){
    n=read();
    for(int i=1;i<n;i++){
        int u=read(),v=read(),w=read();
        G[u].eb(mt(v,w));G[v].eb(mt(u,w));
    }
    k=read();
    dp[rt=0]=inf;
    getroot(1,0);
    solve(rt,0);
    write(ans);
    return c0;
}

P4149

P4149 [IOI2011]

给一棵树,每条边有权。

求一条简单路径,权值和等于 \(k\),且边的数量最小。

递归时,对于当前根节点 \(rt\),计算所有经过 \(r\) 的路径,可以开个桶记录。

具体地,令 \(mn_i\) 为从 \(i\) 开始的权值为 \(i\) 的所有路中边数的最小值。

令 \(dis_i\) 为当前点 \(i\) 到 \(rt\) 的路径长度,\(cnt_i\) 为当前点 \(i\) 到 \(rt\) 的边数。

更新答案时,用当前子树 \(u\) 的 \(dis\) 和前面子树的桶来更新,即 \(ans=\min\limits_{v\in son_u}(ans,cnt_v+mn_{k-dis_v})\)。

const int N=2e6+5;
int n,k,siz[N],vis[N],rt,dp[N],sum,dis[N],d[N],cnt[N],c[N],tot,mn[N],ans;
vector<tuple<int,int>>G[N];

inline void getroot(int u,int fa){
    siz[u]=1;dp[u]=0;
    for(auto[v,w]:G[u])
        if(v!=fa and !vis[v]){
            getroot(v,u);
            siz[u]+=siz[v];
            dp[u]=max(dp[u],siz[v]);
        }
    dp[u]=max(dp[u],sum-siz[u]);
    if(dp[u]<dp[rt])
        rt=u;
}

inline void getdis(int u,int fa){
    if(dis[u]>k)
        return;
    d[++tot]=dis[u];c[tot]=cnt[u];
    for(auto[v,w]:G[u])
        if(v!=fa and !vis[v]){
            dis[v]=dis[u]+w;
            cnt[v]=cnt[u]+1;
            getdis(v,u);
        }
}

inline void work(int u){
    tot=0;mn[0]=0;
    for(auto[v,w]:G[u])
        if(!vis[v]){
            int tmp=tot;
            dis[v]=w;
            cnt[v]=1;
            getdis(v,u);
            for(int i=tmp+1;i<=tot;i++)
                ans=min(ans,mn[k-d[i]]+c[i]);
            for(int i=tmp+1;i<=tot;i++)
                mn[d[i]]=min(mn[d[i]],c[i]);
        }
    for(int i=1;i<=tot;i++)
        mn[d[i]]=inf;
}

inline void solve(int u,int fa){
    vis[u]=1;
    work(u);
    for(auto[v,w]:G[u])
        if(v!=fa and !vis[v]){
            sum=siz[v];rt=0;
            getroot(v,u);
            solve(rt,u);
        }
}

signed main(){
    sum=n=read();k=read();ans=inf;
    for(int i=1;i<n;i++){
        int u=read()+1,v=read()+1,w=read();
        G[u].eb(mt(v,w));G[v].eb(mt(u,w));
    }
    dp[rt=0]=inf;
    memset(mn,0x3f,sizeof(mn));
    getroot(1,0);
    solve(rt,0);
    write((ans>=n)?-1:ans);    
    return 0;
}

P2634

todo.

边分治

todo.

点分树

todo.

标签:rt,路径,int,siz,分治,dp,dis
From: https://www.cnblogs.com/QcpyWcpyQ/p/-/tree_divide

相关文章

  • 揭秘合并排序:分治排序初学者指南
    归并排序由约翰·冯·诺依曼于1945年提出,主要是为了提高大型数据集的排序效率。冯·诺依曼的算法旨在使用分而治之的方法提供一致且可预测的排序过程。这种策略允许归并排序有效地处理小型和大型数据集,保证在所有情况下都能实现稳定的排序,时间复杂度为o(nlogn)。合并排序采用......
  • 洛谷题单指南-分治与倍增-P4155 [SCOI2015] 国旗计划
    原题链接:https://www.luogu.com.cn/problem/P4155题意解读:在m个点的环上,有n个区间,且各个区间之间没有包含关系,计算从每个区间开始,最少要多少个区间能覆盖环上所有m个点。解题思路:本质上是一个区间覆盖问题!1、破环成链由于题目中是一个环,对于环的问题,在区间DP中介绍过,经典处理......
  • 深入分治法:如何用简单步骤解决复杂问题?
    分治法(DivideandConquer)概述分治法是一种经典的算法设计思想,它将一个复杂问题分成多个规模较小的子问题,分别解决这些子问题,然后将子问题的解组合成原问题的解。分治法的思想非常通用,适用于许多经典算法和问题,如归并排序、快速排序、二分搜索、矩阵乘法等。分治法的基本步......
  • 洛谷题单指南-分治与倍增-P3517 [POI2011] WYK-Plot
    原题链接:https://www.luogu.com.cn/problem/P3517题意解读:有n个连续的点p1,p2,...,pn,将这n个点分成不超过m堆,每堆点连续,每一堆都缩成一个点qi,要使得原来的点p1~ps距离qi的最大值最小(最相似),求这个相似度,并计算一共分成几堆,以及每堆缩到的点qi的坐标。解题思路:要使得若干点缩到一......
  • 归并分治
    归并排序912.排序数组#include<iostream>#include<vector>usingnamespacestd;classSolution{public://分治-治voidmerge(vector<int>&arr,intleft,intmid,intright,vector<int>&temp){//[left,mid]和[mi......
  • 洛谷题单指南-分治与倍增-P3509 [POI2010] ZAB-Frog
    原题链接:https://www.luogu.com.cn/problem/P3509题意解读:n个点,每个点上有一只青蛙每次跳到距离自己第k近的点,m次之后跳到哪个点。解题思路:1、计算距离每个点第k近的点,存入ne[N]给定一个点i,距离i第k近的点一定在长度为k+1个点的窗口内,窗口包括i并且,第k近的点只能是左端点或者......
  • Datawhale Leecode基础算法篇 task02:递归算法and分治算法
    官方学习文档:datawhalechina往期task01:枚举算法链接:DatawhaleLeecode基础算法篇task01:枚举算法递归算法递归简介递归(Recursion):指的是一种通过重复将原问题分解为同类的子问题而解决的方法。在绝大数编程语言中,可以通过在函数中再次调用函数自身的方式来实现递归。举......
  • 算法-分治和逆序
    分治法(DivideandConquer)是一种重要的算法设计范式,它通过将复杂的问题分解成更小、更易于管理和解决的子问题,然后递归地解决这些子问题,最后将子问题的解合并以得到原问题的解。分治法通常用于排序、搜索、数学计算和优化等问题。分治法的核心思想可以概括为三个步骤:分......
  • 洛谷题单指南-分治与倍增-P2345 [USACO04OPEN] MooFest G
    原题链接:https://www.luogu.com.cn/problem/P2345题意解读:有n头牛,每头牛都有听力v、坐标x两个属性,要计算每两头牛的max{vi​,vj​}×∣xi​−xj​∣之和。解题思路:首先想到的肯定是枚举法,需要O(n^2)的复杂度有没有优化的方法?可以采用分治法!由于是计算两头牛之间的max{vi​,......
  • 算法学习-CDQ分治
    对于二维偏序,为普通的求逆序对,只需要先排序一遍,然后树状数组或双指针即可而三位偏序甚至更高,则需要用CDQ分治,简单来说,就是将树状数组和双指针结合操作步骤如下:1.开始将数组按第一维排序,保证满足x性质2.在归并中,将左右分别按y排序,因为开始以x排序,所以保证了正确性,保证贡献从左......