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

news2025/1/8 16:36:48

文章目录

    • 引言
    • 正文
      • 0、题目的具体内容
      • 1、树的直径定理推导
      • 3、使用数组链表表示树
        • 使用数组表示链表
          • 数组表示单链表头插法演示
          • 数组表示单链表在索引k出插入一个数字
          • 数组表示链表实现代码
          • 链表表示树
      • 4、树形DP的具体分析
    • 总结

引言

  • 这个问题,每一次都会痛击我,每一次都不会,或者说听懂了,就忘记了,完全不走脑子的,真的!今天就完全整理一下,然后下次多背背
  • 主要分为三个部分,分别是
    • 树的直径公式证明
    • 数组链表表示树
    • 树形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是什么,我狠狠打打他的脸!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2051792.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

养猫劝退?猫咪掉毛严重怎么办?用宠物空气净化器高效清理浮毛

不瞒大家说&#xff0c;养猫以来&#xff0c;我中途有无数次想要把它送人的想法&#xff0c;最终还是敌不过它的可爱留下来了。为什么会产生这样的念头呢&#xff1f;罪魁祸首就是猫毛问题。夏天是猫咪的换毛季&#xff0c;它们为了散热会脱去厚重的毛发&#xff0c;进入疯狂掉…

06结构型设计模式——代理模式

一、代理模式简介 代理模式&#xff08;Proxy Pattern&#xff09;是一种结构型设计模式&#xff08;GoF书中解释结构型设计模式&#xff1a;一种用来处理类或对象、模块的组合关系的模式&#xff09;&#xff0c;代理模式是其中的一种&#xff0c;它可以为其他对象提供一种代…

依赖倒置原则详解

依赖倒置原则详解 一、引言 在大型系统架构设计中&#xff0c;依赖倒置原则&#xff08;Dependency Inversion Principle&#xff0c;DIP&#xff09;被广泛视为增强系统灵活性和可维护性的核心原则之一。最近在架构设计审查中&#xff0c;我们经常遇到由于依赖关系设计不当导…

叠Buff!经典麻雀优化算法+多重双向深度学习!SSA-BiTCN-BiGRU-Attention多输入单输出回归预测

叠Buff!经典麻雀优化算法多重双向深度学习&#xff01;SSA-BiTCN-BiGRU-Attention多输入单输出回归预测 目录 叠Buff!经典麻雀优化算法多重双向深度学习&#xff01;SSA-BiTCN-BiGRU-Attention多输入单输出回归预测效果一览基本介绍程序设计参考资料 效果一览 基本介绍 1.Matl…

P2858 [USACO06FEB] Treats for the Cows

题目描述 约翰经常给产奶量高的奶牛发特殊津贴&#xff0c;于是很快奶牛们拥有了大笔不知该怎么花的钱。为此&#xff0c;约翰购置了 N 份美味的零食来卖给奶牛们。每天约翰售出一份零食。当然约翰希望这些零食全部售出后能得到最大的收益。这些零食有以下这些有趣的特性&…

(python)动态类型语言的灵活性和动态性

前言 一种具有动态类型,动态绑定&#xff0c;动态执行和灵活的对象模型的编程语言。它能够适应不断变化的编程需求的&#xff0c;是否会受欢迎? 动态语言的优点 灵活性高&#xff1a;开发过程更加灵活和快捷&#xff0c;不需要在编写代码前严格定义变量和对象的类型&#xff0…

linux之prometheus+grafana

Prometheus介绍 Prometheus(普罗米修斯)是一套开源的监控&报警&时间序列数据库的组合, 由go语言开发。 适合监控容器平台, 因为kubernetes(俗称k8s)的流行带动了prometheus的发展。 PS&#xff1a;由于目前还未学习容器&#xff0c;所以在今天的课程里使用prometheus监…

嵌入式Linux:proc文件系统

目录 1、使用 cat 命令读取 /proc 文件系统 2、在应用程序中使用 open() 和 read() 函数读取 /proc 文件系统 proc 文件系统是一个虚拟文件系统&#xff0c;它以文件系统的形式为应用层提供访问系统内核数据的接口。用户和应用程序可以通过 proc 文件系统获取系统信息和进程相…

腾讯一面算法题:最长重复子串 1044,讲个比较好理解的思路

文章目录 1044. 最长重复子串前言思路Version 1&#xff1a;暴力Version 2&#xff1a;引入二分&#xff0c;优化 O ( n 2 ) O(n^2) O(n2)Version 3&#xff1a;引入自定义哈希&#xff0c;优化字符串比较Version 4&#xff1a;计算所有字符串的哈希值Version 5&#xff1a;引…

