回溯算法(深度優(yōu)先)

總結(jié)

解決一個回溯問題,實際上就是一個決策樹的遍歷過程。你只需要思考 3 個問題:
1、路徑:也就是已經(jīng)做出的選擇。
2、選擇列表:也就是你當前可以做的選擇。
3、結(jié)束條件:也就是到達決策樹底層,無法再做選擇的條件。

代碼方面,回溯算法的框架:

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

全排列

給定一個 沒有重復 數(shù)字的序列,返回其所有可能的全排列。
示例:
輸入: [1,2,3]
輸出:
[
[1,2,3],
[1,3,2],
[2,1,3],
[2,3,1],
[3,1,2],
[3,2,1]
]

回溯算法1:
路徑:ArrayList<Integer> per,包含所有整數(shù)(不重復)的集合就是一條路徑;
選擇列表:nums和tags決定,所有未選擇過的整數(shù),都可以選擇;
結(jié)束條件:per中的整數(shù)個數(shù)達到最大;
第一種算法的缺點為需要額外的tags來標識nums中的整數(shù)是否已經(jīng)被選擇過,第二種則不需要。時間負責度為O(n*n!)。

public static List<List<Integer>> permute2(int[] nums) {

        List<List<Integer>> pers = new ArrayList<>();
        ArrayList<Integer> per = new ArrayList<>();

        boolean[] tags = new boolean[nums.length];

        backtrack2(pers, per, nums, tags);

        return pers;
    }

    public static void backtrack2(List<List<Integer>> pers, ArrayList<Integer> per, int[] nums, boolean[] tags) {

        if (per.size() == nums.length) {
            pers.add(new ArrayList<>(per));
            return;
        }

        for (int i = 0; i < nums.length; i++) {
            if(!tags[i]){
                per.add(nums[i]);
                tags[i] = true;
                backtrack2(pers, per, nums, tags);
                per.remove(per.size()-1);
                tags[i] = false;
            }
        }

    }

回溯算法2:
將這個問題可以看作有 n 個的空格,我們需要從左往右依此填入題目給定的 n 個數(shù),每個數(shù)只能使用一次。為了在選擇下一個填入的數(shù)時不重復,我們需要將已經(jīng)填過的數(shù)放在數(shù)組的左邊,未填的數(shù)放在數(shù)組的右邊。
具體來說,假設(shè)我們已經(jīng)填到第 pos 個位置,那么nums[] 數(shù)組中 [0, pos?1] 是已填過的數(shù)的集合,[pos , n?1] 是待填的數(shù)的集合。我們肯定是嘗試用 [pos, n?1] 里的數(shù)去填第 pos 個數(shù),假設(shè)待填的數(shù)的下標為 i ,那么填完以后我們將第 i 個數(shù)和第 pos 個數(shù)交換,即能使得在填第 pos 個數(shù)的時候 nums[] 數(shù)組的[0,pos ] 部分為已填過的數(shù),[pos +1,n?1] 為待填的數(shù),回溯的時候交換回來即能完成撤銷操作。

public class Solution {

    private List<List<Integer>> ans;
    private int[] nums;

    public List<List<Integer>> permuteUnique(int[] nums) {

        this.ans = new ArrayList<>();
        this.nums = nums;

        List<Integer> path = new ArrayList<>();
        for (int num : nums) {
            path.add(num);
        }

        backtrack(path, 0);

        return ans;
    }

    public void backtrack(List<Integer> path, int pos) {
        if (pos == nums.length - 1) {
            ans.add(new ArrayList<>(path));
            return;
        }

        for (int i = pos; i < nums.length; i++) {
            Collections.swap(path, i, pos);
            backtrack(path, pos + 1);
            Collections.swap(path, i, pos);
        }
    }
    
}

全排列 II


