< 每日算法 - JavaScript解析:二叉树灯饰【初识动态规划 - dp, 具体理解配合代码看最合适,代码均有注释】 >

每日算法 - JavaScript解析:二叉树灯饰【初识动态规划 - dp, 具体理解配合代码看最合适,代码均有注释】

  • 一、任务描述:
    • 》 示例一:
    • 》示例二
    • 》示例三
  • 二、题意解析
    • 拓展知识
  • 三、解决方案:
  • 往期内容

一、任务描述:

「力扣嘉年华」的中心广场放置了一个巨型的二叉树形状的装饰树。每个节点上均有一盏灯和三个开关。节点值为 0 表示灯处于「关闭」状态,节点值为 1 表示灯处于「开启」状态。每个节点上的三个开关各自功能如下:

  • 开关 1:切换当前节点的灯的状态;
  • 开关 2:切换 以当前节点为根的子树中,所有节点上的灯的状态;
  • 开关 3:切换 当前节点及其左右子节点(若存在的话) 上的灯的状态;

给定该装饰的初始状态 root,请返回最少需要操作多少次开关,可以关闭所有节点的灯。

/**
 * Definition for a binary tree node.
 * function TreeNode(val) {
 *     this.val = val;
 *     this.left = this.right = null;
 * }
 */
/**
 * @param {TreeNode} root
 * @return {number}
 */
var closeLampInTree = function(root) {
	...
}

本题取自 leetcode 秋赛题集

》 示例一:

输入:root = [1,1,0,null,null,null,1]

输出:2

解释:以下是最佳的方案之一,如图所示

》示例二

输入:root = [1,1,1,1,null,null,1]

输出:1

解释:以下是最佳的方案,如图所示

》示例三

输入:root = [0,null,0]

输出:0

解释:无需操作开关,当前所有节点上的灯均已关闭

二、题意解析

根据题目,我们可以先定下几个变量先。然后逐步推进:

  1. 题中,定义了三种类型开关,可以通过推断,枚举出各个情况下。进行操作的步骤,具体可以配合代码理解;
  2. 灯饰状态,可以分为以下四种: 全亮、全灭、仅根亮、仅根不亮,每个节点需记录:全亮、全灭、仅根亮、仅根不亮这四种状态所需要的最小操作数;
  3. 由于三种开关的存在,需要考虑不同情况下。利用不同开关操作的最小值,例如:
//到达当前全亮的情况:
//1、左右子树全亮,无需操作
//2、左右子树全不亮,需要2次操作(左子树开关2,右子树开关2)
//3、左右子树只有根亮,需要2次操作(根开关2,根开关3)

//到达当前全灭的情况:
//1、左右子树全亮,需要1次操作(根开关2)
//2、左右子树全不亮,需要1次操作(根开关1)
//3、左右子树只有根亮,需要1次操作(根开关3)

状态转移可以通过下图来理解:
< 每日算法 - JavaScript解析:二叉树灯饰【初识动态规划 - dp, 具体理解配合代码看最合适,代码均有注释】 >_第1张图片

Tips 思考这道题,得从【操作】入手。这是动态规划类型题的一个常规套路:分清有哪些操作,分别导致了怎样的状态转移。


拓展知识

详细点击跳转 什么是动态规划(Dynamic Programming)?动态规划的意义是什么? - 阮行止的回答 - 知乎


三、解决方案:


/**
 * Definition for a binary tree node.
 * function TreeNode(val) {
 *     this.val = val;
 *     this.left = this.right = null;
 * }
 */
/**
 * @param {TreeNode} root
 * @return {number}
 */
