回溯法


回溯法

全排列系列

46題:

給定一個沒有重復數字的序列,返回其所有可能的全排列。

示例:
輸入: [1,2,3]
輸出:
[
  [1,2,3],
  [1,3,2],
  [2,1,3],
  [2,3,1],
  [3,1,2],
  [3,2,1]
]
代碼:
public List<List<Integer>> permute(int[] nums) {
   List<List<Integer>> list = new ArrayList<>();
   // Arrays.sort(nums); // 不必先排序
   backtrack(list, new ArrayList<>(), nums);
   return list;
}

private void backtrack(List<List<Integer>> list, List<Integer> tempList, int [] nums){
   if(tempList.size() == nums.length){
      list.add(new ArrayList<>(tempList));
   } else{
      for(int i = 0; i < nums.length; i++){ 
         if(tempList.contains(nums[i])) continue; // 元素已經存在,跳過
         tempList.add(nums[i]);
         backtrack(list, tempList, nums);
         tempList.remove(tempList.size() - 1);
      }
   }
} 

47題:

給定一個可包含重復數字的序列,返回所有不重復的全排列。

示例:
輸入: [1,1,2]
輸出:
[
  [1,1,2],
  [1,2,1],
  [2,1,1]
]
代碼:
 public List<List<Integer>> permuteUnique(int[] nums) {
        List<List<Integer>> res=new ArrayList<>();
        List<Integer> temp=new ArrayList<>();
        boolean[] used=new boolean[nums.length]; //指示該值是否已經添加到列表中
        Arrays.sort(nums);     //對數組排序,確保可以跳過相同的值
        helper(res,temp,used,nums);
        return res;

    }
    public void helper(List<List<Integer>> res,List<Integer> temp,boolean[] used,int[] nums){
        if (temp.size()==nums.length){
            res.add(new ArrayList<>(temp));
        }else {
            for (int i = 0; i <nums.length ; i++) {
                //列表中已經添加過這個位置的值,跳過
                if (used[i]) continue;
                //當一個數字與之前的數字具有相同的值時,我們只有在使用前一個數字時才能使用此數字
                if (i>0&&nums[i]==nums[i-1]&&!used[i-1]) continue;  
                
                used[i]=true;
                temp.add(nums[i]);
                helper(res,temp,used,nums);
                used[i]=false;
                temp.remove(temp.size()-1);
            }
        }
    }

子集系列

78題:

給定一組不含重復元素的整數數組 nums,返回該數組所有可能的子集。 說明:解集不能包含重復的子集。

示例:
輸入: nums = [1,2,3]
輸出:
[
  [3],
  [1],
  [2],
  [1,2,3],
  [1,3],
  [2,3],
  [1,2],
  []
]
代碼:
public List<List<Integer>> subsets(int[] nums) {
    List<List<Integer>> list = new ArrayList<>();
   // Arrays.sort(nums);//不必要
    backtrack(list, new ArrayList<>(), nums, 0);
    return list;
}

private void backtrack(List<List<Integer>> list , List<Integer> tempList, int [] nums, int start){
    //先存結果,遞歸邊界不用顯式確定,如果無法添加自然不會再遞歸
    list.add(new ArrayList<>(tempList));
    for(int i = start; i < nums.length; i++){
        tempList.add(nums[i]);
        backtrack(list, tempList, nums, i + 1);
        tempList.remove(tempList.size() - 1);
    }
}

另一種迭代方法

代碼:
public List<List<Integer>> subsets(int[] nums) {
        List<List<Integer>> result = new ArrayList<>();
        result.add(new ArrayList<>());
        for(int n : nums){
            int size = result.size();
            for(int i=0; i<size; i++){
                List<Integer> subset = new ArrayList<>(result.get(i));
                subset.add(n);
                result.add(subset);
            }
        }
        return result;
    }
解釋:

