【leetcode】(十二)树形dp

【树形dp 换根法 BFS】2581. 统计可能的树根数目 Alice 有一棵 n 个节点的树,节点编号为 0 到 n - 1 。树用一个长度为 n - 1 的二维整数数组 edges 表示,其中 edges[i] = [ai, bi] ,表示树中节点 ai 和 bi 之间有一条边。 Alice 想要 Bob 找到这棵树的根。她允许 Bob 对这棵树进行若干次 猜测 。每一次猜测,Bob 做如下事情: 选择两个 不相等 的整数 u 和 v ,且树中必须存在边 [u, v] 。 Bob 猜测树中 u 是 v 的 父节点 。 Bob 的猜测用二维整数数组 guesses 阅读详情

1.树形dp套路

树形dp套路使用前提:

如果题目求解目标是S规则,则求解流程可以定成以每一个节点为头节点的子树在S规则下的每一个答案,并且最终答案一定在其中。

树形dp套路第一步:

以某个节点X为头节点的子树中,分析答案有哪些可能性,并且这种分析是以X的左子树、X的右子树和X整棵树的角度来考虑可能性的。

树形dp套路第二步:

根据第一步的可能性分析,列出所有需要的信息。

树形dp套路第三步:

合并第二步的信息,对左树和右树提出同样的要求,并写出信息结构。

树形dp套路第四步:

设计递归函数,递归函数是处理以X为头节点的情况下的答案。

包括设计递归的basecase,默认直接得到左树和右树的所有信息,以及把可能性做整合,并且要返回第三步的信息结构这四个小步骤。

2.二叉树节点间的最大距离

从二叉树的节点a出发,可以向上或者向下走,但沿途的节点只能经过一次,到达节点b时路径上的节点个数叫作a到b的距离,那么二叉树任何两个节点之间都有距离,求整棵树上的最大距离。(leetcode543)

例如:

           a
         /   \
        /     \
       b       c
      /         \
     /           \
    d            f
                /  \
               /    \
               k    s

d-f的距离为2;d-k的距离为k。

思路:假设以x为节点,先求以x为根节点的这棵树的最大深度是多少(具体情况区分为包含头节点和不包含头节点)。最大距离划分:

(1)x不参与。此时可以划分为左边的最大距离和以右边为最大距离。总最大距离为max{左最大距离,右最大距离}

(2)x参与。此时最大距离为x左树上的最大距离,加上x右树上的最大距离。此时可以通过比较左右两树的深度(高度)来比较,总最大距离为:左高+1+右高

对于任意节点 x:

\text{maxDistance}(x)=\max \begin{cases} \text{left.maxDistance}\\ \text{right.maxDistance}\\ \text{left.height}+1+\text{right.height} \end{cases}

\text{height}(x)=\max(\text{left.height},\text{right.height})+1

时间复杂度:O(N),每个节点只处理一次。

空间复杂度:O(H),递归调用栈最多占用树的高度 H;最坏情况下退化为 O(N)。

这正好对应树形 DP 的四步:分析可能性 → 确定需要的信息 → 设计 Info → 递归整合左右子树的信息。

3.派对的最大快乐值

员工信息的定义如下:

class Employee {
    public int happy; // 这名员工可以带来的快乐值
    List<Employee> subordinates; // 这名员工有哪些直接下级
}

公司的每个员工都符合 Employee 类的描述。整个公司的人员结构可以看作是一棵标准的、没有环的多叉树。树的头节点是公司唯一的老板。除老板之外的每个员工都有唯一的直接上级。叶节点是没有任何下属的基层员工(subordinates列表为空),除基层员工外,每个员工都有一个或多个直接下级。

这个公司现在要办party,你可以决定哪些员工来,哪些员工不来。但是要遵循如下规则:

  1. 如果某个员工来了,那么这个员工的所有直接下级都不能来。

  2. 派对的整体快乐值是所有到场员工快乐值的累加。

  3. 你的目标是让派对的整体快乐值尽量大。

给定一棵多叉树的头节点boss,请返回派对的最大快乐值。

例如:

           10x
         / |  \
        /  |   \
       3x  20y  40y
      /    |    / \
     /     |   /   \
    60y    3x 5x   6x
                

如此,能够做到最大快乐值为120。

           x(头)
         / |  \
        /  |   \
       a   b   c

分析设x为头节点,

在x参与情况下:

x(乐)+a(a不来/a整棵树,最大快乐值)+b(b不来/b整棵树,最大快乐值)+c(b不来/无c整棵树,最大快乐值)

在x不参与的情况下:

0+max{a来时整棵树最大快乐值,a不来整棵树最大快乐值}+max{b来时整棵树最大快乐值,b不来整棵树最大快乐值}+max{c来时整棵树最大快乐值,c不来整棵树最大快乐值}

示例代码:

