1. 程式人生 > 其它 >回溯演算法小結(java)

回溯演算法小結(java)

技術標籤:數字結構和演算法javaleetcode資料結構演算法

回溯演算法小結

1.回溯演算法定義:

回溯法採用試錯的思想,它嘗試分步的去解決一個問題。在分步解決問題的過程中,當它通過嘗試發現現有的分步答案不能得到有效的正確的解答的時候,它將取消上一步甚至是上幾步的計算,再通過其它的可能的分步解答再次嘗試尋找問題的答案。回溯法通常用最簡單的遞迴方法來實現,在反覆重複上述的步驟後可能出現兩種情況:

找到一個可能存在的正確的答案;
在嘗試了所有可能的分步方法後宣告該問題沒有答案;

(來自維基百科)

1.1 回溯演算法和深度優先遍歷

回溯演算法也叫 回溯搜尋演算法,「搜尋」即「搜尋所有的解」。回溯演算法從初始狀態出發,採用 深度優先遍歷

的方式,得到問題的 所有的解。因為採用遍歷的方式,所以可以得到所有的解。回溯在某種程度上也是暴力搜尋。

1.2 回溯演算法適用範圍

題目常見的形式就是問某一個問題的所有解決方案。如果解決一個問題有多個解決方案,每一個解決方案有多個步驟,題目要求我們得到所有的解,就可以使用回溯演算法。多個解決方案,每一個解決方案有多個步驟,通常可以建模成一個 樹形問題。而樹形問題中有著很明顯的遞迴結構,因此 回溯演算法遞迴地建立了區域性的可能的解決方案,當發現一個可能的解決方案無法得出正確的結果時,回退到上一步,嘗試下一個可能的解決方案 ,這裡的 **「回退」就是「回溯」**的意思。

1.3 例題

46. 全排列