在迭代所有數字時,對於每個新數字,我們可以選擇它,也可以不選擇它
1,如果選擇,只需將當前編號添加到每個現有子集。
2,如果沒有選擇,只保留所有現有的子集。
我們只是將兩者結合起來。

例如,{1,2,3}在內部我們有一個結果集[[]]
考慮1,如果不使用它,仍然[],如果使用1,將它添加到[],所以我們現在有[1]
結合它們,現在我們有[[],[1]]作為所有可能的子集

接下來考慮2,如果不使用它,我們仍然有[[],[1]],如果使用2,只需在每個前面的子集中加2,我們有[2],[1,2]
結合他們,現在我們有[[],[1],[2],[1,2]]

接下來考慮3,如果不使用它,我們仍然有[[],[1],[2],[1,2]],如果使用3,只需在每個前面的子集中加3,我們[[3], [1,3],[2,3],[1,2,3]]
結合它們,現在我們有[[],[1],[2],[1,2],[3],[1,3],[2,3],[1,2,3]]

90題:

給定一個可能包含重復元素的整數數組 nums,返回該數組所有可能的子集(冪集)。說明:解集不能包含重復的子集。

示例:

輸入: [1,2,2]
輸出:
[
  [2],
  [1],
  [1,2,2],
  [2,2],
  [1,2],
  []
]

代碼:

public List<List<Integer>> subsetsWithDup(int[] nums) {
    List<List<Integer>> list = new ArrayList<>();
    Arrays.sort(nums);//排序必要,跳過重復
    backtrack(list, new ArrayList<>(), nums, 0);
    return list;
}

private void backtrack(List<List<Integer>> list, List<Integer> tempList, int [] nums, int start){
    list.add(new ArrayList<>(tempList));
    for(int i = start; i < nums.length; i++){
        if(i > start && nums[i] == nums[i-1]) continue; // 跳過重復元素(剪枝)
        tempList.add(nums[i]);
        backtrack(list, tempList, nums, i + 1);
        tempList.remove(tempList.size() - 1);
    }
} 

另一種迭代方法:(在78題迭代法上改進)

代碼:
public List<List<Integer>> subsetsWithDup(int[] nums) {
        Arrays.sort(nums);  //不要忘了排序
        List<List<Integer>> result = new ArrayList<>();
        result.add(new ArrayList<>());
        int size=0;
        for(int j=0;j<nums.length;j++){
            
            int start =(j>=1&&nums[j]==nums[j-1])?size:0;  //定起始位置,這里的size還沒更新,所以是上一次迭代后的結果數目
            size=result.size();  //size用來保存當前結果數目
            for(int i=start; i<size; i++){
                List<Integer> subset = new ArrayList<>(result.get(i));
                subset.add(nums[j]);
                result.add(subset);
            }
        }
        return result;
    }

組合系列

組合 77題:給定兩個整數 n 和 k,返回 1 ... n 中所有可能的 k 個數的組合。

示例:

輸入: n = 4, k = 2
輸出:
[
  [2,4],
  [3,4],
  [2,3],
  [1,2],
  [1,3],
  [1,4],
]

代碼:

public List<List<Integer>> combine(int n, int k) {
        List<List<Integer>> res=new ArrayList<>();
        helper(res, new ArrayList<Integer>(),1,n,k);
        return res;

    }
    private void helper(List<List<Integer>> res,List<Integer> templist,int start,int n,int k){
        if (k==0){
            res.add(new ArrayList<>(templist));
            return;
        }
        for (int i=start;i<=n;i++){
            templist.add(i);
            helper(res,templist,i+1,n,k-1);
            templist.remove(templist.size()-1);
        }
    }

組合總和系列:

組合總和1:39題:

給定一個無重復元素的數組** candidates** 和一個目標數 target ,找出 candidates 中所有可以使數字和為 target 的組合。

candidates 中的數字可以無限制重復被選取

說明:所有數字(包括 target)都是正整數。解集不能包含重復的組合。