為了去重,需要先排序;當相鄰兩個數(shù)相同,且前一個數(shù)剛剛被撤銷(即visit=false),這種情況下會導致重復的排列,需要剪枝。

    public List<List<Integer>> permuteUnique(int[] nums) {
        List<List<Integer>> ans = new ArrayList<>();
        if(nums.length == 0){
            return ans;
        }
        // 排序是去重的前提
        Arrays.sort(nums);
        backtrace(ans, new ArrayList<>(nums.length), nums, new boolean[nums.length]);
        return ans;
    }

    public void backtrace(List<List<Integer>> ans, List<Integer> path, int[] nums, boolean[] visit) {
        if (path.size() == nums.length) {
            ans.add(new ArrayList<>(path));
            return;
        }

        for (int i = 0; i < nums.length; i++) {
            if (i > 0 && nums[i] == nums[i - 1] && !visit[i - 1]) {
                continue;
            }
            if (!visit[i]) {
                visit[i] = true;
                path.add(nums[i]);
                backtrace(ans, path, nums, visit);
                visit[i] = false;
                path.remove(path.size() - 1);
            }
        }
    }

交換去重
交換去重不需要提前排序,也不需要額外的visit數(shù)組,所以推薦使用此種方式進行全排列問題的求解。

public class Solution {
    private List<List<Integer>> ans;
    private int[] nums;

    public List<List<Integer>> permuteUnique(int[] nums) {

        this.ans = new ArrayList<>();
        this.nums = nums;

        List<Integer> path = new ArrayList<>();
        for (int num : nums) {
            path.add(num);
        }

        backtrack(path, 0);

        return ans;
    }

    public void backtrack(List<Integer> path, int pos) {
        if (pos == nums.length - 1) {
            ans.add(new ArrayList<>(path));
            return;
        }

        Set<Integer> set = new HashSet<>();
        for (int i = pos; i < nums.length; i++) {
            // 去重,在給pos位置賦值時,如果前面已經(jīng)有過相同的數(shù)被賦值給pos,那么就過濾
            if(set.contains(path.get(i))){
                continue;
            }
            set.add(path.get(i));

            Collections.swap(path, i, pos);
            backtrack(path, pos + 1);
            Collections.swap(path, i, pos);
        }
    }
}

火柴拼正方形

還記得童話《賣火柴的小女孩》嗎?現(xiàn)在,你知道小女孩有多少根火柴,請找出一種能使用所有火柴拼成一個正方形的方法。不能折斷火柴,可以把火柴連接起來,并且每根火柴都要用到。

輸入為小女孩擁有火柴的數(shù)目,每根火柴用其長度表示。輸出即為是否能用所有的火柴拼成正方形。

示例 1:
輸入: [1,1,2,2,2]
輸出: true
解釋: 能拼成一個邊長為2的正方形,每邊兩根火柴。

求解:
1)相當于把所有的物體(火柴)往4個容量一樣(正方形邊長)的容器里放,最后所有的物體都放入容器中,且每個容器剛好滿,則成功;
2)對于每個物體往哪個容器里放,依次遍歷即可;如果可以放入該容器,并且可以導致最后的成功,則返回true;否則,放入下一個容器中。
3)如果火柴全部放完,此時如果四個容器大小一致,那么則成功;其實只要全部火柴放置完,根據(jù)前面的條件,此時四個容器大小必然一致;
4)先對火柴排序,從大數(shù)開始放,可以減少回溯的次數(shù);類似于裝箱問題的貪婪解法,先放置大體積的物品容易成功。
算法復雜度為O(N^4)。

public static boolean makesquare(int[] nums){

        int sum = 0;
        for(int num : nums){
            sum += num;
        }
        if(sum % 4 != 0){
            return false;
        }
        int edgeLen = sum / 4;
        if(edgeLen <= 0){
            return false;
        }

        // 排序后從大數(shù)開始裝桶, 會減少回溯次數(shù); 類似裝箱問題中, 先裝體積大的物品
        Arrays.sort(nums);
        int[] buckets = new int[4];
        return dfs(nums.length-1, nums, edgeLen, buckets);
    }

    public static boolean dfs(int ind, int[] nums, int len, int[] buckets){

        if(ind < 0){
            // return true; // 也正確
            return buckets[0] == len && buckets[1] == len && buckets[2] == len && buckets[3] == len;
        }

        for(int i = 0; i<4; i++){
            if(buckets[i] + nums[ind] > len){
                continue;
            }
            buckets[i] += nums[ind];
            if(dfs(ind-1, nums, len, buckets)){
                return true;
            }
            buckets[i] -= nums[ind];
        }

        return false;
    }