前后端项目交互异步请求JSON数据类型后端标准响应数据格式

java同步请求 当网页与后端交互时,前端不能再进行其他操作 服务器响应回来的内容,会把整个浏览器中的内容覆盖 这种请求方式在前后端交互时不太友好 现在的前后端交互请求都使用异步请求 异步请求(不同步) 通过在前端中使用js中提供的XMLHttpRequest对象实现发送异步请求…

人工智能与机器学习在医学领域的应用

作者主页: 知孤云出岫 人工智能与机器学习在医学中的应用 目录 作者主页:人工智能与机器学习在医学中的应用1. 引言2. 医学中的AI和ML技术概述2.1 人工智能和机器学习基础2.2 数据在医学AI中的重要性 3. 医学AI和ML的具体应用领域3.1 影像诊断3.2 基因组学与个性化医疗3.3 疾…

JavaEE篇:多线程(1)

一 认识线程(Thread) 1.1 概念 1.1.1 线程是什么&#xff1f; 线程被创建出来是为了完成分配给它的任务。线程又称轻量级进程&#xff0c;是操作系统的基本调度单位。一个线程就是一个执行流。线程的创建销毁和切换都比进程更加的方便。进程是操作系统分配资源的基本单位&am…

C++ //练习 17.16 如果前一题程序中的regex对象用“[^c]ei“进行初始化,将会发生什么?用此模式测试你的程序,检查你的答案是否正确。

C Primer&#xff08;第5版&#xff09; 练习 17.16 练习 17.16 如果前一题程序中的regex对象用"[^c]ei"进行初始化&#xff0c;将会发生什么&#xff1f;用此模式测试你的程序&#xff0c;检查你的答案是否正确。 环境&#xff1a;Linux Ubuntu&#xff08;云服务…

「C++系列」数据结构

文章目录 一、数据结构1. 线性数据结构2. 非线性数据结构3. 其他重要数据结构 二、定义数据结构1. 数组&#xff08;Array&#xff09;2. 链表&#xff08;LinkedList&#xff09;3. 栈&#xff08;Stack&#xff09; 三、指针、关键字1. 指针链表树 2. 关键字 四、相关链接 一…

【TCP/IP】UDP协议数据格式和报文格式

学习一个网络协议&#xff0c;主要就是学习“数据格式”/“报文格式” 源端口/目的端口 端口号是属于传输层的概念UDP 报头使用两个自己的长度来表示端口号之所以端口号的范围是 0~65535&#xff0c;是因为底层网络协议做出了强制要求如果使用一个 10 w 这样的端口&#xff0…

机器学习:多元线性回归模型

目录 前言 一、讲在前面 1.多元_血压.csv&#xff1a; 2.完整代码&#xff1a; 3.运行结果&#xff1a; 二、实现步骤 1.导入库 2.导入数据 3.绘制散点图&#xff08;这步可以省略&#xff09; ​编辑 4.求特征和标签的相关系数 5.建立并训练线性回归模型 6.检验模…

NtripShare全站仪自动化监测之气象改正

最近有幸和自动化监测领域权威专家进行交流&#xff0c;讨论到全站仪气象改正的问题&#xff0c;因为有些观点与专家不太一致&#xff0c;所以再次温习了一下全站仪气象改正的技术细节。 气象改正的概念 全站仪一般利用光波进行测距&#xff0c;首先仪器会处理测距光波的相位漂…

C++| QT图片调整透明度叠加

QT图片调整透明度叠加 实际效果界面UI放置控件设置布局界面自适应 代码项目工程的文件初始化按钮功能滑动条功能图片调整透明度叠加 实际效果 三个图片&#xff08;QLabel&#xff09;显示&#xff0c;两个按钮&#xff08;QPushButton&#xff09;加载图片&#xff0c;一个&a…

【Java学习】反射和枚举详解

所属专栏&#xff1a;Java学习 &#x1f341;1. 反射 在程序运行时&#xff0c;可以动态地创建对象、调用方法、访问和修改字段&#xff0c;以及获取类的各种属性信息&#xff08;如成员变量、方法、构造函数等&#xff09;&#xff0c;这种机制就称为反射 反射相关的类 类名用…

【算法】马踏棋盘(骑士周游)问题回溯算法实现以及使用贪心算法优化

目录 1.游戏规则 2.算法分析 3.解决步骤和思路 4.马踏棋盘算法的代码实现 4.1计算马儿还能走哪些位置 4.2马踏棋盘的核心代码 4.3马踏棋盘算法完整代码 4.4使用贪心算法进行优化 4.4.1思路 4.4.2代码实现 1.游戏规则 将马儿随机放在国际象棋的 8*8 棋盘的某个方格中…