示例:
輸入: candidates = [2,3,5], target = 8,
所求解集為:
[
  [2,2,2,2],
  [2,3,3],
  [3,5]
]
代碼:
public List<List<Integer>> combinationSum(int[] candidates, int target) {
        Arrays.sort(candidates);       //先排序便於跳過大於target的數字
        List<List<Integer>> res=new ArrayList<>();      
        getRes(res,new ArrayList<Integer>(),candidates,target,0);
        return res;

    }
    private void getRes(List<List<Integer>> res, List<Integer> cur,int[] candidates,int target,int start){
        if (target>0){
            for (int i=start;i<candidates.length&&target>=candidates[i];i++){     //從start開始往后找
                cur.add(candidates[i]);
                getRes(res,cur,candidates,target-candidates[i],i);
                cur.remove(cur.size()-1);  //調用返回后及時清除
            }
        }else if (target==0){
            res.add(new ArrayList<>(cur));  //此處不能res.add(cur) 只能添加cur的副本,不然對cur的remove會改變res中相應添加的項
        }
    }

組合總和2 :40題:

題目同1,但是candidates 中的每個數字在每個組合中只能使用一次。

示例:
輸入: candidates = [10,1,2,7,6,1,5], target = 8,
所求解集為:
[
  [1, 7],
  [1, 2, 5],
  [2, 6],
  [1, 1, 6]
]
代碼:
public List<List<Integer>> combinationSum2(int[] candidates, int target) {
        Arrays.sort(candidates);
        List<List<Integer>> res=new ArrayList<>();
        getRes(res,new ArrayList<Integer>(),candidates,target,0);
        return res;

    }
    private void getRes(List<List<Integer>> res, List<Integer> cur,int[] candidates,int target,int start){
        if (target>0){
            for (int i=start;i<candidates.length&&target>=candidates[i];i++){
                if (i>start&&candidates[i]==candidates[i-1]){ continue;}  //跳過數組中的重復元素(剪枝) 注意從當前start開始
                cur.add(candidates[i]);
                getRes(res,cur,candidates,target-candidates[i],i+1);   //注意是i+1 不能重復使用數組中的元素
                cur.remove(cur.size()-1);
            }
        }else if (target==0){
            res.add(new ArrayList<>(cur));
        }
    }

組合總和3:216題:

找出所有相加之和為 n 的 k 個數的組合。組合中只允許含有 1 - 9 的正整數,並且每種組合中不存在重復的數字。

示例:
輸入: k = 3, n = 9
輸出: [[1,2,6], [1,3,5], [2,3,4]]
代碼:
public List<List<Integer>> combinationSum3(int k, int n) {
    int[] num = {1,2,3,4,5,6,7,8,9};
    List<List<Integer>> result = new ArrayList<List<Integer>>();
    helper(result, new ArrayList<Integer>(), num, k, n,0);
    return result;
    }

public void helper(List<List<Integer>> result, List<Integer> list, int[] num, int k, int target, int start){
    if (k == 0 && target == 0){
        result.add(new ArrayList<Integer>(list));
    } else {
        for (int i = start; i < num.length && target > 0 && k >0; i++){
            list.add(num[i]);
            helper(result, list, num, k-1,target-num[i],i+1);
            list.remove(list.size()-1);
        }
    }
}

涉及字符串的回溯問題

括號生成 22題:

給出 n 代表生成括號的對數,請你寫出一個函數,使其能夠生成所有可能的並且有效的括號組合。例如,給出 n = 3,生成結果為:

[
  "((()))",
  "(()())",
  "(())()",
  "()(())",
  "()()()"
]

思路:最初的想法是生成所有的組合情況,再寫輔助函數判斷是否合法。但這么做不必要,因為給定了括號的對數,只要最后生成的左右括號數都等於括號對數就是合法。可以在遞歸參數中引入左右括號各自的計數器,來記錄數量,在當前Stirng中字符數量達到2*括號對數時,添加結果到list。

