在一畝三分地上看到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之類的。非常典型。
睡覺。