單詞拆分

給定一個非空字符串 s 和一個包含非空單詞列表的字典 wordDict,判定 s 是否可以被空格拆分為一個或多個在字典中出現(xiàn)的單詞。

說明:

拆分時可以重復使用字典中的單詞。
你可以假設(shè)字典中沒有重復的單詞。
示例 1:
輸入: s = "leetcode", wordDict = ["leet", "code"]
輸出: true
解釋: 返回 true 因為 "leetcode" 可以被拆分成 "leet code"。
示例 2:
輸入: s = "applepenapple", wordDict = ["apple", "pen"]
輸出: true
解釋: 返回 true 因為 "applepenapple" 可以被拆分成 "apple pen apple"。
注意你可以重復使用字典中的單詞。
示例 3:
輸入: s = "catsandog", wordDict = ["cats", "dog", "sand", "and", "cat"]
輸出: false

記憶化回溯(記錄子問題的解,避免重復計算)
我們可以使用記憶化的方法,其中一個 memo數(shù)組會被用來保存子問題的結(jié)果。每當訪問到已經(jīng)訪問過的后綴串,直接用 memo數(shù)組中的值返回而不需要繼續(xù)調(diào)用函數(shù)。
通過記憶化,許多冗余的子問題可以極大被優(yōu)化,回溯樹得到了剪枝,因此極大減小了時間復雜度。
時間復雜度O(N^2);空間復雜度O(N)

public static boolean wordBreak(String s, List<String> wordDict) {
        Boolean[] results = new Boolean[s.length()];
        return dfs(s, wordDict, 0, results);
    }

    public static boolean dfs(String s, List<String> wordDict, int start, Boolean[] result){

        if(start == s.length()){
            return true;
        }

        if(result[start] != null){
            return result[start];
        }

        for(int end = start + 1; end <= s.length(); end++){
            if(wordDict.contains(s.substring(start, end)) && dfs(s, wordDict, end, result)){
                result[start] = true; // 記錄答案, 以免重復計算子問題
                return true;
            }
        }

        result[start] = false;// 記錄答案, 以免重復計算子問題
        return false;
    }

組合總和

回溯(剪枝)
1)避免重復,需要設(shè)置一個起始下標start;
2)path全局只有一個引用,所以加入ans時需要new新對象。

    public List<List<Integer>> combinationSum(int[] candidates, int target) {
        List<List<Integer>> ans = new ArrayList<>();
        Arrays.sort(candidates);
        backtrace(ans, new ArrayList<>(), candidates, 0, target, 0);
        return ans;
    }

    public void backtrace(List<List<Integer>> ans, List<Integer> path, int[] candidates, int sum, int target, int start) {
        if (sum == target) {
            ans.add(new ArrayList<>(path));
        }

        // 剪枝
        if (sum > target) {
            return;
        }

        for (int i = start; i < candidates.length; i++) {
            // 剪枝
            if (sum + candidates[i] > target) {
                break;
            }

            // 選擇
            sum += candidates[i];
            path.add(candidates[i]);
            // 回溯
            backtrace(ans, path, candidates, sum, target, i);
            sum -= candidates[i];
            // 撤銷選擇
            path.remove(path.size() - 1);
        }

        return;
    }

組合總和 II

回溯解法
1)先不考慮任何剪枝的代碼,按照回溯框架寫出回溯代碼;
2)利用提前排序,進行各種剪枝;

private int[] candidates;
    private List<List<Integer>> ans;
    private int target;

    public List<List<Integer>> combinationSum2(int[] candidates, int target) {
        this.ans = new ArrayList<>();
        this.target = target;
        this.candidates = candidates;

        if(candidates.length == 0){
            return ans;
        }

        Arrays.sort(candidates);
        backtrace(new ArrayList<>(), 0, 0);
        return ans;
    }

    public void backtrace(List<Integer> integers, int sum, int k) {
        if (sum == target) {
            List<Integer> list = new ArrayList<>(integers);
            ans.add(list);
            return;
        }

        // 剪枝
        if(sum > target){
            return;
        }

        for (int i = k; i < candidates.length; i++) {

            // 剪枝
            if(sum + candidates[i] > target){
                break;
            }

            // 剪枝
            if(i > k && candidates[i] == candidates[i-1]){
                continue;
            }

            integers.add(candidates[i]);
            backtrace(integers, sum + candidates[i], i + 1);
            integers.remove(integers.size() - 1);
        }

    }

}

