目录
讀題
39. 组合总和
自己看到题目的第一想法
看到的想法其實跟昨天電話號碼的思路類似,可以想像每一層的集合都一樣,這個題目是由與數組長度n相等的n個集合,而集合內的元素都一致,終止這裡加入到result的結果要大於等於target並且如果是等於則將結果加入到result 當中, 這裏就可以藉由回溯法窮舉。
看完代码随想录之后的想法
看完對於startIndex的想法要記住以下在代碼隨想錄有提到的:
如果是一个集合求组合的话,就需要startIndex,例如:77.组合 (opens new window),216.组合总和III (opens new window)。
如果是多个集合取组合,各个集合之間互不影響,就不用startIndex,例如:17.电话号码的字母组合
一開始想成多個集合取組合但是雖然是這樣,但是集合之間會互相影響,就需要使用index了
自己對於這道題目沒有到非常透徹,看完之後才發現原來要排序,因為照原本的解法也可以通過,但是假設先進行排序,就可以在後續的過程中進行剪枝的操作,讓整體的速度再次提升,這點是我沒想到的。
40.组合总和II
自己看到题目的第一想法
看到題目的第一想法是使用set, 但我對set的作法實在不熟悉,整體思路想的是先進行排序,排序完後將數組傳遞到回溯函數當中,終止條件大致跟組合總和一樣,但加上一個index == candidates.size一致的狀況,至於去重我在想會不會可以使用一個機制是當我回溯完之後,因為數組已經排序過,那我們要做的事情其實就是不再用跟第一個相同的值,因為實際上在第一次回溯時都遍歷過了,那就下來遍歷就直接忽略相同值,讓index直接指向下一個不同的數值
看完代码随想录之后的想法
看完卡哥的想法之後,我發現我一開始就是直接用startIndex來進行去重,應該說我是使用i 來去重比較正確,在看卡哥的used去重時,需要花點時間來理解,而我的思考角度是從假設有很多個1 組成的集合 那我相同的元素在第一個一時都有遍歷過,就不需要再遍歷一次了,不過我還是需要再理解一下卡哥的思維。
反覆看了代碼隨想錄,大概能夠掌握used這個概念的思路,主要就是判斷目前這個重複是出現在樹層還是樹枝,如果是樹枝則繼續,如果重複是出現在樹層則跳過,理解完之後,在去寫code也很快能夠寫出來了。
131.分割回文串
自己看到题目的第一想法
分割我在思考的時候就有點像是我有很多不同的排列組合,整體感覺跟總合總合有點像,但是組合總合中間可以跳過某些節點,但是在分割時,不能跳過,以這個字串為例 aca 在組合裡可能會有三個回文組合 [“a” “c” “a” ] [“aca”] [”aa”] 但是在分割”aa”是不合法的。所以我要想的是,要怎麼去記錄分割,我在想會不會是迴圈的部分其實就是分割的長度,回溯就是進行分割,假設子字符串都符合條件,則將結果存入result,但還是想不通這段要怎麼操作
看完代码随想录之后的想法
看完之後,並且一步一步造著寫,有粗略的了解了概念,但至於是否有懂,可能還需要更多的題目來考驗,但整體思路很清晰,裡面有兩個重點如下,與自己第一次的想法有點類似,但我沒有想到startIndex到i之間就是子串區間,並且對於切割的思路也不清晰,還需要再多複習
- startIndex = 切割線
- startIndex i 左閉右閉的區間
39. 组合总和 - 實作
思路
錯誤思路
- 建立一個result 函數以及path 函數
- 建立一個回溯函數
- 傳入值
candidates
andtarget
andpathSum
- 終止條件: if pathSum ≥ target and if pathSum == target result.push_back(path) return
- 單遞迴
- for I = 0 ; i < candidate.size(); i ++)
- Path.push_back(candidates[i])
- backtracking(candidates, target, pathSum + candidates[i])
- Path.pop_back()
- for I = 0 ; i < candidate.size(); i ++)
- return
主函式
backtracking( candidates, target, 0)
return result
錯誤點
- 沒有一個startIndex 控制下一個集合出現的位置,在題意中我們要找尋唯一的組合,但一個數值又可以重複使用,仍然把全部的集合都當作不同的集合處理,但是因為內部數值都一樣,透過原本i + 1 控制startIndex 找下一個,我們透過傳入i 讓下一個數值由i 開始,而不是i + 1 也不是 0 ,如果是0的話會導致集合可能會出現一樣的組合,因為從0 開始,每個數組又都一樣,以example1 來說可能就會出現 [223] [232] [322]這三種組合,但題意是要我們找出不同的組合,所以我們透過startIndex來控制下一個遞迴從哪開始。
正確思路
- 建立一個result 函數以及path 函數
- 建立一個回溯函數
- 傳入值
candidates
andtarget
andpathSum
andstartIndex
- 終止條件: if pathSum ≥ target and if pathSum == target result.push_back(path) return
- 單遞迴
- for I = startIndex; i < candidate.size(); i ++)
- Path.push_back(candidates[i])
- backtracking(candidates, target, pathSum + candidates[i], i)
- Path.pop_back()
- for I = startIndex; i < candidate.size(); i ++)
- return
主函式
backtracking( candidates, target, 0, 0)
return result
剪枝思路
多加兩個動作
- 排序candidates
- 假設當前的sum + candidates[i] > target則不用進去遞迴
Code
錯誤代碼
class Solution {
public:
vector<vector<int>> results;
vector<int> path;
void backtracking(vector<int> candidates, int target, int pathSum) {
if(pathSum >= target) {
if(pathSum == target) results.push_back(path);
return;
}
for(int i = 0; i < candidates.size(); i++) {
path.push_back(candidates[i]);
backtracking(candidates, target, pathSum + candidates[i]);
path.pop_back();
}
return;
}
vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
backtracking(candidates, target, 0);
return results;
}
};
正確代碼
class Solution {
public:
vector<vector<int>> results;
vector<int> path;
void backtracking(vector<int> candidates, int target, int pathSum, int startIndex) {
if(pathSum >= target) {
if(pathSum == target) results.push_back(path);
return;
}
for(int i = startIndex; i < candidates.size(); i++) {
path.push_back(candidates[i]);
backtracking(candidates, target, pathSum + candidates[i], i);
path.pop_back();
}
return;
}
vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
backtracking(candidates, target, 0, 0);
return results;
}
};
剪枝代碼
class Solution {
public:
vector<vector<int>> results;
vector<int> path;
void backtracking(vector<int> candidates, int target, int pathSum, int startIndex) {
if(pathSum >= target) {
if(pathSum == target) results.push_back(path);
return;
}
for(int i = startIndex; i < candidates.size() && pathSum + candidates[i] <= target; i++) {
path.push_back(candidates[i]);
backtracking(candidates, target, pathSum + candidates[i], i);
path.pop_back();
}
return;
}
vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
sort(candidates.begin(), candidates.end()); // 需要排序
backtracking(candidates, target, 0, 0);
return results;
}
};
40.组合总和II - 實作
思路
錯誤思路
- 建立一個result and path 儲存結果
- 建立一個回溯函數
- 傳入值 candidates target pathSum index
- 終止條件
- pathSum == target
- pathSum > target && index == candidates.size()
- 單遞迴
- i = index i < candidates.size I ++
- path.push_back(candidates[i])
- backtracking ( candidates, target, pathSum + candidates[i], i + 1)
- path.pop_back
- while ( candidates[i] == candidates[i+1] && i < candidates.size())
- i++
主函式
- qsort candidates
- backtracking (candidates, target, 0, 0)
- return result
錯誤點
- pathSum > target && index == candidates.size() 應該為||
- candidates[i] == candidates[i + 1] && i < candidates.size() 順序應相反,不然有可能出現操作空數組
正確思路
- 建立一個result and path 儲存結果
- 建立一個回溯函數
- 傳入值 candidates target pathSum index
- 終止條件
- pathSum == target
- pathSum > target || index == candidates.size()
- 單遞迴
- i = index i < candidates.size I ++
- path.push_back(candidates[i])
- backtracking ( candidates, target, pathSum + candidates[i], i + 1)
- path.pop_back
- while ( i < candidates.size() && candidates[i] == candidates[i+1] )
- i++
主函式
- sort candidates
- backtracking (candidates, target, 0, 0)
- return result
使用used的思路
藉由判斷used[i - 1]是 true 還是 false 判斷這個元素在同一層中是否重複
而要使用這個思路也必須要進行排序,才能夠避免這個元素在後面再次出現時,沒有被包含到前面的去重過程。
- 建立一個result and path 儲存結果
- 建立一個回溯函數
- 傳入值 candidates target pathSum index used
- 終止條件
- pathSum == target
- pathSum > target || index == candidates.size()
- 單遞迴
- i = index i < candidates.size I ++
- if( i > 0 && candidates[i] == candidates[i - 1] && used[i - 1] == false) conntinue;
- path.push_back(candidates[i])
- used[i] = true \\ 接下來在遞迴遇到相同的就是樹枝重複而不是樹層重複
- backtracking ( candidates, target, pathSum + candidates[i], i + 1)
- used[i] = false \\ 回溯操作,接下來的for loop就是樹層的操作,樹層不能重複,所以要返回false
- path.pop_back
主函式
- 建立一個bool 的used 數組
- sort candidates
- backtracking (candidates, target, 0, 0)
- return result
Code
錯誤思路
class Solution {
public:
vector<vector<int>> results;
vector<int> path;
void backtracking(vector<int>& candidates, int target, int pathSum, int index) {
if(pathSum == target) {
results.push_back(path);
return;
}
if(pathSum > target && index == candidates.size()) return;
for(int i = index; i < candidates.size(); i++) {
path.push_back(candidates[i]);
backtracking(candidates, target, pathSum + candidates[i], i + 1);
path.pop_back();
while(candidates[i] == candidates[i + 1] && i < candidates.size()) i++;
}
return;
}
vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {
if(candidates.size() == 0) return results;
sort(candidates.begin(), candidates.end());
backtracking(candidates, target, 0, 0);
return results;
}
};
正確思路
class Solution {
public:
vector<vector<int>> results;
vector<int> path;
void backtracking(vector<int>& candidates, int target, int pathSum, int index) {
if(pathSum == target) {
results.push_back(path);
return;
}
if(pathSum > target || index == candidates.size()) return;
for(int i = index; i < candidates.size(); i++) {
path.push_back(candidates[i]);
backtracking(candidates, target, pathSum + candidates[i], i + 1);
path.pop_back();
while( i + 1 < candidates.size() && candidates[i] == candidates[i + 1] ) i++;
}
return;
}
vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {
if(candidates.size() == 0) return results;
sort(candidates.begin(), candidates.end());
backtracking(candidates, target, 0, 0);
return results;
}
};
使用used 的思路
class Solution {
public:
vector<vector<int>> results;
vector<int> path;
void backtracking(vector<int>& candidates, int target, int pathSum, int index, vector<bool>& used) {
if(pathSum == target) {
results.push_back(path);
return;
}
if(pathSum > target || index == candidates.size()) return;
for(int i = index; i < candidates.size() && pathSum + candidates[i] <= target; i++) {
if(i > 0 && candidates[i] == candidates[i - 1] && used[i - 1] == false) continue;
path.push_back(candidates[i]);
used[i] = true;
backtracking(candidates, target, pathSum + candidates[i], i + 1, used);
used[i] = false;
path.pop_back();
}
return;
}
vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {
if(candidates.size() == 0) return results;
vector<bool> used(candidates.size(), false);
sort(candidates.begin(), candidates.end());
backtracking(candidates, target, 0, 0, used);
return results;
}
};
131.分割回文串 - 實作
思路
- 建立一個results 以及 path 儲存結果
- 回溯函數
- 傳入值 s, startIndex
- 終止條件
- startIndex大於s.size
- 如果startIndex大於s.size 代表已經找到一組切割方案,因為startIndex可以想像成切割線,如果切割線大於等於s的大小,代表已經沒有要切割的部分了
- 並且因為在單遞迴都有判斷是否為回文,所以如果到達終止條件的path裡的子字串都是回文
- 單遞迴
- i = startIndex; i < s.size(); i++
- 獲取startIndex 到 i 之間的區間→ 代表著s中的子串
- 以aab來說 如果startIndex = 0 則是從a 開始切割,遞回到下一層startIndex = 1 切割第二個a 、第三層startIndex = 2 切割b ,將結果存入path
- 第三層回到第二層 i 則++為 2 startIndex = 1 代表的是[1, 2 ]也就是ab這個區間,而這個區間不是回文則直接跳過
- 判斷startIndex 到 i之間是否為回文
- 如果是,則將這個子串存入path
- 不是則跳過這次迴圈
- 判斷回文函數
- 使用雙指針,假設前後不一致,則return false 否則return true;
- 主函式
- 傳入s以及0
- return results.
Code
class Solution {
public:
vector<vector<string>> results;
vector<string> path;
void backtracking(const string& s, int startIndex) {
// 如果startIndex大於s.size 代表已經找到一組切割方案,因為startIndex可以想像成切割線,如果切割線大於等於s的大小,代表已經沒有要切割的部分了
if(startIndex >= s.size()) {
results.push_back(path);
return;
}
for(int i = startIndex; i < s.size(); i++) {
//獲取[startIndex,i] 在s中的子串
if(isPalindrome(s, startIndex, i)) {
string str = s.substr(startIndex, i - startIndex + 1);
path.push_back(str);
} else {
continue; // 如果不是代表這個子串已經不是回文,等同這個切割方案不符合條件,則跳過這一次的處理
}
backtracking(s, i + 1);
path.pop_back();
}
}
bool isPalindrome(const string& s, int start, int end) {
for (int i = start, j = end; i < j; i++, j--) {
if (s[i] != s[j]) {
return false;
}
}
return true;
}
vector<vector<string>> partition(string s) {
backtracking(s, 0);
return results;
}
};
總結
自己实现过程中遇到哪些困难
今天最大的困難點是在分割回文串的部分,自己花了蠻長時間去理解,但看完卡哥的講解後清晰很多,其次就是startIndex的使用,自己沒有到非常理解,不過看到代碼隨想錄當中組合總合裡面的總結,就清晰了很多,另外就是used數組的使用,這部分還需要一點時間來理解。
今日收获,记录一下自己的学习时长
今天大概學了2.5hr,整體學起來,包含自己想辦法做出來的題目都能夠AC真的很爽,另外在學習的過程中可以了解到不同的操作,也讓我的思考更廣了些。
相關資料
● 今日学习的文章链接和视频链接
39. 组合总和
题目链接/文章讲解:https://programmercarl.com/0039.组合总和.html
视频讲解:带你学透回溯算法-组合总和(对应「leetcode」力扣题目:39.组合总和)| 回溯法精讲!_哔哩哔哩_bilibili
40.组合总和II
题目链接/文章讲解:https://programmercarl.com/0040.组合总和II.html
视频讲解:回溯算法中的去重,树层去重树枝去重,你弄清楚了没?| LeetCode:40.组合总和II_哔哩哔哩_bilibili