题目如下:给你一个整数 n
,请你找出所有可能含 n
个节点的 真二叉树 ,并以列表形式返回。答案中每棵树的每个节点都必须符合 Node.val == 0
。
答案的每个元素都是一棵真二叉树的根节点。你可以按 任意顺序 返回最终的真二叉树列表。
真二叉树 是一类二叉树,树中每个节点恰好有 0
或 2
个子节点。
我开始看这道题的题干尝试归类,首先,我们生成的二叉树是有多种可能的 ,但是无论哪种可能,树中的节点个数都必须是n,所以我联想到了回溯法,即个数不到n的话我们尝试添加左右节点(必须同时添加),再继续进行递归,但是很快我便发现了问题,这条路很难走通,之前在如背包问题等问题中使用回溯法,同样是给定一个限定值,但是在背包问题中,一个物品只会有取和不取两种情况(以不能重复取为例),我们取之后加上总重量,回溯时只需要减掉这个物品的重量即位不取的情况。但是在现在的问题中,一个节点在生成左右节点后,我们并不方便对其左右节点进行回溯,比如当前节点的左子节点继续生成左右孙子节点,那我们应该在什么时机回溯去为右子节点生成子树呢?到这里我发觉我的第一直觉并不可靠(或许是太久没碰算法题了)。
然后我开始另换思路,二叉树问题中最常用的不就是递归吗?很明显的是,一个真二叉树的左右子树也刚好必须是真二叉树,这就是一个很好的递归解法了,比如n=7时,左子树我们先取1个节点,右子树取7-1-1=5个节点,那不就刚好将n=7的问题转化成n=1和n=5的问题了吗?
更进一步的是,如果大家对动态规划有所了解的话,应该知道在递归的过程当中会出现很多次重复计算,而动态规划恰好可以解决这个问题。我们从n=1开始,一直求到n=n,过程当中的一些中间结果肯定是要保存到数组中防止重复计算。
另外一些细节是,我们不难发现,由于一个节点要么不生成子节点,生成的话左右子节点必须同时生成,因此n必须为奇数,不满足的话我们就可以直接返回空集合。还有一点,因为各种情况非常多,我们防止各种情况之间的节点互相影响(之前已经指好指针的指向被无意中改变),我们需要在出结果时对临时的生成树进行完全复制(应该可以说是深拷贝吧)。
代码与注释如下所示:
package main
type TreeNode struct {
Val int
Left *TreeNode
Right *TreeNode
}
// 考虑动态规划,节点从1到n,每个int的key对应一个数组记录该个数的满二叉树
var record map[int][]*TreeNode
func allPossibleFBT(n int) []*TreeNode {
//偶数直接返回空集合
if n%2 == 0 {
return nil
}
//go的初始化
record = make(map[int][]*TreeNode)
for i := 1; i <= n; i += 2 {
record[i] = make([]*TreeNode, 0)
}
//Dynamic Program动态规划求出结果
DP(n)
//直接返回我们最终需要的结果
return record[n]
}
func DP(n int) {
node := &TreeNode{Val: 0}
//record[1]是动态规划过程中的起始量,手动赋值
record[1] = append(record[1], copyTree(node))
//这层循环目的是求record[i]
for i := 3; i <= n; i += 2 {
// 给左右分配i-1个,可以从1开始分配左,左j,右i-1-j
for j := 1; j < i-1; j += 2 {
//左树有record[j]种不同可能,右树有record[i-1-j]种不同可能,总共的可能数通过两层遍历做笛卡尔积
for x := 0; x < len(record[j]); x++ {
for y := 0; y < len(record[i-1-j]); y++ {
node.Left = record[j][x]
node.Right = record[i-1-j][y]
record[i] = append(record[i], copyTree(node))
}
}
}
}
}
// 对满足要求是的临时生成树进行深拷贝,前序遍历进行深拷贝
func copyTree(root *TreeNode) *TreeNode {
if root == nil {
return nil
}
newRoot := &TreeNode{Val: 0}
newRoot.Left = copyTree(root.Left)
newRoot.Right = copyTree(root.Right)
return newRoot
}
// 可以自行补充用例测试
func main() {
}
最后运行通过,但是貌似用时和内存击败的用户不多,大家对于这种思路和解法怎么看?有没有什么针对性的优化方式呢?欢迎大家提出建议交流!