代碼:

 public List<String> generateParenthesis(int n) {
        List<String> l=new ArrayList<>();
        backcrack(l,"",0,0,n);
        return l;

    }
    public void backcrack(List<String> l,String current,int open,int close,int max){
        if (current.length()==2*max){
            l.add(current);
        }else {
            if (open<max){   //max為括號對數
                backcrack(l,current+'(',open+1,close,max);
            }
            if (close<open){//注意這里 只有右括號的數量小於左括號的數量,才可以加右括號
                backcrack(l,current+')',open,close+1,max);
            }
        }
    }
    //只生成合法的情況。這里的current相當於數組回溯問題中的List<Integer> templist

注意!:java里邊String對象是不可變的,也就是說current+'('不是簡單的在原來的current指向的String對象后面加上'(',而是又新生成了一個current+'(',和原來的不是一個了。

電話號碼的字母組合 17題:

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

示例:

輸入:"23"
輸出:["ad", "ae", "af", "bd", "be", "bf", "cd", "ce", "cf"].

代碼:

private static final String[] KEYS = { "", "", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz" };
    
    	public List<String> letterCombinations(String digits) {
           
    		List<String> ret = new LinkedList<String>();
             if(digits == null || digits.length() == 0) return ret;
    		combination("", digits, 0, ret);
    		return ret;
    	}
    
    	private void combination(String prefix, String digits, int offset, List<String> ret) {
    		if (offset >= digits.length()) {
    			ret.add(prefix);
    			return;
    		}
    		String letters = KEYS[(digits.charAt(offset) - '0')];
    		for (int i = 0; i < letters.length(); i++) {
    			combination(prefix + letters.charAt(i), digits, offset + 1, ret);
    		}
    	}
    	//這里定義的前綴prefix相當於數組中回溯問題的List<Integer> templist,都是作為遞歸函數的參數保存生成結果的。

復原IP地址 93題:

給定一個只包含數字的字符串,復原它並返回所有可能的 IP 地址格式。

示例:

輸入: "25525511135"
輸出: ["255.255.11.135", "255.255.111.35"]

注意:

什么是有效的IP地址格式?

代碼:

public List<String> restoreIpAddresses(String s) {
        List<String> res = new ArrayList<>();
        helper(s,"",res,0);
        return res;
    }
    public void helper(String s, String tmp, List<String> res,int n){
        if(n==4){
            if(s.length()==0) res.add(tmp.substring(0,tmp.length()-1));
            //substring here to get rid of last '.'
            return;
        }
        for(int k=1;k<=3;k++){
            if(s.length()<k) continue;  //剪枝
            int val = Integer.parseInt(s.substring(0,k));
            if(val>255 || k!=String.valueOf(val).length()) continue;
            /*in the case 010 the parseInt will return len=2 where val=10, but k=3, skip this.*/
            helper(s.substring(k),tmp+s.substring(0,k)+".",res,n+1);
            //每次遞歸,s都是傳的k起始的一個子串,這樣傳參更簡便,不用麻煩地傳s下標
        }
    }

分割回文串 131題:

給定一個字符串 s,將 s 分割成一些子串,使每個子串都是回文串。返回 s 所有可能的分割方案。

示例:

輸入: "aab"
輸出:
[
  ["aa","b"],
  ["a","a","b"]
]

代碼:

   public List<List<String>> partition(String s) {
        List<List<String>> res=new ArrayList<>();
        List<String> cur=new ArrayList<>();
        helper(res,s,cur,0,s.length());
        return res;
    }
    //判斷回文字符串的輔助函數
    public boolean isPalindrome(String s){
        int n=s.length()-1,i=0;
        while (i<n){
            if (s.charAt(i)!=s.charAt(n)){
                return false;
            }
            i++;
            n--;
        }
        return true;

    }
    
    //回溯函數
    public List<List<String>> helper(List<List<String>> res, String s,List<String> cur,int start,int len){
        if (start==len){
            res.add(new ArrayList<>(cur));
        }
        for (int i=start+1;i<=len;i++){
           if (isPalindrome(s.substring(start,i))){
               cur.add(s.substring(start,i));
               helper(res,s,cur,i,len);
               cur.remove(cur.size()-1);
           }
        }
        return res;
    }

圖解:

其他問題:

單詞搜索 79題:

給定一個二維網格和一個單詞,找出該單詞是否存在於網格中。
單詞必須按照字母順序,通過相鄰的單元格內的字母構成,其中“相鄰”單元格是那些水平相鄰或垂直相鄰的單元格。同一個單元格內的字母不允許被重復使用。

示例:

board =
[
  ['A','B','C','E'],
  ['S','F','C','S'],
  ['A','D','E','E']
]

給定 word = "ABCCED", 返回 true.
給定 word = "SEE", 返回 true.
給定 word = "ABCB", 返回 false.

分析:實際上這道題屬於DFS 在一個二維數組里面做查找

代碼:

static boolean[][] visited;
    public boolean exist(char[][] board, String word) {
        visited = new boolean[board.length][board[0].length];
        
        for(int i = 0; i < board.length; i++){
            for(int j = 0; j < board[i].length; j++){
                if((word.charAt(0) == board[i][j]) && search(board, word, i, j, 0)){
                    return true;
                }
            }
        }
        
        return false;
    }
    
    private boolean search(char[][]board, String word, int i, int j, int index){
        if(index == word.length()){
            return true;     //查找到最后 說明找到 返回
        }
        
        if(i >= board.length || i < 0 || j >= board[i].length || j < 0 || board[i][j] != word.charAt(index) || visited[i][j]){
        //下標越界,當前搜索位置的字母與目標字母不同,當前位置字母已經訪問過 這些情況都屬於沒找到,返回flase
            return false;
        }
        
        visited[i][j] = true;    //置為已訪問
        if(search(board, word, i-1, j, index+1) || 
           search(board, word, i+1, j, index+1) ||
           search(board, word, i, j-1, index+1) || 
           search(board, word, i, j+1, index+1)){
            return true;   //有一路找到就直接返回就行 最終一定是找到了 就不用管visited是不是置回false了
        }
        
        visited[i][j] = false;    //遞歸回溯后記得再置回未訪問 以便再找另一路
        return false;  //進行到這一步還沒return 這一路是最終沒找到
    }

格雷編碼 89題:

格雷編碼是一個二進制數字系統,在該系統中,兩個連續的數值僅有一個位數的差異。
給定一個代表編碼總位數的非負整數 n,打印其格雷編碼序列。格雷編碼序列必須以 0 開頭。

示例:

輸入: 2
輸出: [0,1,3,2]
解釋:
00 - 0
01 - 1
11 - 3
10 - 2

對於給定的 n,其格雷編碼序列並不唯一。
例如,[0,2,3,1] 也是一個有效的格雷編碼序列。

00 - 0
10 - 2
11 - 3
01 - 1
輸入: 0
輸出: [0]
解釋: 我們定義格雷編碼序列必須以 0 開頭。
     給定編碼總位數為 n 的格雷編碼序列,其長度為 2n。當 n = 0 時,長度為 20 = 1。
     因此,當 n = 0 時,其格雷編碼序列為 [0]。

分析:這題標簽是回溯法,但實際上回溯不是最好的辦法。

代碼:

public List<Integer> grayCode(int n) {
    List<Integer> rs=new ArrayList<Integer>();
    rs.add(0);
    for(int i=0;i<n;i++){
        int size=rs.size();
        for(int k=size-1;k>=0;k--)
            rs.add(rs.get(k) | 1<<i);  //或者寫成 rs.add(rs.get(k)+(1<<i))
    }
    return rs;
}


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM