A general approach to backtracking questions in Java (Subsets, Permutations, Combination Sum, Palindrome Partitioning)

在一畝三分地上看到leetcode里有樣的summary,今天早上看了一下,下面是我的總結(jié)。

Subsets

下面的代碼是很久前寫的了,當(dāng)時(shí)是什么都不懂。。

再看這個(gè)代碼,發(fā)現(xiàn)有個(gè)奇怪的地方就是它的遞歸沒有顯式的Terminator(終止條件),這里的for循環(huán)就相當(dāng)于terminator,因?yàn)閟tart每次都會(huì)+1。所以for循環(huán)不滿足之后,dfs函數(shù)執(zhí)行完了就會(huì)自動(dòng)return,所有的return都可以看成終止條件,就算是函數(shù)執(zhí)行完了之后的return(void的話就是隱式return)也一樣。那么如果i不+1進(jìn)入下一次dfs,就會(huì)棧溢出。

另外,我以為解集會(huì)是[],[1],[2],[3]...這樣,其實(shí)是:

[[], [1], [1, 2], [1, 2, 3], [1, 3], [2], [2, 3], [3]]。

還是跟N queens那種一樣,畫個(gè)N * N的matrix來理解。
注意這里是有「歸去來兮」的,因?yàn)?code>cell進(jìn)入下一次遞歸時(shí)是同一個(gè)引用。

另外,這題跟普通dfs不同的是,普通dfs一般是走到max depth才backtracking,這題是沒有判斷,直接把遇到的解加到res里。那為什么不會(huì)出現(xiàn)重復(fù)解?因?yàn)閕每次都+1,不會(huì)走回頭路。類似右上三角(這么描述可能只有我自己能聽懂。。。)

--
Jun 16 review : 有點(diǎn)像二叉樹層序遍歷的遞歸,每次來到新的一層就新建list,但是這題沒有tree的level那么好區(qū)分,而是直接利用遞歸的棧來在以前的基礎(chǔ)上用for循環(huán)移動(dòng)數(shù)據(jù)并添加。這就是為什么結(jié)果是[[], [1], [1, 2], [1, 2, 3], [1, 3], [2], [2, 3], [3]] 這種,而且需要手動(dòng)恢復(fù)現(xiàn)場。

為什么1,2,3之后會(huì)連續(xù)remove兩次,然后add 3呢?因?yàn)榈谝唬叩降谌龑又筮M(jìn)入第四層遞歸發(fā)現(xiàn)for循環(huán)不滿足了,于是回到第三層,執(zhí)行dfs后面的代碼,remove掉末尾元素;第二,remove這句話完成之后,第三層的任務(wù)結(jié)束了,return void,回到第二層繼續(xù)執(zhí)行dfs后面的代碼,自然就又remove掉2了。然后第二層的指針指向3,繼續(xù)dfs。

    public List<List<Integer>> subsets(int[] nums) {
        List<List<Integer>> result = new ArrayList<>();
        List<Integer> cell = new ArrayList<>();
        dfs(nums, result , cell , 0);
        return result;
    }

    public void dfs(int [] nums , List<List<Integer>> result , List<Integer> cell , int start){
        //add 放在for的外面,new一個(gè)
        result.add(new ArrayList<>(cell));
        //start指針的作用,是在dfs的時(shí)候向著深處走
        for(int i = start ; i < nums.length ; i ++){
            cell.add(nums[i]);
            //for中進(jìn)行的dfs
            dfs(nums , result , cell , i+1);
            //backtracking
            cell.remove(cell.size()-1);
        }
    }

另,這題leetcode高票答案里還有用位運(yùn)算來做的。

Subsets II

這題跟Subsets I不同的地方是,它給的nums可以有重復(fù)元素了,比如[1,2,2]。按照上一題的解法,會(huì)出現(xiàn)這樣的解集:

[[], [1], [1, 2], [1, 2, 2], [1, 2], [2], [2, 2], [2]]

怎么避免有duplicated的解呢。其實(shí)跟Combination SumII很像,follow up也是不允許有重復(fù)解,當(dāng)時(shí)是先排序,然后判重如果有重復(fù)就調(diào)過。
瑪?shù)?這題也是一個(gè)同一個(gè)套路:

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; // skip duplicates
        tempList.add(nums[i]);
        backtrack(list, tempList, nums, i + 1);
        tempList.remove(tempList.size() - 1);
    }
} 

Permutations

