二叉树问题中的动态规划

本文介绍了如何使用递归和动态规划解决LeetCode894题,生成所有含n个节点的真二叉树,重点讨论了回溯法的挑战、递归策略以及如何避免重复计算。作者提到n必须为奇数,并强调了深拷贝以防止节点影响
摘要由CSDN通过智能技术生成

题目来源于leetcode894:所有可能的真二叉树

题目如下:给你一个整数 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() {

}

          最后运行通过,但是貌似用时和内存击败的用户不多,大家对于这种思路和解法怎么看?有没有什么针对性的优化方式呢?欢迎大家提出建议交流!

  • 6
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值