遞歸回溯中的一些套路

從一個題說起

leetcode 39. 組合總和

class Solution {
    public List<List<Integer>> combinationSum(int[] candidates, int target) {
       
    }
}

首先題目要求返回的類型爲List<List<Integer>>,那麼我們就新建一個List<List<Integer>>作爲全局變量,最後將其返回。

class Solution {
    List<List<Integer>> lists = new ArrayList<>();
    public List<List<Integer>> combinationSum(int[] candidates, int target) {
       
        return lists;
    }
}

再看看返回的結構,List<List<Integer>>。因此我們需要寫一個包含List<Integer>的輔助函數,加上一些判斷條件,此時結構變成了

class Solution {
    List<List<Integer>> lists = new ArrayList<>();
    public List<List<Integer>> combinationSum(int[] candidates, int target) {
        if (candidates == null || candidates.length == 0 || target < 0) {
            return lists;
        }

        List<Integer> list = new ArrayList<>();
        process(candidates, target, list);
        return lists;
    }

    private void process(int[] candidates, int target, List<Integer> list) {
    

    }
}

重點就是如何進行遞歸。遞歸的第一步,當然是寫遞歸的終止條件啦,沒有終止條件的遞歸會進入死循環。那麼有 哪些終止條件呢?由於條件中說了都是正整數。因此,如果target<0,當然是要終止了,如果target==0,說明此時找到了一組數的和爲target,將其加進去。此時代碼結構變成了這樣。

class Solution {
    List<List<Integer>> lists = new ArrayList<>();
    public List<List<Integer>> combinationSum(int[] candidates, int target) {
        if (candidates == null || candidates.length == 0 || target < 0) {
            return lists;
        }

        List<Integer> list = new ArrayList<>();
        process(candidates, target, list);
        return lists;
    }

    private void process(int[] candidates, int target, List<Integer> list) {
        if (target < 0) {
            return;
        }
        if (target == 0) {
            lists.add(new ArrayList<>(list));
        }
       

    }
}

我們是要求組成target的組合。因此需要一個循環來進行遍歷。每遍歷一次,將此數加入list,然後進行下一輪遞歸。代碼結構如下。

class Solution {
    List<List<Integer>> lists = new ArrayList<>();
    public List<List<Integer>> combinationSum(int[] candidates, int target) {
        if (candidates == null || candidates.length == 0 || target < 0) {
            return lists;
        }

        List<Integer> list = new ArrayList<>();
        process(candidates, target, list);
        return lists;
    }

    private void process(int[] candidates, int target, List<Integer> list) {
        if (target < 0) {
            return;
        }
        if (target == 0) {
            lists.add(new ArrayList<>(list));
        } else {
            for (int i = 0; i < candidates.length; i++) {
                list.add(candidates[i]);
                //因爲每個數字都可以使用無數次,所以遞歸還可以從當前元素開始
                process( candidates, target - candidates[i], list);
      
            }
        }

    }
}

似乎初具規模,測試一把結果如下

結果差距有點大,爲何會出現如此大的反差。而且發現一個規律,後面的一個組合會包含前面一個組合的所有的數字,而且這些數加起來和target也不相等啊。原因出在哪呢?java中除了幾個基本類型,其他的類型可以算作引用傳遞。這就是導致list數字一直變多的原因。因此,在每次遞歸完成,我們要進行一次回溯。把最新加的那個數刪除。此時代碼結構變成這樣。

class Solution {
    List<List<Integer>> lists = new ArrayList<>();
    public List<List<Integer>> combinationSum(int[] candidates, int target) {
        if (candidates == null || candidates.length == 0 || target < 0) {
            return lists;
        }

        List<Integer> list = new ArrayList<>();
        process(candidates, target, list);
        return lists;
    }

    private void process(int[] candidates, int target, List<Integer> list) {
        if (target < 0) {
            return;
        }
        if (target == 0) {
            lists.add(new ArrayList<>(list));
        } else {
            for (int i = 0; i < candidates.length; i++) {
                list.add(candidates[i]);
                //因爲每個數字都可以使用無數次,所以遞歸還可以從當前元素開始
                process( candidates, target - candidates[i], list);
                list.remove(list.size() - 1);
            }
        }

    }
}

再測一把,結果如下,

還是不對。這次加起來都等於7了,和上次結果相比算是一個很大的進步了。分析下測試結果。不難能看出,本次結果的主要問題包含了重複的組合。爲什麼會有重複的組合呢?因爲每次遞歸我們都是從0開始,所有數字都遍歷一遍。所以會出現重複的組合。改進一下,只需加一個start變量即可。 talk is cheap, show me the code。代碼如下。

List<List<Integer>> lists = new ArrayList<>();

    public List<List<Integer>> combinationSum(int[] candidates, int target) {
        if (candidates == null || candidates.length == 0 || target < 0) {
            return lists;
        }

        List<Integer> list = new ArrayList<>();
        process(0, candidates, target, list);
        return lists;
    }

    private void process(int start, int[] candidates, int target, List<Integer> list) {
        //遞歸的終止條件
        if (target < 0) {
            return;
        }
        if (target == 0) {
            lists.add(new ArrayList<>(list));
        } else {
            for (int i = start; i < candidates.length; i++) {
                list.add(candidates[i]);
                //因爲每個數字都可以使用無數次,所以遞歸還可以從當前元素開始
                process(i, candidates, target - candidates[i], list);
                list.remove(list.size() - 1);
            }
        }

    }

最後梭哈一把。

代碼通過,但是效率並不高。本題有效果更好的動態規劃的解法。本文主要展示遞歸回溯,就不做具體介紹了。

 

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