public class permute {
    public List<List<Integer>> permute(int[] nums) {
        int len=nums.length;
        List<List<Integer>> res=new ArrayList<>();
        if(len==0) return res;
        Deque<Integer> path=new ArrayDeque<>();
        boolean[] used=
new boolean[len]; backTrack(nums,len,0,path,used,res); return res; } /** * * @param nums * @param len * @param index 當前需要確定的path中的元素的下標 * @param path 記錄當前的路徑 * @param used 記錄已經被選擇的數字 * @param res */ public void backTrack(int[] nums,int len,int index,Deque<Integer> path,boolean[] used,List<List<Integer>> res){ // 遞迴終止條件 if(index==len) { res.add(new ArrayList<>(path)); return; } for (int i = 0; i < len; i++) { if(used[i]) continue; used[i]=true; path.offerLast(nums[i]); backTrack(nums,len,index+1,path,used,res); path.pollLast(); used[i]=false; } } }

113. 路徑總和 II

public class pathSum {
    List<List<Integer>> res = new ArrayList<>();
    Deque<Integer> path = new ArrayDeque<>();
    public List<List<Integer>> pathSum(TreeNode root, int sum) {
        if (root == null) return res;
        backTrack(root, sum);
        return res;
    }

    /**
     *
     * @param root
     * @param sum
     */

    public void backTrack(TreeNode root, int sum) {
        // 首先考慮終止條件
        if (root==null) {
            return;
        }
        path.offerLast(root.val);
        sum-=root.val;
        if(root.left==null && root.right==null &&sum==0) res.add(new ArrayList<>(path));
        backTrack(root.left,sum);
        backTrack(root.right,sum);
        path.pollLast();
    }

    public static void main(String[] args) {
        TreeNode root = new TreeNode(5);
        root.left = new TreeNode(4);

    }
}

2. 剪枝

回溯演算法其實是一個遍歷的演算法,通過遍歷搜尋所有的解 其實是沒有技巧的,並且時間複雜度很高。因此在遍歷的時候,如果能夠提前知道 即將要遍歷分支 不能搜尋到符合條件的結果,這一分支就可以跳過,這一步操作就像是在一棵樹上剪去一個枝葉,因此稱為 剪枝。在我個人看來,剪枝一般體現在backTrack函式中,for迴圈遍歷所有元素時,新增一個if,滿足某種條件的直接continue

47. 全排列 II

public class permuteUnique {
    List<List<Integer>> res=new ArrayList<>();
    Deque<Integer> path=new ArrayDeque<>();
    public List<List<Integer>> permuteUnique(int[] nums) {
        int len=nums.length;
        if(len==0) return res;
        Arrays.sort(nums);
        boolean[] used=new boolean[len];
        backTrack(nums,len,0,used);
        return res;
    }
    public void backTrack(int[] nums,int len,int index,boolean[] used){
        if(len==index){
            res.add(new ArrayList<>(path));
            return;
        }
        for (int i = 0; i < len; i++) {
            if(used[i]) continue;
            // 剪枝條件
            if(i>0 && nums[i]==nums[i-1] && !used[i-1]) continue;
            used[i]=true;
            path.offerLast(nums[i]);
            backTrack(nums,len,index+1,used);
            path.pollLast();
            used[i]=false;
        }
    }
    List<List<Integer>> res = new ArrayList<>();
    Deque<Integer> path = new ArrayDeque<>();

    public List<List<Integer>> combinationSum(int[] candidates, int target) {
        int len = candidates.length;
        if (len == 0) return res;
        backTrack(candidates, target, len ,0);
        return res;

    }

    public void backTrack(int[] candidates, int target, int len, int begin) {
        if (target == 0) {
            res.add(new ArrayList<>(path));
            return;
        }
        for (int i = begin; i < len; i++) {
            if (target < candidates[i]) return;

            path.offerLast(candidates[i]);
            backTrack(candidates, target-candidates[i], len ,i);

            path.pollLast();
        }

39. 組合總和

public class combinationSum {
    List<List<Integer>> res = new ArrayList<>();
    Deque<Integer> path = new ArrayDeque<>();

    public List<List<Integer>> combinationSum(int[] candidates, int target) {
        int len = candidates.length;
        if (len == 0) return res;
        backTrack(candidates, target, len ,0);
        return res;

    }

    public void backTrack(int[] candidates, int target, int len, int begin) {
        if (target == 0) {
            res.add(new ArrayList<>(path));
            return;
        }
        for (int i = begin; i < len; i++) {
            if (target < candidates[i]) return;

            path.offerLast(candidates[i]);
            backTrack(candidates, target-candidates[i], len ,i);

            path.pollLast();
        }
    }

    public static void main(String[] args) {
        int[] candidates = {2, 3, 6, 7};
        combinationSum cs = new combinationSum();
        List<List<Integer>> lists = cs.combinationSum(candidates, 7);
        System.out.println(lists);
    }
}

注意理解begin在裡面所起到的作用:

  • 沒用begin時,candidates = [2,3,6,7], target = 7,答案:image-20201211205239381

  • 使用後答案:image-20201211205153544

區別在於前者把同樣元素的List當成不同答案了。

打個比方,當2開頭的方案被遍歷完了之後,方法中的begin相當於+1了,所以從3開始的方案中,不再重複出現前面出現過的2.

注意,這裡就是此題可以用begin排除重複方案的原因。用used陣列可以起到一樣的效果,但是這裡begin相對佔用的時間和空間更小。

40. 組合總和 II

public class combinationSum2 {
    List<List<Integer>> res = new ArrayList<>();
    Deque<Integer> path = new ArrayDeque<>();

    public List<List<Integer>> combinationSum2(int[] candidates, int target) {
        int len = candidates.length;
        if (len == 0) return res;
        Arrays.sort(candidates);

        backTrack(candidates, target, len ,0);
        return res;

    }

    public void backTrack(int[] candidates, int target, int len, int begin) {
        if (target == 0) {
            res.add(new ArrayList<>(path));
            return;
        }
        for (int i = begin; i < len; i++) {
            if (target < candidates[i]) return;
            // 剪枝 有重複數字導致的重複方案
            if(i>begin && candidates[i]==candidates[i-1]) continue;
            path.offerLast(candidates[i]);
            // i+1是因為不能重複使用
            backTrack(candidates, target-candidates[i], len ,i+1);

            path.pollLast();
        }
    }

    public static void main(String[] args) {
        int[] candidates = {10,1,2,7,6,1,5};
        combinationSum2 cs = new combinationSum2();
        List<List<Integer>> lists = cs.combinationSum2(candidates, 8);
        System.out.println(lists);
    }
}

注意思考剪枝過程的那一句是怎麼起作用的,為什麼前面直接return 但是後面用continue

if(i>begin && candidates[i]==candidates[i-1]) continue;

因為,該陣列事先排序過了,當target<candidates[i]後,無論i遍歷到後面的哪個元素,都成立,所以可以直接return。

第二個主要是刪除重複方案,不代表後面的i組成的方案都不成立。

---- 未完 待續 內容主要參考自https://leetcode-cn.com/leetbook/read/learning-algorithms-with-leetcode/9el6vj/

if(i>begin && candidates[i]==candidates[i-1]) continue;

因為,該陣列事先排序過了,當target<candidates[i]後,無論i遍歷到後面的哪個元素,都成立,所以可以直接return。

第二個主要是刪除重複方案,不代表後面的i組成的方案都不成立。

---- 未完 待續 內容主要參考自https://leetcode-cn.com/leetbook/read/learning-algorithms-with-leetcode/9el6vj/