電話號碼的字母組合

給定一個僅包含數(shù)字 2-9 的字符串,返回所有它能表示的字母組合。
給出數(shù)字到字母的映射如下(與電話按鍵相同)。注意 1 不對應(yīng)任何字母。

示例:
輸入:"23"
輸出:["ad", "ae", "af", "bd", "be", "bf", "cd", "ce", "cf"].
說明:
盡管上面的答案是按字典序排列的,但是你可以任意選擇答案輸出的順序。

回溯

private static String[] cc = {"", "", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz"};

    public static List<String> letterCombinations(String digits) {

        if(digits.length() == 0){
            return new ArrayList<>();
        }

        List<String> ret = new ArrayList<>();

        List<String> words = new ArrayList<>();
        for(int i=0; i<digits.length(); i++){
            int index = digits.charAt(i) - '0';
            words.add(cc[index]);
        }

        backtrace(ret, new ArrayList<>(), words, 0);
        return ret;
    }

    public static void backtrace(List<String> ret, List<Character> comb, List<String> words, int wordInd){

        if(wordInd > words.size()-1){
            ret.add(listToString(comb));
            return;
        }

        String word = words.get(wordInd);
        for(int i=0; i<word.length(); i++){
            comb.add(word.charAt(i));
            backtrace(ret, comb, words, wordInd+1);
            comb.remove(comb.size()-1);
        }
    }

    public static String listToString(List<Character> comb){
        StringBuilder sb = new StringBuilder();
        for(int i=0; i<comb.size(); i++){
            sb.append(comb.get(i));
        }
        return sb.toString();
    }

復原IP地址


回溯算法
1)路徑:即構(gòu)成IP的4個數(shù)字的集合:segs,由分割點落下的位置決定;
2)選擇列表:即分割點落下的位置,或者每個seg結(jié)束的位置。所以需要保留每個分割點開始的位置索引:sInd,每次的seg結(jié)束位置選擇都有三個:sInd+1,sInd+2,sInd+3;
3)返回條件:集齊構(gòu)成IP的4個數(shù)字,且s被遍歷完;
同時注意剪枝:
1)如果發(fā)現(xiàn)選擇完一個數(shù)字后,后面的數(shù)字位數(shù)明顯不可能滿足IP的條件,直接break;
2)三位數(shù)不能超過255;兩位數(shù)或者三位數(shù)不能以'0'開頭;

    public static List<String> restoreIpAddresses(String s) {
        List<String> ips = new ArrayList<>();
        backtrace(ips, s, new ArrayList<>(), 0);
        return ips;
    }

    public static void backtrace(List<String> ips, String s, List<String> segs, int sInd){

        if(segs.size() ==  4){
            if(sInd == s.length()){
                ips.add(String.join(".", segs));
            }
            return;
        }

        for(int end = sInd + 1; end <= sInd + 3 && end <= s.length(); end++){

            String ipSeg = s.substring(sInd, end);

            // 剪枝
            int leftLength = s.length() - end;
            int leftCount = 4 - segs.size() - 1;
            if(leftLength < leftCount || leftLength > leftCount * 3){
                continue;
            }

            // 三位數(shù)不能超過255
            if(end == sInd + 3 && ipSeg.compareTo("255") > 0){
                break;
            }

            // 二位數(shù)/三位數(shù)不能以0開頭
            if(end > sInd + 1 && ipSeg.startsWith("0")){
                break;
            }

            segs.add(ipSeg);
            backtrace(ips, s, segs, end);
            segs.remove(segs.size()-1);
        }

    }

括號生成

回溯算法
剪枝:
1)左括號可選條件:只要左括號還有剩余,就可以選擇;
2)有括號可選條件:目前已經(jīng)選擇的左括號的數(shù)量大于右括號的數(shù)量,所以需要兩個變量分別保存已經(jīng)選擇的左括號的數(shù)量和右括號的數(shù)量。