package Class012;
import java.util.ArrayList;
import java.util.List;
public class Code_MaxHappy {
    public static int maxHappy(int[][] matrix){
        int[][] dp=new int[matrix.length][2];
        boolean[] visited=new boolean[matrix.length];
        int root=0;
        for (int i=0;i<matrix.length;i++){
            if (i==matrix[i][0]){
                root=i;
            }
        }
        process(matrix,dp,visited,root);
        return Math.max(dp[root][0],dp[root][1]);
    }
    public static void process(int[][] matrix,int[][] dp,boolean[] visited,int root){
        visited[root]=true;
        dp[root][1]=matrix[root][1];
        for (int i=0;i<matrix.length;i++){
            if (matrix[i][0]==root&&!visited[i]){
                process(matrix,dp,visited,i);
                dp[root][1]+=dp[i][0];
                dp[root][0]+=Math.max(dp[i][1],dp[i][0]);
            }
        }
    }
    public static class Employee{
        public int happy;//这名员工可以带来的快乐道
        public List<Employee>nexts;
    }
    public static int maxHappy(Employee boss){
        Info headInfo=process(boss);
        return Math.max(headInfo.laiMaxHappy,headInfo.buMaxHappy);
    }
    public static class Info{
        //x来的情况下,整棵树最大收益
        public int laiMaxHappy;
        //x不来的情况下,整棵树的最大收益
        public int buMaxHappy;
        public Info(int lai,int bu){
            laiMaxHappy=lai;
            buMaxHappy=bu;
        }
    }
    public static Info process(Employee x){
        //base case
        if (x.nexts.isEmpty()){//x是基层员工的时候
            return new Info(x.happy,0);
        }
        int lai=x.happy;//x来的情况下,整棵树最大收益
        int bu=0;//x不来的情况下,整棵树的最大收益
        for (Employee next:x.nexts){
            Info nextInfo=process(next);
            lai+=nextInfo.buMaxHappy;
            bu+=Math.max(nextInfo.laiMaxHappy,nextInfo.buMaxHappy);
        }
        return new Info(lai,bu);
    }

    public static void main(String[] args) {
        // ==========================================
        // 测试1:题目中的多叉树
        // ==========================================
        //              10
        //           /   |   \
        //          3   20    40
        //          |    |   /  \
        //         60    3  5    6
        Employee boss = new Employee();
        boss.happy = 10;
        Employee a = new Employee();
        a.happy = 3;
        Employee b = new Employee();
        b.happy = 20;
        Employee c = new Employee();
        c.happy = 40;
        Employee d = new Employee();
        d.happy = 60;
        Employee e = new Employee();
        e.happy = 3;
        Employee f = new Employee();
        f.happy = 5;
        Employee g = new Employee();
        g.happy = 6;
        // 初始化下属列表
        Employee[] employees = {boss, a, b, c, d, e, f, g};
        for (Employee employee : employees) {
            employee.nexts = new ArrayList<>();
        }
        // 建立上下级关系
        boss.nexts.add(a);
        boss.nexts.add(b);
        boss.nexts.add(c);
        a.nexts.add(d);
        b.nexts.add(e);
        c.nexts.add(f);
        c.nexts.add(g);
        // 调用原有的process()和maxHappy()
        Info info = process(boss);
        System.out.println("测试1:普通多叉树");
        System.out.println("老板参加:" + info.laiMaxHappy);
        System.out.println("老板不参加:" + info.buMaxHappy);
        System.out.println("最大快乐值:" + maxHappy(boss));
        // 测试2:只有一个员工
        Employee boss2 = new Employee();
        boss2.happy = 50;
        boss2.nexts = new ArrayList<>();
        System.out.println("\n测试2:只有一个员工");
        System.out.println("最大快乐值:" + maxHappy(boss2));
        // 测试3:老板参加更有利
        Employee boss3 = new Employee();
        boss3.happy = 100;
        boss3.nexts = new ArrayList<>();
        Employee x = new Employee();
        x.happy = 20;
        x.nexts = new ArrayList<>();
        Employee y = new Employee();
        y.happy = 30;
        y.nexts = new ArrayList<>();
        boss3.nexts.add(x);
        boss3.nexts.add(y);
        System.out.println("\n测试3:老板参加更有利");
        System.out.println("最大快乐值:" + maxHappy(boss3));
    }
}

运行结果:

测试1:普通多叉树
老板参加:84
老板不参加:120
最大快乐值:120

测试2:只有一个员工
最大快乐值:50

测试3:老板参加更有利
最大快乐值:100

4.Morris遍历

一种遍历二叉树的方式,并且时间复杂度O(N),额外空间复杂度O(1)。

通过利用原树中大量空闲指针的方式,达到节省空间的目的。类似于线索二叉树。

Morris遍历细节

假设来到当前节点cur,开始时cur来到头节点位置。

1)如果cur没有左孩子,cur向右移动(cur = cur.right)。

2)如果cur有左孩子,找到左子树上最右的节点mostRight:

  a. 如果mostRight的右指针指向空,让其指向cur,然后cur向左移动(cur = cur.left)。

  b. 如果mostRight的右指针指向cur,让其指向null,然后cur向右移动(cur = cur.right)。

3)cur为空时遍历停止。

分析:

            1
          /   \
         /     \
        2       3
       / \     / \
      /   \   /   \
     4    5   6    7
    / \  / \ / \  / \
   n   n n n n n  n n
步骤cur 当前指向cur.left

cur.right

mostRight指针修改cur 移动路径当前保留的临时指针
11(第一次)2355.right: null → 11 → 2(左)5 → 1
22(第一次)4544.right: null → 22 → 4(左)5 → 1,4 → 2
34null2(临时)无不修改4 → 2(右)5 → 1,4 → 2
42(第二次)4544.right: 2 → null2 → 5(右)5 → 1
55null1(临时)无不修改5 → 1(右)5 → 1
61(第二次)2355.right: 1 → null1 → 3(右)无
73(第一次)6766.right: null → 33 → 6(左)6 → 3
86null3(临时)无不修改6 → 3(右)6 → 3
93(第二次)6766.right: 3 → null3 → 7(右)无
107nullnull无不修改7 → null(右)无

指针完整移动过程:


初始:
cur → 1

第1步:
cur = 1
mostRight = 5
5.right → 1(建立临时指针)
cur = cur.left → 2

第2步:
cur = 2
mostRight = 4
4.right → 2(建立临时指针)
cur = cur.left → 4