var closeLampInTree = function(root) {
    let res = []
      
    /*
    	总共有4种状态,全亮,全灭,仅根亮,仅根不亮,分别记为a,b,c,d,
	   	
	   	记当前节点的左右子树(l和r)到达4种状态所需要的操作次数分别是[la,lb,lc,ld]和[ra,rb,rc,rd]
	    
	    总共分为32种情况, 由于可以定义不同的灯饰状态,可以自己打草稿看看具体有多少种状态切换情况。
    */

	// 通过内部递归计算,直至空节点退出
    let dfs = (root) => {
    	// 空节点不需要操作
        if (root == null) return [0,0,0,0]
		/* 
			通过方法得出左右子节点的各个状态所需次数,逐层深入。
			然后最终只需要取顶点的全灭最少操作次数即可。 
			其实有点定制方程,然后输入常量,让代码去像逆推出结果,也就是自底向上运算,一层一层往上传输自己这个节点运算的结果。
		*/
		// 先执行递归函数,后进行逻辑判断
        let l = dfs(root.left)
        let r = dfs(root.right)
        
        // 当前节点各个状态最小操作次数 
        let [a, b, c, d] = [0, 0, 0, 0]
        
        // 每次循环都往下取当前节点的左右子树的各个状态操作最小次数
        let [la, lb, lc, ld] = l
        let [ra, rb, rc, rd] = r
        
        //首先讨论当前节点灯亮的情况,各种状态操作次数:
        if (root.val == 1) {
			// 以下均基于根亮的情况下进行情况判断
			
            //到达当前节点树全亮的情况(a状态):
            /* 
            	1、左右子树全亮的情况下,无需操作。 即: la + ra
            	2、左右子树全不亮,需要在原先左右子树全灭的情况下,2次操作(左子树开关2,右子树开关2)。
            	也就是当前节点子树全灭(b)+ 2 = lb + rb + 2(当前操作次数)
            	3、左右子树只有根亮,需要2次操作(根节点开关2,根节点开关3)。
            	也就是当前节点的C状态最少操作数 = lc + rc + 2
           		
           		然后将上面三种情况均可以实现全亮(a)进行取最小值(次数)
           	*/
           	
            a = Math.min(la + ra, lb + rb + 2, lc + rc + 2);
            
            //到达当前全灭的情况(b状态):
            /* 
            	1、左右子树全亮,需要1次操作(根节点开关2)。 
            	也就是在根节点亮的情况下, 左右全亮,只需要操作根节点开关一次即可关闭所有灯。
            	于示例二效果一样。 所以,是要在 la + ra + 1,至于子节点下如何取,就要通过递归计算返回上来了。	
            	
            	2、左右子树全不亮,需要1次操作(根节点开关1)。
            	在根节点,亮的情况下,只需要切换根节点的开关1即可实现。
            	也就是要达到左右子树b(全灭)情况 + 1 = lb + rb + 1
            	
            	3、左右子树只有根亮,需要1次操作(根节点开关1)。
            	子树均灭,只需要操作根节点的开关1,也就是仅根亮的情况下,要达到b(全灭)= lc + rc + 1。

				然后将上面三种情况均可以实现全灭(b)进行取最小值(次数)
            */
            
            b = Math.min(la + ra + 1, lb + rb + 1, lc + rc + 1);
            
			//到达仅根亮的情况(c):
            /*
            	1、左右子树全亮(la, ra),需要2次操作(左子树开关2,右子树开关2)。
            	也就是 c = la + ra + 2
            
	            2、左右子树全灭(lb, rb),无需操作。因为是在根亮的情况下,所以无需操作,即: c = lb + rb
	            
            	3、要在根节点亮,达成仅根亮(c),然后再需要2次操作(根节点开关2,根节点开关3)。
            	可以在达到左右子树仅根不亮(d)且 根节点亮 的情况下,执行根节点开关2。
            	将树变成仅当前根节点和根节点下的左右子树节点亮的情况,然后再执行根节点开关3,将当前根节点下的左右子节点灭掉。
            	 c = ld + rd + 2。比较难理解,故图解如下:
            	 
							1
						0		0
					1		1		1
					...
							 根节点开关2

							1
						1		1
					0		0		0
					...
							 根节点开关3
							
							1
						0		0
					0		0		0
			
            */
            
            c = Math.min(la + ra + 2, lb + rb, ld + rd + 2);
          
            //到达仅根不亮的情况:
            /* 
            	1、左右子树全亮,需要1次操作(根节点开关1)。d = la + ra + 1
            	
            	2、左右子树全不亮,需要1次操作(根节点开关2,反转根和子树状态)d = lb + rb + 1
            	
            	3. 根左右子树根不亮,左右子树的子树亮 需要1次操作(根节点开关3)。 d = ld + rd + 1。 情况和上面的c的第三种情况很像。
            */
            
            d = Math.min(la + ra + 1,lb + rb + 1,ld + rd + 1);
            
        }
        //接下来讨论节点灯不亮的情况, 具体逻辑也是和上面差不多,但是前提变成了根不亮的情况。情况均省略描述
        else {
            //到达当前全亮的情况:
            //1、左右子树全亮,需要1次操作(根1)
            //2、左右子树全不亮,需要1次操作(根2)
            //4、左右子树只有根不亮,需要1次操作(根3)
            
            a = Math.min(la + ra + 1,lb + rb + 1,ld + rd + 1)
            
            //到达当前全灭的情况:
            //1、左右子树全亮,需要2次操作(左2,右2)
            //2、左右子树全不亮,无需操作
            //4、左右子树只有根不亮,需要2次操作(根1,根3)
            
            b = Math.min(la + ra + 2,lb + rb,ld + rd + 2)
            
            //到达只有根亮的情况:
            //1、左右子树全亮,需要1次操作(根2)
            //2、左右子树全不亮,需要1次操作(根1)
            //3、左右子树只有根亮,需要1次操作(根3)
    
            c = Math.min(la + ra + 1,lb + rb + 1,lc + rc + 1)
            
            //到达只有根不亮的情况:
            //1、左右子树全亮,无需操作
            //2、左右子树全不亮,需要2次操作(左2,右2)
            //3、左右子树只有根亮,需要2次操作(根2,根3)
            
            d = Math.min(la + ra, lb + rb + 2, lc + rc + 2)
        }
        // 返回顶的几种情况次数
        return [a, b, c, d]
    }
    
    // 递归的第一次调用,返回根节点:全亮,全灭,仅根亮,仅根不亮中全灭的最小次数
    res = dfs(root)
  return res[1]
}

往期内容

< 每日算法 - Javascript解析:经典弹珠游戏 >

< 每日算法 - JavaScript解析:从尾到头打印链表 >

< JavaScript技术分享: 大文件切片上传 及 断点续传思路 >

< 每日知识点:关于Javascript 精进小妙招 ( Js技巧 ) >

你可能感兴趣的:(前端算法解析,每日算法,硬泡,JavaScript,算法,javascript,动态规划)