class Solution {
    public List<String> generateParenthesis(int n) {
        List<String> ans = new ArrayList();
        backtrack(ans, new StringBuilder(), 0, 0, n);
        return ans;
    }

    public void backtrack(List<String> ans, StringBuilder cur, int open, int close, int max){
        if (cur.length() == max * 2) {
            ans.add(cur.toString());
            return;
        }

        if (open < max) {
            cur.append('(');
            backtrack(ans, cur, open+1, close, max);
            cur.deleteCharAt(cur.length() - 1);
        }
        if (close < open) {
            cur.append(')');
            backtrack(ans, cur, open, close+1, max);
            cur.deleteCharAt(cur.length() - 1);
        }
    }
}

路徑總和 II

    public static List<List<Integer>> pathSum(TreeNode root, int sum) {
        List<List<Integer>> paths = new ArrayList<>();
        if(root == null){
            return paths;
        }
        List<Integer> path = new ArrayList<>();
        path.add(root.val);
        dfs(paths, path, sum, root.val, root);
        return paths;
    }

    public static void dfs(List<List<Integer>> paths, List<Integer> path, int target, int sum, TreeNode root){

        if(sum == target && root.left == null && root.right == null){
            paths.add(new ArrayList<>(path));
            return;
        }

        if(root.left != null){
            sum += root.left.val;
            path.add(root.left.val);
            dfs(paths, path, target, sum, root.left);
            sum -= root.left.val;
            path.remove(path.size()-1);
        }

        if(root.right != null){
            sum += root.right.val;
            path.add(root.right.val);
            dfs(paths, path, target, sum, root.right);
            sum -= root.right.val;
            path.remove(path.size()-1);
        }
    }

路徑總和 III


回溯算法

  • 前綴和:題中所求部分路徑之和 = 全部路徑之和 - 前綴路徑之和;即 target = pathSum - prefixSum;所以遍歷到當前節(jié)點時,只需判斷截止到當前節(jié)點的pathSum ?= target + prefixSum??紤]到前綴和可能有相同值,所以需要記錄數(shù)量。
  • 回溯:回到當前層時,需要恢復前綴和Map。
    private int count;

    public int pathSum(TreeNode root, int sum) {
        if(root == null){
            return 0;
        }
        count = 0;
        Map<Integer, Integer> prefixSumCount = new HashMap<>();
        prefixSumCount.put(0, 1);
        backtrace(root, prefixSumCount, 0, sum);
        return count;
    }

    public void backtrace(TreeNode node, Map<Integer, Integer> prefixSumCount, int pathSum, int target){
        if(node == null){
            return;
        }

        pathSum += node.val;
        count += prefixSumCount.getOrDefault(pathSum - target, 0);
        prefixSumCount.put(pathSum, prefixSumCount.getOrDefault(pathSum, 0) + 1);

        backtrace(node.left, prefixSumCount, pathSum, target);
        backtrace(node.right, prefixSumCount, pathSum, target);

        prefixSumCount.put(pathSum, prefixSumCount.get(pathSum) - 1);
    }

組合

回溯

    public List<List<Integer>> combine(int n, int k) {
        List<List<Integer>> ans = new ArrayList<>();
        backtrace(ans, new ArrayList<>(), n, 0, k);
        return ans;
    }

    public void backtrace(List<List<Integer>> ans, List<Integer> path, int n, int num, int k){
        if(path.size() == k){
            ans.add(new ArrayList<>(path));
            return;
        }
        // 剪枝:當后面所有的數(shù)的個數(shù)都加入path,仍然小于k個,可以返回
        if(k - path.size() > n - num){
            return;
        }

        for(int i = num + 1; i <= n; i ++){
            path.add(i);
            backtrace(ans, path, n, i, k);
            path.remove(path.size()-1);
        }
    }

安卓系統(tǒng)手勢解鎖


回溯
1)列舉出下一步棋可能的方向:總共8組,16個方向;容易漏掉馬字格的8個方向。


