算法学习——树形DP——多叉树的最长路径

96 篇文章 10 订阅
30 篇文章 0 订阅

引言

  • 这个问题,每一次都会痛击我,每一次都不会,或者说听懂了,就忘记了,完全不走脑子的,真的!今天就完全整理一下,然后下次多背背
  • 主要分为三个部分,分别是
    • 树的直径公式证明
    • 数组链表表示树
    • 树形DP的具体分析

正文

0、题目的具体内容

在这里插入图片描述
对应的树形结构如下

在这里插入图片描述

1、树的直径定理推导

定理

  • 任取一点u,然后找到距离该点最远的点x,然后再以x为起点,找到距离x最远的点y,然后路径xy就是树的直径。

直径

  • 树中任意两个节点之间最长路径的长度

  • 如果x是直径的某一个端点的话,那么y是距离x最远的点,y肯定也是直径上的一个端点,所以只需要证明x是直径上的一个端点

证明——x是直径的一个端点

  • 这个分两种情况

  • 1、xy和一条直径相交
    在这里插入图片描述

  • 2、xy和一条直径不相交

在这里插入图片描述

3、使用数组链表表示树

使用数组表示链表
  • 使用数组实现链表,为了省时,减少指针转换的耗时
  • 构成如下
    • head:表示链表的头节点
    • 数组e:存储元素
    • 数组ne:ne[idx] = k,当前元素的索引是idx,是第k次插入的元素,idx是k之后的一次,idx和k都是数组e中的索引,通过ne来获取下一个元素的索引位置
    • indx:下一个可以存储元素的位置索引
  • 相关操作具体实现
    • 头插法
      • 在存储元素的数组e中插入对应的元素,e[idx] = x;
      • 将该元素插入到头节点后面,ne[idx] = head;
      • 头节点指向该元素,head = idx;
      • idx指向下一个可以存储元素的位置,idx ++
    • 在索引k后插入一个数
      • 在e的idx存储元素,e[idx] = x;
      • 该元素插入到第k个插入的元素后面,ne[idx] = ne[k];
      • 第k个插入的数,指向该元素 ne[k] = idx
      • idx向后移动
    • 删除索引为k的元素的后一个元素
      • ne[k] 的值更新为ne[ne[k]];
数组表示单链表头插法演示

在这里插入图片描述

  • 创建对应的头节点
    在这里插入图片描述

  • 头指针修改转向

在这里插入图片描述

  • 再次使用头插法,插入一个新的节点6,下面的head值是0

在这里插入图片描述

  • 修改指针的指向

在这里插入图片描述

  • 通过上述流程可以更加清晰的认识到数组ne的作用,就是保存的是下一个节点的索引,所以遍历这一类链表也是通过ne进行遍历的。

  • 下面就多添加几个节点,对这个数据结构有更加清晰的认知。

在这里插入图片描述

关于这个数据结构重点掌握的就是一下几点

  • 数组e用来存储节点的数据
  • 数组ne用来存储当前索引表示的节点,在逻辑结构上的下一个节点的值
  • 索引idx是下一个插入的位置,
数组表示单链表在索引k出插入一个数字
  • 以下图中的链表为例子,在k = 2的地方插入节点4,下面还是分别从传统链表和数组链表两个角度出发,去演示
    在这里插入图片描述

  • 新建节点

在这里插入图片描述

  • 修改对应的连接

在这里插入图片描述

  • 这里遍历的方向和链表不一致,在数组中是对应第k个元素,然后在链表中第k个节点,不一样。

  • 模板记忆如下

ne(idx) = ne(k);  // 告诉我第k个节点的后继节点的索引
ne(k) = idx;  // 将当前节点坐标保存为目标数组的后继节点
idx ++;
数组表示链表实现代码
const int N = 100010;
int head,e[N],ne[N],idx = 0;  

void init(){
	head = -1;
	idx = 0;
}

// 并没有必要单独写一个尾插法,因为链表尾部插入法,都需要实现
void add_head(int x){
	e[idx] = x;
	ne[idx] = head;  // 保存头节点后续节点序列
	head = idx;  // 头节点的坐标为当前节点
	idx ++;  // 索引位置要自增
}

// 此处的k并不是对应索引的位置,是索引的位置
void add(int k,int x){
	e[idx] = x;
	ne[idx] = ne[k];
	ne[k] = idx;
	idx ++
}


void remove(int k){
	ne[k] = ne[ne[k]];
}


int main(){
	// 遍历
	for(int i = head;i != -1;i = ne[i]){
		cout<<e[i]<<endl;
	}
}

使用数组表示链表的优缺点

  • 优点

    • 数组结构的内存中是连续的,更好利用CPU
    • 操作类似指针的索引,使得插入和删除操作能够在O(1)的时间内完成
  • 缺点

    • 数组大小固定,超出大小之后无法的扩展,除非手动分配一个更大的数据并复制数据
    • 在大量插入和删除操作下,未使用的空间无法立即释放,会浪费空间。

具体运行如下

import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;


public class Main {
    static int head = -1;
    static int[] e;
    static int[] ne;
    static int idx = 0;

    static void add(int x){
        e[idx] = x;
        ne[idx] = head;
        head = idx;
        idx ++;
    }