第3步:
cur = 4
4没有左孩子
4.right → 2(临时指针)
cur = cur.right → 2

第4步:
cur = 2(第二次)
mostRight = 4
发现4.right → 2
恢复4.right → null
cur = cur.right → 5

第5步:
cur = 5
5没有左孩子
5.right → 1(临时指针)
cur = cur.right → 1

第6步:
cur = 1(第二次)
mostRight = 5
发现5.right → 1
恢复5.right → null
cur = cur.right → 3

第7步:
cur = 3
mostRight = 6
6.right → 3(建立临时指针)
cur = cur.left → 6

第8步:
cur = 6
6没有左孩子
6.right → 3(临时指针)
cur = cur.right → 3

第9步:
cur = 3(第二次)
mostRight = 6
发现6.right → 3
恢复6.right → null
cur = cur.right → 7

第10步:
cur = 7
7没有左孩子
7.right = null
cur = cur.right → null

遍历结束。

遍历一轮后,得到:遍历序列:1,2,4,2,5,1,3,6,3,7

遍历完后,指针记录如下图:

观察 Morris 遍历可以发现:如果某个节点有左子树,那么该节点会被访问两次;如果没有左子树,那么该节点只会被访问一次。另:只要节点存在左子树,不管有没有右子树,在标准 Morris 遍历过程中都会来到该节点两次。

节点情况

Morris 到达次数

同时具有左、右子树

2

只有左子树

2

只有右子树

1

没有任何孩子

1

如何判断是第一次还是第二次到达?

对于有左子树的节点 cur,找到其左子树的最右节点 mostRight。

如果 mostRight.right == null,说明第一次到达 cur,将 mostRight.right 指向 cur,然后进入左子树。

如果 mostRight.right == cur,说明第二次到达 cur,将 mostRight.right 恢复为 null,然后进入右子树。

因此,Morris 遍历利用临时右指针记录节点的访问状态,实现不使用递归栈的二叉树遍历,时间复杂度为 O(N),额外空间复杂度为 O(1)。分析所有节点遍历左子树右边界的总代价,分析:

             1
          /     \
         /       \
        2         3
       / \       / \
      /   \     /   \
     4    5    6     7
    / \  / \  / \   / \
   8  9 10 11 12 13 14 15

1:2,5,11;
2:4,9
3:6,13
4:8
5:10
6:12
7:14

我们发现所有右边界节点都是不同的节点,如果全部算上,时间复杂度为O(n)。

此外,从morris遍历重建先序,中序和后序遍历。

            1
          /   \
         /     \
        2       3
       / \     / \
      /   \   /   \
     4    5   6    7
   

morris序:1,2,4,2,5,1,3,6,3,7

改造:

先序遍历:如果一个节点只到达一次,直接打印;如果一个节点到达两次,只打印第一次
最后变成:1,2,4,5,3,6,7

中序遍历:如果一个节点只到达一次,直接打印;如果一个节点到达两次,只打印第二次
最后变成:4,2,5,1,6,3,7

后序遍历:把打印时机只放在能回到自己两次的节点,当这个前提确定时,逆序打印左树的右边界;整棵树遍历完,单独逆序打印整棵树的右边界。
         1,2,4,2,5,1,3,6,3,7
         x,x,x,y,x,y,x,x,y,x,y
最后变成:4,5,2,6,7,3,1

另:后序遍历的验证:

             1
          /     \
         /       \
        2         3
       / \       / \
      /   \     /   \
     3    4    10    9
         /      \
        /        \
       5          11
      /           /
     /           /
    6           12
     \            \
      \            \
       7            13
        \
         \
          8

对于这样的一棵极其丑陋的树(ikun),
直接后序遍历的结果为:
3,8,7,6,5,4,2,13,12,11,10,9,3,1

morris遍历的结果为:
1,2,3,2,4,5,6,7,8,5,4,1,3,10,11,12,13,11

最后改造morris得到的后序遍历为:
3,8,7,6,5,4,2,13,12,11,10,9,3,1

发现两者完全相同,测试通过

另:逆序打印右边界可以使用单链表操作实现。

代码:

package Class012;

public class Code_MorrisTraversal {
    public static class Node{
        public int value;
        Node left;
        Node right;
        public Node(int data){
            this.value=data;
        }
    }
    public static void process(Node head){
        if (head==null){
            return;
        }
        //这里的实质是系统压栈的方法告诉你是第几次遍历
        //1,如果有打印语句,先序遍历
        process(head.left);
        //2.如果有打印语句,中序遍历
        process(head.right);
        //3.如果有打印语句,后序遍历

        //另,在递归版本中,对于带节点的二叉树,遍历流程如此:
        //      1
        //    /   \
        //   2     3
        //  / \   / \
        // n   n n   n
        //
        //递归遍历经过的节点:1,2,2,2,1,3,3,3,1
        //一个节点遍历三次
    }

