力扣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的話,可以將數組空間壓縮一下,不那麼浪費空間。確實很厲害。

所以我們做題需要多思考,比如遞歸過程中,有沒有多餘的計算,有的話就把結果存放起來,不然太浪費計算過程。然後在考慮能不能動態規劃。這也是對我的一個建議吧,如果哪位小夥伴有更好的建議,可以評論一下,咱們一塊兒共同成長。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章