文章目录
- 引言
- 正文
- 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是什么,我狠狠打打他的脸!