    public static void morris(Node head){
        if (head==null){
            return;
        }
        Node cur=head;
        Node mostRight=null;
        while (cur!=null){//过流程
            mostRight=cur.left;//mostRight是cur左孩子
            if(mostRight!=null){//有左子树
                //1.mostRight不断往右移动的过程中most的右指针不能为空,
                //2.mostRight不断往右移动的过程中most的右指针不能指向当前节点
                while (mostRight.right!=null&&mostRight.right!=cur){
                    mostRight=mostRight.right;
                }
                //mostRight变成了cur左子树上最右的节点
                if (mostRight.right==null){//这是第一次来到cur
                    mostRight.right=cur;
                    cur=cur.left;
                    continue;
                    //continue指向的地点:while (cur!=null){//过流程
                    // mostRight=cur.left;//mostRight是cur左孩子
                }else {//mostRight==cur,第二次来到当前节点
                    mostRight.right=null;
                }
            }
            cur=cur.right;
        }
    }
    public static void morrisPre(Node head){
        if (head==null){
            return;
        }
        Node cur=head;
        Node mostRight=null;
        while (cur!=null){//过流程
            mostRight=cur.left;//mostRight是cur的左孩子
            if (mostRight!=null){//有左子树的情形,对应能够到两次的情形
                while (mostRight.right!=null&&mostRight.right!=cur){
                    mostRight=mostRight.right;
                }
                //mostRight变成了cur左子树上最右的节点
                if(mostRight.right==null){//这是第一次来到cur
                    System.out.println(cur.value);
                    mostRight.right=cur;
                    cur=cur.left;
                    continue;
                }else {//mostRight.right==cur
                    mostRight.right=null;
                }
            }else {//没有左子树的情况,对应只能到一次的情形
                System.out.println(cur.value);
            }
            cur=cur.right;
        }
    }
    public static void morrisIn(Node head){
        if (head==null){
            return;
        }
        Node cur=head;
        Node mostRight=null;
        while (cur!=null){//过流程
            mostRight=cur.left;//mostRight是cur左孩子
            if(mostRight!=null){//有左子树
                while (mostRight.right!=null&&mostRight.right!=cur){
                    mostRight=mostRight.right;
                }
                //mostRight变成了cur子树上,最右的节点
                if(mostRight.right==null){//这是第一次来到cur
                    mostRight.right=cur;
                    cur=cur.left;
                    continue;
                }else {//mostRight.right==cur;
                    mostRight.right=null;
                }
            }
            //能够遍历两次的节点
            System.out.println(cur.value);
            cur=cur.right;
        }
    }
    public static void morrisPos(Node head){
        if (head==null){
            return;
        }
        Node cur=head;
        Node mostRight=null;
        while (cur!=null){
            mostRight=cur.left;
            if (mostRight!=null){
                while (mostRight.right!=null&&mostRight.right!=cur){
                    mostRight=mostRight.right;
                }
                if (mostRight.right==null){
                    mostRight.right=cur;
                    cur=cur.left;
                    continue;
                }else {
                    mostRight.right=null;
                    printEdge(cur.left);
                }
            }
            cur=cur.right;
        }
        printEdge(head);
        System.out.println();
    }
    //以x为头的树,逆序打印这棵树的有边界
    public static void printEdge(Node X){
        //先reverse右边界,打印完后再返还回去
        Node tail=reverseEdge(X);
        Node cur=tail;
        while (cur!=null){
            System.out.println(cur.value+" ");
            cur=cur.right;
        }
        reverseEdge(tail);
    }
    public static Node reverseEdge(Node from){
        Node pre=null;
        Node next=null;
        while (from!=null){
            next=from.right;
            from.right=pre;
            pre=from;
            from=next;
        }
        return pre;
    }

    //for test--print tree
    public static void printTree(Node head){
        System.out.println("Binary Tree:");
        printInOrder(head,0,"H",17);
        System.out.println();
    }
    public static void printInOrder(Node head,int height,String to,int len){
        if (head==null){
            return;
        }
        printInOrder(head.right,height+1,"v",len);
        String val=to+head.value+to;
        int lenM=val.length();
        int lenL=(len-lenM)/2;
        int lenR=len-lenM-lenL;
        val=getSpace(lenL)+val+getSpace(lenR);
        System.out.println(getSpace(height*len)+val);
        printInOrder(head.left,height+1,"^",len);
    }
    public static String getSpace(int num){
        String space=" ";
        StringBuffer buf=new StringBuffer("");
        for (int i=0;i<num;i++){
            buf.append(space);
        }
        return buf.toString();
    }
    public static void main(String[] args){
        Node head=new Node(4);
        head.left=new Node(2);
        head.right=new Node(6);
        head.left.left=new Node(1);
        head.left.right=new Node(3);
        head.right.left=new Node(5);
        head.right.right=new Node(7);
        printTree(head);
//        morris(head);
        morrisIn(head);
        morrisPre(head);
        morrisPos(head);
        printTree(head);
    }
}

运行结果:

Binary Tree:
                                         v7v       
                        v6v       
                                         ^5^       
       H4H       
                                         v3v       
                        ^2^       
                                         ^1^       

1
2
3
4
5
6
7
4
2
1
3
6
5
7
1 
3 
2 
5 
7 
6 
4 

Binary Tree:
                                         v7v       
                        v6v       
                                         ^5^       
       H4H       
                                         v3v       
                        ^2^       
                                         ^1^       

传统二叉树遍历与morris遍历的区别:

比较项目

传统递归遍历

传统非递归遍历

Morris 遍历

遍历方式

系统递归调用

手动维护栈

临时修改右指针

时间复杂度

O(N)

O(N)

O(N)

额外空间复杂度

O(H)

O(H)

O(1)

是否需要栈

系统调用栈

显式栈

不需要

是否临时修改原树

否

否

是

遍历后树结构

不变

不变

恢复原状

节点处理方式

递归调用与返回

出栈、入栈

第一次或第二次到达

实现难度

简单

中等

较高

先序遍历

支持

支持

支持

中序遍历

支持

支持

支持

后序遍历

支持

支持

支持,需要逆序打印右边界

主要优势

逻辑清晰、代码简洁

避免递归调用

额外空间 O(1)

主要不足

深树可能栈溢出

需要额外存储节点

指针操作复杂,不能随意中断

详细对比:

