力扣894:所有可能的满二叉树,思考递归、记忆化递归、动态规划的关联

894. 所有可能的满二叉树

满二叉树是一类二叉树,其中每个结点恰好有 0 或 2 个子结点。

返回包含 N 个结点的所有可能满二叉树的列表。 答案的每个元素都是一个可能树的根结点。

答案中每个树的每个结点都必须有 node.val=0。

你可以按任何顺序返回树的最终列表。

示例:

输入:7
输出:[[0,0,0,null,null,0,0,null,null,0,0],[0,0,0,null,null,0,0,0,0],[0,0,0,0,0,0,0],[0,0,0,0,0,null,null,null,null,0,0],[0,0,0,0,0,null,null,0,0]]

刚开始我们的解题思路可能是递归计算就完事了。

class Solution {
    public List<TreeNode> allPossibleFBT(int N) {
        List<TreeNode> list = new ArrayList<>();
        if(N == 1) {
            TreeNode tree = new TreeNode(0);
            list.add(tree);
            return list;
        }
        for(int i = 1; i < N; i+=2) {
            List<TreeNode> left = allPossibleFBT(i);  //先初次给左孩子分配一个节点
            List<TreeNode> right = allPossibleFBT(N-1-i);//对应的右孩子得到的结点就是总结点数量减去分配给左孩子的结点数减去根结点
            for(TreeNode l :left) {
                for(TreeNode r : right) {
                    //先将当前的根赋值,其次再将递归处理过的左右赋值,加入到数组当中
                    TreeNode root = new TreeNode(0);
                    root.left = l;
                    root.right = r;
                    list.add(root);
                }
            }
        }
        //最后返回当前递归栈处理的好的list
        return list;
        
    }

之后我们会发现很多计算过程都重复了,就犹如斐波那契数列的递归一样,但是经过记忆化处理就节省了大量的时间,(贴上斐波那契的代码有助于理解记忆化过程)伪代码如下

fibonacci(n)
  if n == 0 || n == 1
    return  F[n] = 1   //将1记忆在F[n]中并返回
  if F[n] 已计算完毕
    return F[n]
  return F[n] = fibonacci(n-2) + fibonacci(n-1)

所以对于这道题,我们需要将之前计算过的多少个结点可能有多少的排列组合利用起来,所以需要声明一个map,key为结点数,value存放当前结点(key)对应的多少种排列组合

class Solution {
    Map<Integer, List<TreeNode>> map = new HashMap();
    public List<TreeNode> allPossibleFBT(int N) { 
        if(map.containsKey(N)) {
            return map.get(N);
        }
        List<TreeNode> list = new ArrayList<>();
        if(N == 1) {
            TreeNode root = new TreeNode(0);
            list.add(root);
        } else {
            for(int i = 1; i < N; i+=2) {
                List<TreeNode> left = allPossibleFBT(i);   //左半边独自处理
                List<TreeNode> right = allPossibleFBT(N-1-i);    //右半边独自处理
                for(TreeNode l : left) {
                    for(TreeNode r : right) {
                        TreeNode root = new TreeNode(0);
                        root.left = l;
                        root.right = r;
                        list.add(root);
                    }
                }
            }
        }
        map.put(N, list);
        return list;
    }   
}

记忆化固然好,但是递归一直开辟的空间实际是占用系统给程序分配的一块空间,也就是栈,所以这道题我们可以使用动态规划的问题来解决,当然我是想过可能存在动态规划的方式,但是想不出来如何解决,看了看力扣的题解,大佬还是多啊,人家就提供了动态规划的方式解决,看完犹如醍醐灌顶,并将人家的解决思路也写上来作总结,让小伙伴们和以后的自己看一看。动态规划也是将算是的计算结果记录在内存中,需要时直接调用,与记忆化递归的方式不同点是:1、递归是从顶部开始调用自身一直到最终的底层,比如目前来说的这道题,递归直接从结点为7的时候开始解决,一直到结点为1时给出结果,并依次出栈到结点为3的结果再到结点为5的结果,最终到结点为7的最终答案,而动态规划是直接从1开始出发,计算出结果,下一步到结点数量为3的时候,在调用结点数量为1的计算结果的基础上接着调用,最终求出结果为7的答案。2、递归占用系统空间,而动态规划不需要临时开辟栈空间。所以能动态规划时,动态规划还是比较好的解决档案。我们还是以斐波那契数列的动态规划为切入点,理解从递归到动态规划的这个转变过程,伪代码如下:

fibonacci(n)
   F[0] = 1
   F[1] = 1
   for i 从2到n
     F[i] = F[i-2] + F[i-1]

所以我们想办法从结点数量为刚开始的时候开始计算,当结点数量为1的时候,肯定只有一种可能,所以dp[1]=1,当然满二叉树结点数量不可能为偶数,当N = 3时,一个根节点,左边是N = 1时的子树,右边是N= 3 - 1 - 1,所以也是N= 1的子树。
当N = 5时,一个根节点,左边可以是N = 1 或者N =3,相应的右边是N= 3或者N=1。这个计算让机器来执行,我们只需要想怎么让计算机实现这个过程就行了(当然我这种菜鸟是想不出来的),于是,上别人的代码

public List<TreeNode> allPossibleFBT(int N) {
        List<TreeNode> n1 = new ArrayList();
        if (N % 2 == 0) return n1;
        n1.add(new TreeNode(0));
        if (N == 1) return n1;
        
        int len = (N + 1) / 2;
        List<TreeNode>[] dp = new List[len];
        dp[0] = n1;
        
        for (int total = 3; total <= N; total += 2){
            List<TreeNode> nodes = new LinkedList();
            for (int leftCount = 1; leftCount < total; leftCount += 2){
                List<TreeNode> leftNodes = dp[leftCount / 2];
                List<TreeNode> rightNodes = dp[(total - leftCount - 1) / 2];
                
                for (TreeNode left : leftNodes){
                    for (TreeNode right : rightNodes){
                        TreeNode root = new TreeNode(0);
                        root.left = left;
                        root.right = right;
                        nodes.add(root);
                    }
                }
            }
            dp[total/2] = nodes;
        }
        return dp[len - 1];
    }

作者:w1sl1y
链接:https://leetcode-cn.com/problems/all-possible-full-binary-trees/solution/dong-tai-gui-hua-fa-by-w1sl1y/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

人家的方法dp数组除以2,我想了想不够直观,也不好理解,我只好在人家的基础上进行了一点点改造

class Solution {
 public List<TreeNode> allPossibleFBT(int N) {
        List<TreeNode> n1 = new ArrayList();
        if (N % 2 == 0) return n1;
        n1.add(new TreeNode(0));
        if (N == 1) return n1;
        
        List<TreeNode>[] dp = new List[N+1];
        for(int i = 0; i < dp.length; i++) {
            dp[i] = n1;   //先将dp里面的都初始化,之后由于是dp从底层实现,
                         //所以不必担心有脏数据(要是有脏数据,说明这个dp是有问题的,所以放心)
        }
        
        for (int total = 3; total <= N; total += 2){
            List<TreeNode> nodes = new LinkedList();
            for (int leftCount = 1; leftCount < total; leftCount += 2){
                List<TreeNode> leftNodes = dp[leftCount];
                List<TreeNode> rightNodes = dp[total - leftCount - 1];
                
                for (TreeNode left : leftNodes){
                    for (TreeNode right : rightNodes){
                        TreeNode root = new TreeNode(0);
                        root.left = left;
                        root.right = right;
                        nodes.add(root);
                    }
                }
            }
            dp[total] = nodes;
        }
        return dp[N];           //dp[N]就是最终的结果
    }

}

最终的结果出来了,也是正确的。后来我有想了想,上面的方法可能是循环数组都是+2,如果除以2的话,可以将数组空间压缩一下,不那么浪费空间。确实很厉害。

所以我们做题需要多思考,比如递归过程中,有没有多余的计算,有的话就把结果存放起来,不然太浪费计算过程。然后在考虑能不能动态规划。这也是对我的一个建议吧,如果哪位小伙伴有更好的建议,可以评论一下,咱们一块儿共同成长。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值