2)每個方向上,判斷下一步落腳點是否已經(jīng)被訪問,如果已經(jīng)訪問過,那么在這個方向上繼續(xù)一步,直至在這個方向上找到一個未訪問的落腳點。如果找到,那么可以作為下一步的位置;如果超出范圍仍未找到,那么這個方向無效。(實際上每個方向上最多連續(xù)走兩步)
3)技巧:涉及走棋的題目,往往涉及到數(shù)字和方格中位置的轉(zhuǎn)換技巧,通過除法取余和取整可以分別得到行號和列號。

    由數(shù)字得到位置: num => [(num-1)/3][(num-1)%3]
    由位置得到數(shù)字: [i][j] => i * 3 + j + 1;

4)利用對稱進行優(yōu)化,本題從1開始的手勢總數(shù)和從3,7,9開始是一樣的;從2開始的手勢總數(shù)和從4,6,8開始是一樣的;剩下最后一種從5開始的。

public class Solution {

    private int total;

    // 16 個方向
    public static int[][] directions = {
            {0, 1}, {0, -1},
            {1, 0}, {-1, 0},
            {1, 1}, {-1, -1},
            {-1, 1}, {1, -1},
            {1, 2}, {-1, -2},
            {2, 1}, {-2, -1},
            {-1, 2}, {2, -1},
            {1, -2}, {-2, 1}};
    
    public int numberOfPatterns(int m, int n) {
        int ans = 0;
        ans += getNumberOfPatterns(1, m, n) * 4; // 1,3,7,9
        ans += getNumberOfPatterns(2, m, n) * 4; // 2,4,6,8
        ans += getNumberOfPatterns(5, m, n); // 5
        return ans;
    }

    public int getNumberOfPatterns(int start, int m, int n){
        total = 0;
        boolean[] visit = new boolean[10];
        visit[start] = true;
        backtrace(1, start, visit, m, n);
        return total;
    }

    public void backtrace(int currentCount, int currentNum, boolean[] visit,  int m, int n) {

        if (currentCount >= m && currentCount <= n) {
            total++;
        }

        if (currentCount >= n) {
            return;
        }

        int currentRow = (currentNum - 1) / 3;
        int currentCol = (currentNum - 1) % 3;

        for (int[] direction : directions) {
            int nextRow;
            int nextCol;
            int tempRow = currentRow;
            int tempCol = currentCol;
            // 確定這個方向上是否有滿足條件的下一個落腳點
            boolean hasNextPos = true;
            while (true){
                nextRow = tempRow + direction[0];
                nextCol = tempCol + direction[1];
                if(nextRow >= 0 && nextRow <= 2 && nextCol >= 0 && nextCol <= 2){
                    int nextNum = nextRow * 3 + nextCol + 1;
                    if(!visit[nextNum]){
                        break;
                    }
                    tempRow = nextRow;
                    tempCol = nextCol;
                }else {
                    hasNextPos = false;
                    break;
                }
            }
            // 回溯迭代
            if(hasNextPos){
                int nextNum = nextRow * 3 + nextCol + 1;
                visit[nextNum] = true;
                currentCount++;
                backtrace(currentCount, nextNum, visit, m, n);
                currentCount--;
                visit[nextNum] = false;
            }
        }
    }

二叉樹中和為某一值的路徑


回溯

  • 正向思維,記錄當前路徑的和;注意作為參數(shù)的變量,在回溯調(diào)用之前和之后必須保持一致,所以在添加path到ans的時候,將當前節(jié)點的值加入的是剛new出來的rightAns中,而不是先加入path,再付給rightAns。
   public List<List<Integer>> pathSum(TreeNode root, int sum) {
        List<List<Integer>> ans = new ArrayList<>();
        if(root == null){
            return ans;
        }
        dfs(ans, new ArrayList<>(), root, 0, sum);
        return ans;
    }

    public void dfs(List<List<Integer>> ans, List<Integer> path, TreeNode current, int pathSum, int sum){
        if(pathSum + current.val == sum && current.left == null && current.right == null){
            List<Integer> rightAns = new ArrayList<>(path);
            rightAns.add(current.val);
            ans.add(rightAns);
            return;
        }

        if(current.left != null){
            path.add(current.val);
            dfs(ans, path, current.left, pathSum + current.val, sum);
            path.remove(path.size()-1);
        }

        if(current.right != null){
            path.add(current.val);
            dfs(ans, path, current.right, pathSum + current.val, sum);
            path.remove(path.size()-1);
        }
    }
  • 逆向思維,記錄剩下的路徑值。
    public void dfs(List<List<Integer>> ans, List<Integer> path, TreeNode current, int diff){
        if(current == null){
            return;
        }

        path.add(current.val);
        diff -= current.val;

        if(diff == 0 && current.left == null && current.right == null){
            ans.add(new ArrayList<>(path));
        }

        dfs(ans, path, current.left, diff);
        dfs(ans, path, current.right, diff);
        path.remove(path.size()-1);
    }

可能的二分法


深度優(yōu)先
這里是純深度優(yōu)先算法,沒有任何回溯,也不需要回溯;