比较项目

传统先序

Morris 先序

传统中序

Morris 中序

传统后序

Morris 后序

遍历规则

根→左→右

根→左→右

左→根→右

左→根→右

左→右→根

左→右→根

有左子树节点的打印时机

递归进入节点时

第一次到达

左子树处理完后

第二次到达

左右子树处理完后

第二次到达时打印左子树右边界

无左子树节点的处理

进入时打印

唯一一次到达时打印

进入时打印

唯一一次到达时打印

右子树处理完后打印

由祖先处理右边界时打印

是否使用递归栈

是

否

是

否

是

否

是否反转右边界

否

否

否

否

否

是

时间复杂度

O(N)

O(N)

O(N)

O(N)

O(N)

O(N)

额外空间复杂度

O(H)

O(1)

O(H)

O(1)

O(H)

O(1)

morris遍历的应用:

如何判断一棵树是搜索二叉树?

在中序遍历中,如果一棵树是升序的,它就是搜索二叉树

代码:

package Class012;

public class Code_MorrisBST {
    public static class Node{
        public int value;
        Node left;
        Node right;
        public Node(int data){
            this.value=data;
        }
    }
    public static void process(Node head){
        if (head==null){
            return;
        }
        //这里的实质是系统压栈的方法告诉你是第几次遍历
        //1,如果有打印语句,先序遍历
        process(head.left);
        //2.如果有打印语句,中序遍历
        process(head.right);
        //3.如果有打印语句,后序遍历

        //另,在递归版本中,对于带节点的二叉树,遍历流程如此:
        //      1
        //    /   \
        //   2     3
        //  / \   / \
        // n   n n   n
        //
        //递归遍历经过的节点:1,2,2,2,1,3,3,3,1
        //一个节点遍历三次
    }

    //以x为头的树,逆序打印这棵树的有边界
    public static void printEdge(Node X){
        //先reverse右边界,打印完后再返还回去
        Node tail=reverseEdge(X);
        Node cur=tail;
        while (cur!=null){
            System.out.println(cur.value+" ");
            cur=cur.right;
        }
        reverseEdge(tail);
    }
    public static Node reverseEdge(Node from){
        Node pre=null;
        Node next=null;
        while (from!=null){
            next=from.right;
            from.right=pre;
            pre=from;
            from=next;
        }
        return pre;
    }

    //for test--print tree
    public static void printTree(Node head){
        System.out.println("Binary Tree:");
        printInOrder(head,0,"H",17);
        System.out.println();
    }
    public static void printInOrder(Node head,int height,String to,int len){
        if (head==null){
            return;
        }
        printInOrder(head.right,height+1,"v",len);
        String val=to+head.value+to;
        int lenM=val.length();
        int lenL=(len-lenM)/2;
        int lenR=len-lenM-lenL;
        val=getSpace(lenL)+val+getSpace(lenR);
        System.out.println(getSpace(height*len)+val);
        printInOrder(head.left,height+1,"^",len);
    }
    public static String getSpace(int num){
        String space=" ";
        StringBuffer buf=new StringBuffer("");
        for (int i=0;i<num;i++){
            buf.append(space);
        }
        return buf.toString();
    }

    public static boolean isBST(Node head){
        if (head==null){
            return true;
        }
        Node cur=head;
        Node mostRight=null;
        //前一个中序遍历节点
        Node pre=null;
        //判断结果
        boolean isBST=true;
        while (cur!=null){//过流程
            mostRight=cur.left;//mostRight是cur左孩子
            if(mostRight!=null){//有左子树
                //1.mostRight不断往右移动的过程中most的右指针不能为空,
                //2.mostRight不断往右移动的过程中most的右指针不能指向当前节点
                while (mostRight.right!=null&&mostRight.right!=cur){
                    mostRight=mostRight.right;
                }
                //mostRight变成了cur左子树上最右的节点
                if (mostRight.right==null){//这是第一次来到cur
                    mostRight.right=cur;
                    cur=cur.left;
                    continue;
                    //continue指向的地点:while (cur!=null){//过流程
                    // mostRight=cur.left;//mostRight是cur左孩子
                }else {//mostRight==cur,第二次来到当前节点
                    mostRight.right=null;
                }
            }
            if (pre!=null&&cur.value<=pre.value){
                isBST=false;
            }
            //更新前一个节点
            pre=cur;
            //进入右子树
            cur=cur.right;
        }
        return isBST;
    }
    public static void main(String[] args){
        // 测试1:正常的搜索二叉树
        Node head = new Node(4);
        head.left = new Node(2);
        head.right = new Node(6);
        head.left.left = new Node(1);
        head.left.right = new Node(3);
        head.right.left = new Node(5);
        head.right.right = new Node(7);
        System.out.println("测试1:正常搜索二叉树");
        printTree(head);
        System.out.println("是否为搜索二叉树:" + isBST(head));

        // 测试2:不符合搜索二叉树规则
        // 将节点3改成8
        head.left.right.value = 8;
        System.out.println("\n测试2:非搜索二叉树");
        printTree(head);
        System.out.println("是否为搜索二叉树:" + isBST(head));
        // 测试3:Integer.MIN_VALUE边界情况
        Node head3 = new Node(Integer.MIN_VALUE);
        System.out.println("\n测试3:只有一个最小整数节点");
        printTree(head3);
        System.out.println("是否为搜索二叉树:" + isBST(head3));
    }
}

运行结果:

测试1:正常搜索二叉树
Binary Tree:
                                         v7v       
                        v6v       
                                         ^5^       
       H4H       
                                         v3v       
                        ^2^       
                                         ^1^       

