树的最长路径

树的最长路径

题目描述

树的最长路径_第1张图片


样例解释

树的最长路径_第2张图片


前置知识

树的直径,又称树的最长链,定义为一棵树上最远的两个节点的路径,即树上一条不重复经过某一条边的最长的路径。树的直径也可以代指这条路径的长度。

求树的直径有两种比较常用的方法:树形DP和搜索。

如何找树的直径呢?

  1. 任取一点作为起点,找到距离该点最远的一个点u。 这一步可以用DFS或者BFS来解决。
  2. 再找到距离点u最远的一个点v。这一步可以用DFS或者BFS来解决。
  3. 那么点u到点v之间的路径就是一条直径了。

如何证明点u到点v之间的路径就是一条直径了呢?

要想证明点u到点v之间就是一条直径,得证明出从点u出发到达点v得路径是最长。

树的最长路径_第3张图片


核心思路

  • 任取一点u,从点u向下搜索,返回时搜集边的权值,记录两条路径:
  • d1:记录从点u向下走的最长路径的长度
  • d2:记录从点u向下走的次长路径的长度
  • d [ u ] = d 1 + d 2 d[u]=d1+d2 d[u]=d1+d2:表示悬挂在点u上的最长路径的长度,也就是穿过点u的最长路径的长度。
  • 因为点u是任取的,所以遍历完树中的所有节点后,会得到一组 d [ i ] d[i] d[i],答案就是 a n s = m a x ( d [ i ] ) ans=max(d[i]) ans=max(d[i])
  • 但是我们可以不需要开 d [ i ] d[i] d[i]数组,只要我们每次遍历完一个节点,及时更新全局变量ans即可,即 a n s = m a x ( a n s , d 1 + d 2 ) ans=max(ans,d1+d2) ans=max(ans,d1+d2)
  • 从整棵树的根节点出发,从父节点依次到叶子节点,再从叶子节点回到父节点,最终回到整棵树的根节点。总是在返回时收集子树的信息(这里是边的权值),从小到大更新每个节点的信息d[u],更新最大值。这就是在树上做DP。
    树的最长路径_第4张图片

以节点1作为根节点,得到的搜索过程如下:

树的最长路径_第5张图片

以节点2为根节点,得到搜索过程如下:

树的最长路径_第6张图片

如何理解开的bool st数组呢?

树的最长路径_第7张图片

为什么dfs函数的返回类型是int,但是我们在主函数中并没有设置变量来接收这个返回值呢?

因为dfs函数它的返回值是d1,含义是返回从节点u往下走的最大长度,但是d1并不是我们想要求的树的最长路径的长度,ans才是我们想要求的,并且我们在dfs函数也已经求出来了。所以我们并不需要d1的值,也就不需要设置变量来接收dfs的返回值啦。当然如果你想设置变量来接收也可以,只不过它的返回值是d1而不是说ans。


代码

写法1

#include
#include
#include
using namespace std;
const int N=10010,M=N*2;
int n;
int h[N],e[M],ne[M],w[M],idx;
bool st[N]; //标记点是否被搜过,防止向上搜索
int ans;//记录树的最长路径的长度。
//邻接表存边
void add(int a,int b,int c)
{
    e[idx]=b,w[idx]=c,ne[idx]=h[a],h[a]=idx++;
}