  • dislikes關(guān)系可以用鄰接矩陣表示;
  • 數(shù)字分組的過程可以映射成給數(shù)字染色的過程,相鄰節(jié)點(數(shù)字)之間必須異色;
  • 可以從1到N,逐一進行深度優(yōu)先染色,dfs函數(shù)表示:給某個數(shù)字染成某種顏色,是否能夠成功;如果從1到N都染色成功,則返回true;否則,返回false;
  • dfs函數(shù)在給某個數(shù)字染色的時候,需要將鄰居染成異色;如果染色之前發(fā)現(xiàn)自己已經(jīng)是異色了,則矛盾,返回false;如果發(fā)現(xiàn)自己已經(jīng)是同色了,則已經(jīng)完成了染色,返回true;
  • 技巧:可以使用數(shù)字1和-1表示異色,0表示未染色,這樣可以使用一個數(shù)組保存所有節(jié)點染色情況。
public boolean possibleBipartition(int N, int[][] dislikes) {
        List<Integer>[] table = new ArrayList[N + 1];
        for (int i = 1; i <= N; i++) {
            table[i] = new ArrayList<>();
        }
        for (int i = 0; i < dislikes.length; i++) {
            int[] dis = dislikes[i];
            int num0 = dis[0];
            int num1 = dis[1];
            table[num0].add(num1);
            table[num1].add(num0);
        }
        int[] colored = new int[N+1]; // 0未染色, 1和-1代表相反的兩種顏色
        for(int number = 1; number <= N; number ++){
            if(colored[number] != 0){ // 已經(jīng)染過色, 下一個
                continue;
            }
            // 對number進行深度優(yōu)先染色: 注意既然前面所有的節(jié)點的深度優(yōu)先染色都沒有使number著色, 
            // 說明這個number與所有number都不是鄰居關(guān)系(即dislike關(guān)系),所以這里隨便染成1或者-1都是正確的
            if(!dfs(number, 1, colored, table)){
                return false;
            }
        }
        return true;
    }

    // 深度優(yōu)先染色: color為-1/+1
    public boolean dfs(int number, int color, int[] colored, List<Integer>[] table) {
        if(colored[number] == -color){
            return false;
        }
        if(colored[number] == color){
            return true;
        }
        colored[number] = color; // 染色
        for(int neighbor : table[number]){
            if(!dfs(neighbor, -color, colored, table)){
                return false;
            }
        }
        return true;
    }

子集 II

  • 同層去重
i > index && nums[i] == nums[i-1]
  • 剪枝
  • 按子集個數(shù)遍歷回溯函數(shù)
    public List<List<Integer>> subsetsWithDup(int[] nums) {
        List<List<Integer>> ans = new ArrayList<>();
        Arrays.sort(nums);
        for(int count = 0; count <= nums.length; count++){
            backtrace(ans, new ArrayList<>(), count, nums, 0);
        }
        return ans;
    }

    public void backtrace(List<List<Integer>> ans, List<Integer> sub, int count, int[] nums, int index){
        if(sub.size() == count){
            ans.add(new ArrayList<>(sub));
            return;
        }

        // 剪枝
        if(sub.size() + nums.length - index < count){
            return;
        }

        for(int i=index; i<nums.length; i++){
            // 同層去重
            if(i > index && nums[i] == nums[i-1]){
                continue;
            }
            sub.add(nums[i]);
            backtrace(ans, sub, count, nums, i+1);
            sub.remove(sub.size()-1);
        }
    }

字符串的排列


回溯算法

