初学点分治

本文深入探讨了点分治算法,一种在树形结构中高效解决问题的方法。通过寻找树的重心,将问题分解为更小的子问题,实现时间复杂度的优化。文章详细介绍了点分治的核心思想,以洛谷3806模板题为例,展示了如何在树上应用该算法,并提供了完整的代码实现。

前言

在我的心目中,点分治是一个非常难的算法,但在解决一些树上问题时也非常实用。为此,我特地去学了学点分治这个高深的算法。

什么是树的重心

在学习点分治之前,我们先来解决一个问题:什么是树的重心

在一棵树上找到一个点,使得删去这个点后得到的子树中节点数最大的子树最小,那么这个点就叫做树的重心

那么树的重心具体有什么作用呢?

这在之后会提到。

下面,让我们先来看一看点分治的核心思想。

核心思想

以一道模板题为例:【洛谷3806】【模板】点分治1

第一步,我们要找到一个节点\(rt\),将无根树转化为有根树

不难发现,在这棵树上的路径只有两种:

  1. 经过根节点的路径。

  2. 不经过根节点的路径,则这条路径必定位于当前根节点的某个子树中。

对于第一种情况,我们可以在确定根节点的情况下在\(O(n)\)的时间复杂度下处理掉。

而对于第二种情况,我们就需要继续处理这条路径所在的子树,并重复以上步骤,求出答案

这个时候就不难想到分治了。

我们可以将当前处理到的子树分成若干个小子树,分别求出答案。

这时我们再考虑,\(rt\)取什么节点才会使复杂度最优?

我们可以取树的重心,使得剩下的子树大小尽量平均,时间复杂度也会大大优化,大致为\(O(nlog^2n)\)

代码

#include<bits/stdc++.h>
#define max(x,y) ((x)>(y)?(x):(y))
#define min(x,y) ((x)<(y)?(x):(y))
#define LL long long
#define swap(x,y) (x^=y,y^=x,x^=y)
#define abs(x) ((x)<0?-(x):(x))
#define Fsize 100000
#define tc() (FinNow==FinEnd&&(FinEnd=(FinNow=Fin)+fread(Fin,1,Fsize,stdin),FinNow==FinEnd)?EOF:*FinNow++)
#define N 10000
#define M 100
#define add(x,y,z) (e[++ee].nxt=lnk[x],e[lnk[x]=ee].to=y,e[ee].val=z)
char Fin[Fsize],*FinNow=Fin,*FinEnd=Fin;
using namespace std;
const char ans[2][4]={"NAY","AYE"};
int n,m,rt,top,ee=0,lnk[N+5],Size[N+5],MaxSize[N+5],used[N+5],dis[N+5],Stack[N+5],Exist[10000005];
struct edge
{
    int to,nxt,val;
}e[2*N+5];
struct Query
{
    int Question,Answer;
}q[M+5];
inline void read(int &x)
{
    x=0;static char ch;
    while(!isdigit(ch=tc()));
    while(x=(x<<3)+(x<<1)+ch-48,isdigit(ch=tc()));
}
inline void GetRt(int x,int lst,int sum)//确定根节点,即找到树的重心
{
    register int i;
    for(Size[x]=1,MaxSize[x]=0,i=lnk[x];i;i=e[i].nxt)//初始化当前节点大小为1,最大子树大小为0
    {
        if(used[e[i].to]||!(e[i].to^lst)) continue;//如果当前边连向的节点为根节点,或是上一个访问到的节点,就跳过
        GetRt(e[i].to,x,sum),Size[x]+=Size[e[i].to],MaxSize[x]=max(MaxSize[x],Size[e[i].to]);//对当前边连向的节点进行操作,更新当前子树的大小以及最大子树大小
    }
    if((MaxSize[x]=max(MaxSize[x],sum-Size[x]))<MaxSize[rt]) rt=x;//如果当前节点最大子树节点数比原本找到的重心的最大子树节点数小,就更新重心
}
inline void dfs(int x,int lst)//搜索处理出答案
{
    register int i;
    for(Stack[++top]=dis[x],i=lnk[x];i;i=e[i].nxt)//将到达当前节点的距离加入栈中,枚举与当前节点相邻的每一个节点
        if(e[i].to^lst&&!used[e[i].to]) dis[e[i].to]=dis[x]+e[i].val,dfs(e[i].to,x);//如果当前边连向的节点不是根节点且不是上一个访问到的节点,就继续dfs当前边连向的节点
}
inline void F5(int x,int t)//更新答案
{
    register int i;
    for(i=1;i<=m;++i) if(q[i].Question>=x) q[i].Answer|=!(Exist[q[i].Question-x]^t);//如果存在一条为q[i].Question的路径,就将q[i].Answer更新为1
}
inline void GetAns(int x)//处理以x为根节点的子树
{
    register int i,j;
    for(used[Exist[0]=x]=1,i=lnk[x];i;i=e[i].nxt)//标记当前节点已作为根节点,枚举每一个与当前节点相邻的节点
    {
        if(used[e[i].to]) continue;//如果枚举到的节点已作为根节点,就跳过
        dis[e[i].to]=e[i].val,dfs(e[i].to,0);//dfs一遍,求出根节点到这个子树中每一个节点的距离
        for(j=1;j<=top;++j) F5(Stack[j],x);//对于每一个距离更新答案
        while(top) Exist[Stack[top--]]=x;//将栈中的元素一个个标记为存在
    }
    for(i=lnk[x];i;i=e[i].nxt)//枚举每一个与当前节点相邻的节点
        if(!used[e[i].to]) GetRt(e[i].to,rt=0,Size[e[i].to]),GetAns(rt);//如果枚举到的节点没有被作为过根节点,就对这个子树进行处理
}
int main()
{
    register int i,x,y,z;
    for(read(n),read(m),i=1;i<n;++i) read(x),read(y),read(z),add(x,y,z),add(y,x,z);
    for(i=1;i<=m;++i) read(q[i].Question);
    for(MaxSize[0]=1e9,GetRt(1,rt=0,n),GetAns(rt),i=1;i<=m;++i) puts(ans[q[i].Answer]);//点分治并输出答案
    return 0;
}

