「力扣嘉年华」的中心广场放置了一个巨型的二叉树形状的装饰树。每个节点上均有一盏灯和三个开关。节点值为 0 表示灯处于「关闭」状态,节点值为 1 表示灯处于「开启」状态。每个节点上的三个开关各自功能如下:
给定该装饰的初始状态 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、左右子树全不亮,需要2次操作(左子树开关2,右子树开关2)
//3、左右子树只有根亮,需要2次操作(根开关2,根开关3)
//到达当前全灭的情况:
//1、左右子树全亮,需要1次操作(根开关2)
//2、左右子树全不亮,需要1次操作(根开关1)
//3、左右子树只有根亮,需要1次操作(根开关3)
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技巧 ) >