    public static void main(String[] args) {
        int m = 10;
        e = new int[m];
        ne = new int[m];
        for (int i = 0; i < m; i++) {
            add(i);
        }

        for(int i = head;i != -1;i = ne[i]){
            System.out.println(e[i]);
        }
    }
}
链表表示树
  • 这类本质上还是使用邻接链表表示的树结构,对于每一个都构建一个头节点,然后构建一个链表,就是邻接链表的具体实现
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Arrays;
import java.util.Random;


public class Main {
    static int[] head;
    static int[] e;     // 当前链表的下一个节点的序号
    static int[] ne;
    static int[] w;
    static int idx = 0;

    static void add(int startPoint,int endPoint,int val){
        e[idx] = endPoint;
        w[idx] = val;
        ne[idx] = head[startPoint];
        head[startPoint] = idx;
		idx ++;
    }


    public static void main(String[] args) {
        // m表示节点的数量
        int m = 10;
        e = new int[m];
        ne = new int[m];
        head = new int[m];
        w = new int[m];
        Arrays.fill(head,-1);


        // 随机添加节点
        Random random = new Random();
        for (int i = 0; i < m; i++) {
        int startPoint  = random.nextInt(5);
        int endPoint = random.nextInt(5,10);
        // 无向边,这里需要连续添加两次边
        add(startPoint,endPoint,random.nextInt());
         add(endPoint,startPoint,random.nextInt());
        }

        for(int i = head[0];i != -1;i = ne[i]){
            System.out.println(e[i]);
        }
    }
}

4、树形DP的具体分析

  • 这个题目是枚举出所有可能路径,然后找出最长的,但是不能盲目列举,这里得有一个分类的依据,然后根据这个分类依据进行划分!

针对树形DP,采用树的最高点作为划分依据,而且是所有经过最高点的路径

主要分为两种情况,存在多个子树,和仅仅只有一个子树

情况一、存在多个子树

  • 在下图中,就是以6作为最高点的情况,具体有如下的相关路径!左右子树都存在节点,所以他的最优点解肯定是
    • 最长边x
    • 次长边y(仅仅只有顶点是相同的,其他的节点都不相同)
  • 当前这种情况种,对应的最长边就是,x + y
    在这里插入图片描述

情况二、仅仅存在一个子树

  • 下述是以节点3为根节点请款,只有一个子树
    在这里插入图片描述
    树的相关知识点补充
  • 树不会成环,所以不需要像图一样,设置一个visited数组,保证访问
  • 每一个边都是无向边,是双向的,所以避免出现环形路径的只需要判定是否是父节点就行!

整体代码是以dfs为基础的树的深度的遍历,具体实现如下

  • 需要遍历所有的树的深度,然后保存最长深度和次长深度,进行判定
import java.util.Arrays;
import java.util.Scanner;

public class Main {
    static int[] e;  // 元素矩阵
    static int[] ne;  // 索引坐标
    static int[] head;  // 头节点的坐标
    static int[] w;  // 权重坐标
    static int idx = 0;
    static int ans = 0;

    static void add(int startP, int endP, int val) {
        e[idx] = endP;
        w[idx] = val;
        ne[idx] = head[startP];
        head[startP] = idx;
        idx++;
    }

    static int dfs(int root, int fa) {
        // 进行树的深度遍历
        int dist = 0; // 表示当前的路径大小
        int d1 = 0, d2 = 0; // 定义最长边和次长边

        for (int i = head[root]; i != -1; i = ne[i]) {
            int curRoot = e[i];
            if (curRoot == fa) continue;
            // 遍历所有子树,并返回最长路径长度
            int d = dfs(curRoot, root) + w[i];
            dist = Math.max(d, dist);

            // 保存最长子树和次长子树
            if (d > d1) {
                d2 = d1;
                d1 = d;
            } else if (d > d2) {
                d2 = d;
            }

        }
        // 返回最大值进行判定
        ans = Math.max(ans, d1 + d2);

        return dist;
    }


    public static void main(String[] args) {
        // 需要添加对应的元素
        Scanner in = new Scanner(System.in);
        int m = in.nextInt();
        in.nextLine();
        e = new int[2 * m + 1];
        ne = new int[2 * m + 1];
        w = new int[2 * m + 1];
        head = new int[m + 1];
        Arrays.fill(head, -1);

        // 添加对应的元素
        while (in.hasNextLine()) {
//            int start = in.nextInt();
//            int end = in.nextInt();
//            int val = in.nextInt();
//            add(start, end, val);
//            add(end, start, val);

            String str = in.nextLine().trim();
            if (str.isEmpty()) {
                continue; // Skip empty lines
            }
            Scanner strIn = new Scanner(str);
            if (strIn.hasNextInt()) {
                int start = strIn.nextInt();
                int end = strIn.nextInt();
                int val = strIn.nextInt();
                add(start, end, val);
                add(end, start, val);
            }

            strIn.close();
        }
        in.close();


        // 随机指定一个点进行遍历
        dfs(1, -1);

        // 向下进行输出
        System.out.println(ans);
    }
}

在这里插入图片描述

笑死了,你敢信,我在输入输出上整了半个多小时,我想提交怎么没问题,我怎么自己写老是让我输入,一直等着我输入,合着检测控制台会结束输入,但是这里一直按下回车没屁用,不得行!

总结

  • 这个树形DP算是全部总结完毕了,下次谁再来问我,说树形DP是什么,我狠狠打打他的脸!
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值