  • 關(guān)鍵是如何通過剪枝去重:排序后去重:前后相鄰的元素如果相同,那么這些相同的連續(xù)元素中,只需要固定一次;一般選擇固定第一個元素,后面相同的元素不再固定。程序中判斷是否是第一個固定的元素,通過判斷前一個元素是否訪問過決定:如果前一個相同的元素已經(jīng)訪問過,說明是這些相同的元素都是第一次固定,否則就是第二次固定;
            if (i > 0 && chars[i] == chars[i - 1] && !visit[i - 1]) {
                continue;
            }
    public String[] permutation(String s) {
        char[] chars = s.toCharArray();
        Arrays.sort(chars);
        List<String> ans = new ArrayList<>();
        backtrace(ans, new StringBuilder(), chars, new boolean[s.length()]);
        String[] ret = new String[ans.size()];
        for (int i = 0; i < ans.size(); i++) {
            ret[i] = ans.get(i);
        }
        return ret;
    }

    public void backtrace(List<String> ans, StringBuilder sb, char[] chars, boolean[] visit) {
        if (sb.length() == chars.length) {
            String s = new StringBuilder(sb).toString();
            ans.add(s);
            return;
        }

        for (int i = 0; i < chars.length; i++) {
            if (visit[i]) {
                continue;
            }
            // 去重(剪枝)
            if (i > 0 && chars[i] == chars[i - 1] && !visit[i - 1]) {
                continue;
            }
            sb.append(chars[i]);
            visit[i] = true;
            backtrace(ans, sb, chars, visit);
            visit[i] = false;
            sb.deleteCharAt(sb.length() - 1);
        }
    }
  • 另一種更加高效的寫法(算法的時間復雜度一樣),即另一種實現(xiàn)操作:是利用交換元素的操作,代替上面sb.append的操作。這種swap的操作在很多算法或技巧的實現(xiàn)中都用到了,所以建議必須掌握。
  • 原理:想要得到某數(shù)組的一個全排列,一種做法是新建一個數(shù)組,然后從原數(shù)組中依次取元素到新數(shù)組空間中,待原數(shù)組的元素都取光了,新數(shù)組就是原數(shù)組的一個全排列,可以用visit數(shù)組記錄原數(shù)組哪些元素被取過,哪些元素沒有被取過。另一種做法是,不用新建一個數(shù)組,在原數(shù)組進行操作:依次固定第i個元素,固定的方法是從(i~length-1)元素中任取一個位置x上的元素放到第i個元素的位置上(即swap(i, x)),當固定到最后一個元素時,就得到原數(shù)組的一個全排列。
  • 具體的過程為
    1)固定第0個元素,從(0~length-1)中選取位置x,然后交換元素swap(0, x),即將位置x上的元素放置到位置0處;
    2)然后固定第1個元素,因為第0個元素已經(jīng)被固定了,所以第1個元素的候選者只能從(1~length-1)中選取位置x,然后交換元素swap(1, x),即將位置x上的元素放置到1處;
    3)依次類推,當固定到第length-1個元素時,候選者只剩第length-1位置上的元素了,此刻得到原數(shù)組的全排列。
  • 代碼如下,幾點說明
    1)backtrace方法的參數(shù)為pos,作用即為固定pos位置的數(shù);
    2)這里去重的方法就比較容易理解多了:pos位置的數(shù)如果選取到了重復的數(shù),則過濾;而且不再需要將原數(shù)組進行排序。
public class Solution {

    List<String> res = new LinkedList<>();
    char[] c;

    public String[] permutation(String s) {
        c = s.toCharArray();
        backtrace(0);
        return res.toArray(new String[res.size()]);
    }

    void backtrace(int pos) {
        if (pos == c.length - 1) {
            res.add(String.valueOf(c));
            return;
        }
        HashSet<Character> set = new HashSet<>();
        for (int i = pos; i < c.length; i++) {
            if (set.contains(c[i])) {
                continue; // 重復,因此剪枝
            }
            set.add(c[i]);
            swap(i, pos); // 交換,將 c[i] 固定在第 pos 位
            backtrace(pos + 1); // 開啟固定第 pos + 1 位字符
            swap(i, pos); // 恢復交換
        }
    }

    void swap(int a, int b) {
        char tmp = c[a];
        c[a] = c[b];
        c[b] = tmp;
    }

}

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容