转载于:https://www.cnblogs.com/chenxiaoran666/p/DotSolver.html

分治和递归算法是计算机科学中的两种重要算法思想,它们可以帮助我们解决很多复杂的问题。下面我介绍一下这两种算法以及它们在Java中的实现。 1. 分治算法 分治算法是指将一个大问题分成若干个小问题,然后逐个解决这些小问题,最后将所有小问题的解合并起来得到大问题的解。分治算法通常采用递归的方式来实现。 Java代码示例: ```java public int divideAndConquer(int[] nums, int left, int right){ if(left == right) return nums[left]; // 如果数组只有一个元素,则直接返回该元素 int mid = (left + right) / 2; // 将数组分成两部分 int leftSum = divideAndConquer(nums, left, mid); // 递归求解左半部分的和 int rightSum = divideAndConquer(nums, mid + 1, right); // 递归求解右半部分的和 return leftSum + rightSum; // 合并左右两部分的和 } ``` 上面的代码实现了求解一个数组中所有元素的和的问题。首先将数组分成两部分,然后递归地求解左右两部分的和,最后将左右两部分的和合并起来得到整个数组的和。 2. 递归算法 递归算法是指在函数的定义中调用自身的算法。递归算法通常用于解决具有递归结构的问题,比如树和图等数据结构。递归算法需要满足两个条件:基本情况和递归情况。 Java代码示例: ```java public int fibonacci(int n){ if(n <= 1) return n; // 基本情况 return fibonacci(n-1) + fibonacci(n-2); // 递归情况 } ``` 上面的代码实现了求解斐波那契数列的第n项的值的问题。斐波那契数列中的第一项和第二项都是1,从第三项开始,每一项都是前两项的和。递归算法中,基本情况是当n小于等于1时直接返回n,递归情况是求解前两项的和,然后递归求解前两项中的每一项。 希望这些示例代码能够帮助你理解分治和递归算法的基本思想和Java实现方式。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值