是否为搜索二叉树:true

测试2:非搜索二叉树
Binary Tree:
                                         v7v       
                        v6v       
                                         ^5^       
       H4H       
                                         v8v       
                        ^2^       
                                         ^1^       

是否为搜索二叉树:false

测试3:只有一个最小整数节点
Binary Tree:
  H-2147483648H  

是否为搜索二叉树:true

总结:

如果方法要求遍历完左子树和右子树后,再回到原来的节点做第三次强整合,最后才能得出答案,那么用搜索二叉树的套路解法是最优解。如果不用做第三次强整合,那么morris遍历是最优解

用WorkBuddy和ima搭建本地知识库:从概念到实操 在AI应用普及的今天,私有知识的高效管理与调用成为痛点。RAG(检索增强生成)技术通过将外部知识库与语言模型耦合,为精准问答提供了技术底座。然而,传统RAG方案往往涉及向量数据库、文本切分等复杂环节。一种更轻量的路径应运而生:借助WorkBuddy与ima的组合,将知识收集、语义检索与智能体工作流整合,让本地知识库真正服务于日常办公和研发场景。从概念到实操,系统解析这套轻量级方案的核心逻辑与配置方法,并对比重方案适用边界,帮助读者快速搭建属于自己的AI知识助手。 阅读详情

相关推荐

Field-Oriented Control (FOC) 矢量控制 磁场定向控制

Field-Oriented Control (FOC) 磁场定向控制(FOC),也称为矢量控制,是一种用于控制永磁同步电动机(PMSM)和交流感应电动机(ACIM)的技术。 FOC在整个扭矩和速度范围内都具有良好的控制能力。 FOC的实现需要将定子电流从固定参考系转换为转子磁通参考系(也称为d-q参考系)。 速度控制和转矩控制是FOC最常用的控制模式。位置控制模式不太常见。大多数牵引应用使用转矩控制模式,在该模式下,电动机控制系统遵循参考转矩值。在速度控制模式下,电机控制器遵循参考速度值,并生成用于形成内

>>> 1万+

树形dp求树的直径