Subsets是「組合」,Permutations是排列。
那么這題也容易想到,跟subsets相比,把進(jìn)入下一層dfs的i+1去掉就好了(同時(shí)要加上terminator哦)。但是這樣一來對(duì)于[1,2,3]這樣的nums,第一個(gè)解會(huì)變成[1,1,1]呀。怎么辦,增加一個(gè)數(shù)組來保存是否使用過(模擬HashMap),有點(diǎn)像圖的遍歷了。或者直接利用ArrayList的contains函數(shù)判重,如果有了就continue。但是我們知道鏈表的查找操作復(fù)雜度是O(n),HashMap是O(1),所以更推薦用前一種方法呀,雖然要進(jìn)行兩次恢復(fù)現(xiàn)場。

public List<List<Integer>> permute(int[] num) {
    if (num.length == 0) return null;
    List<Integer> cell = new ArrayList<>();
    List<List<Integer>> result = new ArrayList<>();
    return backtracking(num, cell, result, new boolean[num.length]);

}
public List<List<Integer>> backtracking(int[] nums, List<Integer> cell, List<List<Integer>> result, boolean[] used) {
    if (cell.size() == nums.length) {
        result.add(new ArrayList<>(cell));
        return null;
    }
    for (int i = 0; i < nums.length; i++) {
        if (!used[i]) {
            cell.add(nums[i]);
            used[i] = true;
            backtracking(nums, cell, result, used);
            cell.remove(cell.size()-1);
            used[i] = false;
        }
    }
    return result;
}

九點(diǎn)了。。先去上班了。。以后可能得早點(diǎn)。


Permutations II

我以為這題用I 里的contains的那種方法可以AC,結(jié)果發(fā)現(xiàn)用它的方法打印出來的是空集。。然后看了下,原來它是在templist里(而不是result里)判斷有沒有用過一個(gè)數(shù)字,那對(duì)于1,1,2這樣的nums,就永遠(yuǎn)找不到一個(gè)length==3的解了,所以res是空集。那可不可以直接在加入到res的時(shí)候判斷是否contains呢?當(dāng)然也是不行的了,因?yàn)槟愣颊也坏揭粋€(gè)合法的解可以加入result。

那么我又想到,如果可以的話,我可以用 permutations I的那種used標(biāo)記的代碼,然后在res add的地方判斷res解集里有沒有重復(fù)的解,沒有才添加。

//這說明result.contains判斷的不是cell的內(nèi)存地址啊,而是里面的元素
            if (!result.contains(cell))
                result.add(new ArrayList<>(cell));

我自己用[1,1,2]這樣的test case測試是沒問題的,但是當(dāng)解集是[1,1,0,0,1,-1,-1,1],leetcode就TLE了,我在IDE中跑發(fā)現(xiàn)這個(gè)解集已經(jīng)非常長, 目測有幾百個(gè),那么用O(n)來查找的話肯定很耗時(shí)。

應(yīng)該這樣:

if(used[i] || i > 0 && nums[i] == nums[i-1] && !used[i - 1]) continue;

兩種情形,
1是如果進(jìn)入下一層發(fā)現(xiàn)這個(gè)slot的數(shù)字在前面已經(jīng)用過了,就continue;
2是發(fā)現(xiàn)這個(gè)slot沒用過,但是這個(gè)slot和前一個(gè)slot的數(shù)字相等,而且前一個(gè)slot沒用過,continue(因?yàn)檫@樣的話 cell.add(nums[i])一定會(huì)把前一個(gè)slot的數(shù)字加進(jìn)去,這樣就重復(fù)了。例如[1,1,2]的test case。)

還有,不要忘了先排序!!
瑪?shù)拢试S重復(fù)的follow up一般都還有點(diǎn)思維難度的。

public List<List<Integer>> permuteUnique(int[] nums) {
    List<List<Integer>> list = new ArrayList<>();
    Arrays.sort(nums);
    backtrack(list, new ArrayList<>(), nums, new boolean[nums.length]);
    return list;
}

private void backtrack(List<List<Integer>> list, List<Integer> tempList, int [] nums, boolean [] used){
    if(tempList.size() == nums.length){
        list.add(new ArrayList<>(tempList));
    } else{
        for(int i = 0; i < nums.length; i++){
            if(used[i] || i > 0 && nums[i] == nums[i-1] && !used[i - 1]) continue;
            used[i] = true; 
            tempList.add(nums[i]);
            backtrack(list, tempList, nums, used);
            used[i] = false; 
            tempList.remove(tempList.size() - 1);
        }
    }
}

剩下的Combination Sum,Palindrome Partitioning單獨(dú)開文章寫了。

總結(jié)一下,這種for循環(huán)里的dfs一般就是用來列舉解集的(一串string,數(shù)組之類的可以用for遍歷的),同樣的題目還有n queens,word search之類的。非常典型。

睡覺。


最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容