算法套路學習之回溯算法-----子集,排列,組合等問題如何解決

來源:https://www.cnblogs.com/labuladong/p/12320463.html
我是看這個大佬的博客學習到的,下面我自己總結一下,並且記錄一下遇到的使用回溯的題目,驗證一下這個套路的可行性。一直比較討厭這種題,因爲沒有總結出一套框架來。

一.需要思考的問題

解決一個回溯算法題,只需要以下三點:

  1. 路徑:也就是已經做出的選擇。
  2. 選擇列表:也就是你當前可以做的選擇
  3. 結束條件:也就是到達決策樹底層,無法再做選擇的條件

其實我看到這個也是一臉懵逼的,但是後來看例子就明白了,下面是算法的框架:

result = []
def backtrack(路徑, 選擇列表):
    if 滿足結束條件:
        result.add(路徑)
        return
    
    for 選擇 in 選擇列表:
        做選擇
        backtrack(路徑, 選擇列表)
        撤銷選擇

簡單解釋:首先result是在這個函數之外,屬於一個全局的變量,是保存結果的,這個函數比較重要。


二.例題

1.全排列

給定一個 沒有重複 數字的序列,返回其所有可能的全排列。這是LeetCode的46題,非常常見,我做了幾次還是經常寫不對,這太難受了,原理文章中已經講得很清楚,下面我自己實現一下:

class Solution {
    List<List<Integer>> res = new LinkedList<>();
    public List<List<Integer>> permute(int[] nums) {
         // 記錄「路徑」
        LinkedList<Integer> track = new LinkedList<>();
        backtrack(nums, track);
        return res;
    }

    private void backtrack(int[] nums, LinkedList<Integer> track){
        //寫結束條件,就是路徑長度跟選擇列表一樣的時候
        if(track.size() == nums.length){
            res.add(new LinkedList(track));
            return;
        }

        for (int i = 0; i < nums.length; i++) {
        //如果已經選了就過濾掉
        if (track.contains(nums[i]))
            continue;
        //做選擇
        track.add(nums[i]);

        //進入下一層決策樹
        backtrack(nums, track);

        //取消選擇
        track.removeLast();
    }
    }
}

解釋一下:

這是決策數結構:
在這裏插入圖片描述

  1. 首先建立一個成員變量res,是一個嵌套的List,對應上面框架的result,這就是結果
  2. 確定什麼是路徑,什麼是路徑呢,其實就是我這個結果其中一個List,這個List保存着一條完整的路徑,也就是一個排列,比如【1,2,3】這三個數其中一個排列是3,2,1,這個【3,2,1】就是一條路徑,它是結果的一部分
  3. 重點說一下選擇和選擇列表這塊。在這個題裏頭,選擇是什麼呢,其實就是選不選這個數,比如我現在剛選完1,下面可以選擇2和3,【2,3】就是選擇列表,1已經不能選了,因爲已經選過了,所以通過continue過濾掉。

2.子集問題

LeetCode地址

我現在感覺做這種題要先把決策樹畫出來:
在這裏插入圖片描述

然後看代碼:

class Solution {
	//這是保存結果的嵌套List
    List<List<Integer>> res = new LinkedList<>();
    public List<List<Integer>> subsets(int[] nums) {
    	//路徑
        LinkedList<Integer> track = new LinkedList<>();
        backtrack(nums,track,0);
        return res;
    }

    private void backtrack(int[] nums, LinkedList<Integer> track, int start){
    	//結束條件
        res.add(new LinkedList<>(track));
		
		//for循環 遍歷選擇列表
        for(int i = start; i< nums.length; i++){
            track.add(nums[i]);

            backtrack(nums, track, i+1);

            track.removeLast();
        }
    }
}

代碼跟剛纔的題非常相似 下面說一下解釋:

  1. 路徑:路徑是什麼,這題的路徑就是一個子集,因爲答案是求子集總合,其中一個路徑就是一個子集
  2. 選擇列表:也就是當前可以做的選擇,假設我們走在第一步,也就是空的時候,可以直接加到結果裏頭(空集也是子集),也可以選擇1 ,2,3,所以選擇列表是從start=0開始遍歷到結束。在方法裏把start不停累加,這樣能選的就會越來越少,符合預期。
  3. 結束條件:這道題沒有判斷,每次調用backtrack就相當於到了一個新的樹節點,可以直接加進去

3.全排列 II

LeetCode地址
這個題和全排列類似,都是找出全排列的結果,但是加上了一個條件:不重複的

代碼:

class Solution {
	//跟上面一樣
    List<List<Integer>> res = new LinkedList<>();
    
    public List<List<Integer>> permuteUnique(int[] nums) {
    	//一樣的track
        LinkedList<Integer> track = new LinkedList<>();
        //先排序方便判斷
        Arrays.sort(nums);
        backtrack(nums, new boolean[nums.length], track);
        return res;
    }

    private void backtrack(int[] nums, boolean[] visited, LinkedList<Integer> track){
        if(track.size() == nums.length){
            res.add(new LinkedList<>(track));
            return;
        }
         //選擇列表
        for(int i = 0; i<nums.length; i++){
            //已經選擇過的不需要再放進去了
            if(visited[i]) continue;
            //接下來,如果當前節點與他的前一個節點一樣,並其他的前一個節點已經被遍歷過了,那我們也就不需要了。
            if(i>0 && nums[i] == nums[i-1] && visited[i-1]) continue;
            //做出選擇
            track.add(nums[i]);
            visited[i] = true;

            backtrack(nums,visited,track);

            //撤銷選擇
            track.removeLast();
            visited[i] = false;
        }

    }
}

本題和第一題大致一樣,關鍵是 for 選擇 in 選擇列表這塊有點難。怎麼做呢,就是要知道當前位於的節點有多少選擇,應不應該選,所以需要維護一個visited[]數組,已經選過了的就不要再選了(全排列),並且如果當前節點與他的前一個節點一樣,並其他的前一個節點已經被遍歷過了,那我們也就不需要了。實際上就是加了兩個判斷,過濾掉 然後選擇 / 回溯/ 撤銷

4.子集 II

這題就是原題加了個條件:不能重複

什麼樣的情況下會出現重複呢,比如數組是【1,2,2】,假如我選了【1】,【2】,這時候再選【2】就重複了,假如我選了【1,2】,下次本來要回溯去選第三個數,發現這個數也是2,跟前一個一樣,那麼就不選了

結論:當這個數和前一個數一樣那麼就不選了,注意這裏要判斷,就是當前的i下標一定要大於開始的索引,才過濾,否則就選不出【1,2,2】這個子集了。

class Solution {
    List<List<Integer>> res = new LinkedList<>();
    public List<List<Integer>> subsetsWithDup(int[] nums) {
        LinkedList<Integer> track = new LinkedList<>();
        Arrays.sort(nums);
        backtrack(track, nums, 0);
        return res;
    }

    private void backtrack(LinkedList<Integer> track, int[] nums,int start){
        res.add(new LinkedList<>(track));

        for(int i = start; i< nums.length; i++){
            if(i > start && nums[i]==nums[i-1]){//這裏最重要
                continue;
            }

            track.add(nums[i]);

            backtrack(track, nums, i + 1);

            track.removeLast();
        }
    }
}

5.組合

Leetcode鏈接

同樣的,先畫出決策樹

在這裏插入圖片描述
代碼如下:

class Solution {
    List<List<Integer>> res = new LinkedList<>();
    public List<List<Integer>> combine(int n, int k) {
        LinkedList<Integer> track = new LinkedList<>();
        backtrack(n, k, track, 1);
        return res;
    }

    private void backtrack(int n, int k, LinkedList<Integer> track,int start){
        if(track.size() == k){
            res.add(new LinkedList<>(track));
        }

        for(int i = start; i <= n; i++){
            track.add(i);

            backtrack(n, k, track, i+1);

            track.removeLast();
        }

    }
}

這個和前幾道題非常的相似,基本上一樣

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