int dfs(int u)
{
    //由于我们需要记录的是从u这个点向下搜的最长路径,比如u->x->v,权值为4,8,那么为4+8=12
    //但是由于无向边被我们看做是方向相反的两条有向边,回溯时有可能把从下到上的那种有向边的权值都算进去了
    //即4+8+8+4=24,但是我们只需要记录从上到下的路径长度,不需要回溯时的长度,所以,我们要开一个bool数组
    //来记录节点是否已经被遍历过了,这样再回溯时就不会加上那种从下往上的有向边的权值了。
    st[u]=true;//标记u这个点已经被搜过了,防止往上搜
    int d1=0;   //记录从u节点往下走的最大长度
    int d2=0;   //记录从u节点往下走的次大长度
    for(int i=h[u];~i;i=ne[i])  //i是边的编号
    {
        int j=e[i]; //j是u的邻接点
        //如果j这个点已经被遍历过了,则跳过,避免向上查找
        //有可能j是u的父节点,那么j也是u的邻接点,但是由于我们是记录从上往下的路径长度,所以不能
        //又回去遍历上面的,这样就又重复记录权值了
        if(st[j])
        continue;
        //d是记录从节点u经过节点j往下走的最大长度
        int d=dfs(j)+w[i];
        //如果d的长度大于等于d1的长度
        if(d>=d1)
        {
            d2=d1;//要先把原来的最大长度传给次大长度,更新现在新的次大长度
            d1=d;//更新现在新的最大长度
        }
        //否则如果d的长度大于d2的长度(也可以写成大于等于),则只能更新次大长度
        //注意这里要写成else if 而不能写成if 因为首先最大长度d1肯定是大于次大长度d2的
        //如果d>=d1,则说明d>d2,如果写成if,那么可以进入第一个if语句,还能进入第二个if语句,又更新了d2,这样就错了
        else if(d>d2)
        d2=d;   //更新次大长度
    }
    ans=max(ans,d1+d2); //更新答案
    return d1;  //返回从节点u往下走的最大长度
}
int main()
{
    memset(h,-1,sizeof h);
    scanf("%d",&n);
    //树有n个节点,则有n-1条边
    for(int i=0;i

写法2

#include
#include
#include
using namespace std;
const int N=10010,M=N*2;
int n;
int h[N],e[M],ne[M],w[M],idx;
int ans;//记录树的最长路径的长度。
//邻接表存边
void add(int a,int b,int c)
{
    e[idx]=b,w[idx]=c,ne[idx]=h[a],h[a]=idx++;
}

//father是节点u的父节点
int dfs(int u,int father)
{
    //由于我们需要记录的是从u这个点向下搜的最长路径,比如u->x->v,权值为4,8,那么为4+8=12
    //但是由于无向边被我们看做是方向相反的两条有向边,回溯时有可能把从下到上的那种有向边的权值都算进去了
    //即4+8+8+4=24,但是我们只需要记录从上到下的路径长度,不需要回溯时的长度,所以,我们要开一个bool数组
    //来记录节点是否已经被遍历过了,这样再回溯时就不会加上那种从下往上的有向边的权值了。
    int d1=0;   //记录从u节点往下走的最大长度
    int d2=0;   //记录从u节点往下走的次大长度
    for(int i=h[u];~i;i=ne[i])  //i是边的编号
    {
        int j=e[i]; //j是u的邻接点
        //如果j这个点已经被遍历过了,则跳过,避免向上查找
        //有可能j是u的父节点,那么j也是u的邻接点,但是由于我们是记录从上往下的路径长度,所以不能
        //又回去遍历上面的,这样就又重复记录权值了
        if(j==father)
        continue;
        //d是记录从节点u经过节点j往下走的最大长度
        int d=dfs(j,u)+w[i];
        //如果d的长度大于等于d1的长度
        if(d>=d1)
        {
            d2=d1;//要先把原来的最大长度传给次大长度,更新现在新的次大长度
            d1=d;//更新现在新的最大长度
        }
        //否则如果d的长度大于d2的长度(也可以写成大于等于),则只能更新次大长度
        //注意这里要写成else if 而不能写成if 因为首先最大长度d1肯定是大于次大长度d2的
        //如果d>=d1,则说明d>d2,如果写成if,那么可以进入第一个if语句,还能进入第二个if语句,又更新了d2,这样就错了
        else if(d>d2)
        d2=d;   //更新次大长度
    }
    ans=max(ans,d1+d2); //更新答案
    return d1;  //返回从节点u往下走的最大长度
}
int main()
{
    memset(h,-1,sizeof h);
    scanf("%d",&n);
    //树有n个节点,则有n-1条边
    for(int i=0;i

你可能感兴趣的:(算法提高)