题目描述 路径 被定义为一条从树中任意节点出发,沿父节点-子节点连接,达到任意节点的序列。同一个节点在一条路径序列中 至多出现一次 。该路径 至少包含一个 节点,且不一定经过根节点。 路径和 是路径中各节点值的总和。 给你一个二叉树的根节点 root ,返回其 最大路径和 。 示例 1: 输入:root = [1,2,3] 输出:6 解释:最优路径是 2 -> 1 -> 3 ,路径和为 2 + 1 + 3 = 6 示例 2: 输入:root = [-10,9,20,null,null,

IAMLSL的博客 4217

Zynq UltraScale+ RFSoC实战:用XCZU49DR开发板实现雷达信号处理全流程

本文详细介绍了如何利用Xilinx Zynq UltraScale+ RFSoC系列中的XCZU49DR开发板,高效实现雷达信号处理全流程。文章深入剖析了该芯片集成的射频直采ADC/DAC、可编程逻辑与Arm多核处理器的异构计算优势,并通过实战案例展示了从环境搭建、硬件链路设计到软件优化的完整开发路径,为相控阵雷达等高性能应用提供了单芯片解决方案。

weixin_29291863的博客 578

【算法】力扣【树形DP】687. 最长同值路径

本题是一道中等难度的树形DP问题,通过分治的思想和递归实现,仅仅是在树形DP的外皮上套了一层需求,我们只需要思考一下如何将节点的值考虑进状态转移方程即可非常简单地解决这个问题。

未来就在脚下。 1052

STM32智能头盔DIY全攻略:从心率监测到跌倒报警的完整实现

本文提供了基于STM32的智能头盔完整DIY实现方案,涵盖从心率监测到跌倒报警的核心功能。详细解析了系统设计、硬件搭建、软件算法及系统联调,重点介绍了STM32主控、MAX30102心率传感器和MPU6050姿态传感器的应用,并分享了跌倒检测算法与功耗优化等实战经验,为嵌入式开发与物联网创新项目提供全面指导。

sony5的博客 500

基于STM32的INA226电压电流检测仪

该功率检测装置以STM32F103C8T6微控制器为核心,集成功率检测、数据交互、状态显示和用户提示功能,通过模块化设计实现稳定运行。系统包括按键、LED指示灯、OLED屏幕、INA226电压电流检测、复位电路、晶振电路、蜂鸣器驱动、输入输出接口、调试接口和3.3V降压电路等模块。微控制器通过按键检测用户输入,启动功率检测,采集数据并通过OLED显示,异常情况触发蜂鸣器报警,LED状态指示同步反馈。3.3V降压电路为所有模块提供稳定电源,晶振电路确保时序精度。INA226芯片通过I2C接口与微控制器通信,实

a2784288164的博客 1910

算法:树形dp(树状dp)

树状dp的基本概念 和 模板题

m0_63997099的博客 3929

通达信-申万宏源金融终端本地数据转sqlite3,2、解析申万宏源C:/zd_swhy_gm/vipdoc/sh/lday的sh600000股票数据

为量化做准备,通达信-申万宏源金融终端本地数据转sqlite3,解析申万宏源C:/zd_swhy_gm/vipdoc/sh/lday的sh600000股票数据

wuyaqi的博客 511

2023-04-26 力扣LeetCode上的DP动态规划问题分类汇总

所有线性递推关系都可以用矩阵快速幂做,可以O(logN),最典型是斐波那契数列。计数型DP都可以以组合数学的方法写出组合数,然后dp求组合数。本质是 dfs + 记忆化,用在状态的转移方向不确定的情况。策梅洛定理,SG 定理,minimax。

lsgwr的博客 801

从零开始学习PX4源码9(部署px4源码到gitee)

由于PX4-Autopilot托管在github,在没有翻墙的情况下,下载固件无法下载。为了提高下载速度,这里将PX4在github上的仓库以及其子仓库(依赖仓库)都搬移到gitee上了。本教程讲解如何快速部署px4固件到gitee上,实现不需要翻墙快速本地就可以下载PX4固件。暂时没有找到如何批量直接通过修改.gitmodules后,使用什么命令可以直接把所有的一级二级子模块的指向直接正常映射。!!

陌城烟雨 810

Open Dynamics Engine(ODE)物理引擎教程(1)--下载与安装准备

引言 ODE是一个免费的工业级库,用于模拟关节式刚体动力学,例如地面车辆、腿部生物和虚拟现实环境中的移动对象。它具有快速、灵活、健壮和独立于平台的特点,具有先进的关节、摩擦接触和内置的碰撞检测功能。 项目主页:http://www.ode.org/ 线上手册:http://ode.org/wiki/index.php?title=Manual 本教程为博主亲自亲测过程的总结,转载请注明出处! ...

腿足机器人领域 2216

潮汐:[力扣] DP问题分类汇总

力扣中的DP问题分类汇总,总结得挺全的,题目前面的序号就是在力扣中的序号,原文链接:https://zhuanlan.zhihu.com/p/126546914?utm_source=wechat_session&utm_medium=social&utm_oi=56598949003264&from=singlemessage&s_s_i=rIsgHJqZBNaslkK8ImXR1xGA6iybytF5h6TpNyYqU%2FA%3D&s_r=0

何时立秋的博客--向前有路,未来可期 799

Vue3 - Element Plus 全局修改更换主题色配置详细教程,element-plus自定义主题色全局替换主色调强力解决方案,支持动态修改主题颜色/动态皮肤,将默认蓝色主题色更换为其他颜色!

vue3,nuxt3,element-plus,组件库,主题色,修改主题颜色,更换主题色,如何修改组件默认主题蓝色,项目主题色定制,修改失败,主题色覆盖不了,:root直接修改,hover颜色没改,自定义主题色,vue3全局设置elementPlus主题色最新详细修改教程,怎么修改全局的颜色,scss变量,覆盖原来的主题色改为自己的颜色,统一替换,有没有好办法,颜色不变,失败,无效,动态主题切换,主题颜色怎么在项目里直接改,plus主题色更换,覆盖默认主题色,最强最好用,设置主题色,换肤,皮肤,动态更换

🏆 前端领域优质创作者 1万+

浅说树形dp

因为树的形式非常适合递归,他所带来的访问顺序也是非常符合拓扑排序的,故而在处理子树类问题时,dp可以很好的利用相邻层级之间的关系和逻辑,非常符合dp的“口味”,所以我们才有了这个树形dp。树形dp和线性dp没有什么本质上的区别,只不过一个是在树上,一个是在线上,唯一的一个不同点就是树形dp可以大致的定形,而线性dp却不可以。

CylMK的博客 1276

Probability and Statistics - 4th edition

Probability and Statistics - 4th edition

Devexpress16.2.6破解工具

Devexpress16.2.6破解工具Devexpress16.2.6破解工具Devexpress16.2.6破解工具Devexpress16.2.6破解工具

LeetCode 力扣 337. 打家劫舍 III rob 树形动态规划DP

大家觉得写还可以,可以点赞、收藏、关注一下吧! 也可以到我的个人博客参观一下,估计近几年都会一直更新!和我做个朋友吧!https://motongxue.cn 文章目录337. 打家劫舍 III题目描述样例分析提交结果在这里插入图片描述 337. 打家劫舍 III 题目描述 在上次打劫完一条街道之后和一圈房屋后,小偷又发现了一个新的可行窃的地区。这个地区只有一个入口,我们称之为“根”。 除了“根”之外,每栋房子有且只有一个“父“房子与之相连。一番侦察之后,聪明的小偷意识到“这个地方的所有房屋的排列类似于一

motongxue的博客 369

老版QQ原生表情gif动态

这是老版的QQ表情,不过全是动态的的,Windows照片查看器是不会动的,可以右击用浏览器打开查看,再说一遍是老版的,只是分享给宁愿用老版也要动态的人。

【树形 DP】树形 DP 的通用思路

篇,系列开始于 2021/01/01,截止于起始日 LeetCode 上共有 1916 道题目,部分是有锁题,我们将先把所有不带锁的题目刷完。在仓库地址里,你可以看到系列文章的题解链接、系列文章的相应代码、LeetCode 原题链接和其他优选题解。为根节点的最大深度,必然是下面三种情况之一(往下、往上 和 往上再往下)。,目的都是为了能够正确统计「往上再往下」的情况(统计该情况时,不能考虑从。的出边」将其分为「往上」和「往下」两个方向。在所有可能的树中,具有最小高度的树(即,可选择树中任何一个节点作为根。

宫水三叶的刷题日记 174

数学建模模型总结

这是本人在两年内准备参加数学建模竞赛搜集到的模型资料,已经经过了本人的筛选和整理,希望能够对准备参加数学建模竞赛的学弟学妹们提供一些帮助!

力扣第 179 场周赛补题报告(思维)(思维)(树形dp)(树形dp)

听说不是很难,本弱鸡就来试一试,感觉还是有所收获的。 目录1374. 生成每种字符都是奇数个的字符串1375. 灯泡开关 III1376. 通知所有员工所需的时间1377. T 秒后青蛙的位置 1374. 生成每种字符都是奇数个的字符串 这道是思维题:我们那到一个数n,有两种情况: 若他是奇数,那么直接返回n个字符组成的字符串 若他是偶数,那么,我们分成一个1+一个奇数输出就行了。简单的一题目。 ...

nagisa2019的博客 427

数据结构与算法(Java)-树形DP题单

543. 二叉树的直径 - 力扣(LeetCode)给你一棵二叉树的根节点,返回该树的 直径 。二叉树的 直径 是指树中任意两个节点之间最长路径的 长度 。这条路径可能经过也可能不经过根节点 。两节点之间路径的 长度 由它们之间边数表示。示例 1: 示例 2: 提示: 124 二叉树的最大路径和 124. 二叉树中的最大路径和 - 力扣(LeetCode)二叉树中的 路径 被定义为一条节点序列,序列中每对相邻节点之间都存在一条边。同一个节点在一条路径序列中 至多出现一次 。该路径 至少包含一个 节点,且不

fffffall的博客 1217

树形DP

目录 一,树形DP 二,OJ实战 CSU 1022: 菜鸟和大牛 SCU-1114 数字三角 HDU - 2032杨辉三角 CSU 1010: Water Drinking 一,树形DP 以树作为动态规划的良基集合的问题,称为树形DP 二,OJ实战 CSU 1022: 菜鸟和大牛 题目: Description blue和AutoGerk是好朋友。他们的相同点是都喜欢研究算法,不同点是AutoGerk已是大牛而blue还是菜鸟。blue经常拿一些自以为很难的问题去问Aut...

nameofcsdn的博客 2022

树形dp总结

树形dp总结

Tikitian的博客 301

算法套路十九——树形DP

树形 DP,即在树上进行的 DP。由于树固有的递归性质,这里的DP是指是一种通过把原问题分解为相对简单的子问题的方式求解复杂问题的方法,故虽然带有DP,但一般都是通过来进行。

Pistachiout的学习博客 2258

为什么 4bit 量化会被拒绝?Confucius4-R2T2-gguf 实时语音识别模型只支持 Q8_0/F16 的完整解析

Confucius4-R2T2-gguf 是网易有道实时语音识别模型 Confucius4-R2T2 的 GGUF 量化版本,基于 Qwen3-ASR-1.7B 微调,支持流式低延迟语音转文字,可通过 audio.cpp 在本地 Metal / CUDA / CPU 上运行。很多人会好奇:既然 GGUF 生态里 4bit 量化最流行,为什么这个项目偏偏只提供 Q8_0 和 F16 两个精度,还在加

gitblog_00190的博客 1009

【LeetCode834】【树形dp】每日一题day35

道题要求每个节点到其他所有节点的距离之和 先考虑一个节点到其他所有节点的距离之和的问题。看到距离首先我想到的就是最短路,但是树上没啥最短路_(:з」∠)_ 然后我就没想出什么行之有效的方法_(:з」∠)_ 看了题解是树形dp,那么问题来了,为什么呢?这道题也没有什么最值,看起来距离之和是一个很固定的东西,一点都不动态。 其实我觉得,与其说是dp,不如说是递推。这里的中心思想就是从叶子往上推,处理距离 distSum[root] = sigma(distSum[root'sChild] + childSum[

lyx_2016的博客 293

LeetCode 2581.统计可能的树根数目:换根DP(树形DP)

LeetCode 2581.统计可能的树根数目:换根DP(树形DP) Alice 有一棵 n 个节点的树,节点编号为 0 到 n - 1 。树用一个长度为 n - 1 的二维整数数组 edges 表示,其中 edges[i] = [ai, bi] ,表示树中节点 ai 和 bi 之间有一条边。 Alice 想要 Bob 找到这棵树的根。她允许 Bob 对这棵树进行若干次 猜测 。每一次猜测,Bob 做如下事情: 选择两个 不相等 的整数 u 和 v ,且树中必须存在边 [u, v] 。 Bob 猜测树中

<font color="Pink">Tisfy的博客</font> 1558

动态规划进阶(六):树形DP原理详解

节点维度dp[u][s]表示节点u在状态s下的最优解常见状态选择/不选当前节点颜色标记(如红黑树着色问题)距离限制(如树的直径)dp[u][0]:不选节点u时的最大快乐值dp[u][1]:选择节点u时的最大快乐值int val;树形DP的解题关键定义合适的状态:通常与节点选择/属性相关设计正确的转移方程:考虑所有子节点状态组合掌握后序遍历特性:先处理子树再合并结果高阶扩展方向结合图论处理森林问题处理动态树结构(Link-Cut Tree)结合贪心思想优化状态转移。

xiaoyu❅的博客 1638

动态规划(四):树形DP——树上最大独立集、树的直径、树形背包

树形DP是在树结构上进行的动态规划。由于树天然的递归特性,通常采用深度优先搜索(DFS)的方式,先处理子树,再向上汇总信息。树形DP广泛应用于树上的最优化问题,如选择节点、计算最长路径、分配资源等。树上最大独立集:选择不相邻的节点,使权值和最大(如“打家劫舍 III”)树的直径:树中相距最远的两个节点之间的距离树形背包:在树上做分组背包,每个子树可选若干节点每部分均包含一句话记住核心思想流程图推荐代码实现LeetCode实战(附题目链接及解题代码),让你彻底掌握树形DP的套路。问题类型状态定义。

2302_81155164的博客 1326
上一篇: 【leetcode】(十一)Manacher算法,滑动窗口和单调栈
下一篇: 【leetcode】(十三)大数据处理
Cccp.123
博客等级 码龄3年 5粉丝 35原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值