目录
效能分析: 效能分析目前我無暇去了解,但代碼隨想錄有相關的說明,可以直接去看。
回溯算法總結圖: 这个图是 代码随想录知识星球 (opens new window)成员:莫非毛 (opens new window),所画,总结的非常好,分享给大家。
讀題
332.重新安排行程
自己看到题目的第一想法
看到第一眼,要返回最小詞序的路程,要去判斷整個路徑要怎麼走,看完題意,以當前我的理解沒辦法馬上想到一個解題思路去解決連終止條件單遞迴要怎麼做也沒有想法,先來看題解。
看完代码随想录之后的想法
- 紀錄映射關係,
- 一個機場映射多個機場: unorder_map
- 多個機場之間有順序 map、multimap、multiset3
- 映射關係
- unorder_map<string, multiset>: unorder_map<出發機場, 到達機場的集合> → 出發機場與到達機場之間進行映射關係
- unorder_map<string, map<string, int>> unorder_map < 出發機場,<到達機場,行班> 出發機場與到達機場的映射關係,並且到達機場也會與航班次數有一個映射關係
- 建議使用第二種做法,可使用航班次數來記錄使用次數,假設大於零代表這個目的地能飛,如果等於零,代表不能飛了
- 回溯法
-
模板
void backtracking(參數) { if(終止條件) { 存放結果; return; } for(選擇: 本層集合中的元素) { 處理節點 backtracking(路徑, 選擇列表) 回溯,撤銷處理結果 }
-
參數
- unordered_map<string, map<string, int>> targets → 紀錄航班的映射關係,定義為全局變量不用回溯與遞迴,只是在過程中進行次數的刪減
- ticketNum 代表有多少航班
- backtracking 設為bool 因為假設找到一個路徑是合法的,直接將這個路徑一路往上傳
-
參數初始值
-
將航班插入到unordered_map中進行映射
for (const vector<string>& vec : tickets) { targets[vec[0]][vec[1]]++; // 记录映射关系 } result.push_back("JFK"); // 起始机场
假設有以下機票
JFK -> LAX JFK -> SFO LAX -> SFO
將這些機票放入數據結構中會呈現以下狀態
{ "JFK": {"LAX": 1, "SFO": 1}, "LAX": {"SFO": 1} }
-
-
終止條件
- 假設有四個航班,只要找到機場數量是五,四個航班可以想像程點之間的線,所以四個航班有五個機場,假設機場個數 達到了航班數加一,則終止遞迴
-
單層搜索的邏輯
for (pair<const string, int>& target : targets[result[result.size() - 1]]) // 選擇要在map中選擇哪個到達機場 { if (target.second > 0 ) { // 紀錄機場是否飛過了 result.push_back(target.first); target.second--; if (backtracking(ticketNum, result)) return true; // 假設找到一個路徑符合條件,一路回傳回去,也因此不需要做回溯操作,因為一旦做回溯操作,結果就不對了 result.pop_back(); target.second++; } }
- 第一次調用會從JFK開始,是因為在for函數中的鍵值對在一開始就是JFK,因為是在result當眾去尋找目的機場。
- 在第一次搜索的時候,以上面為例會從LAX開始,假設target.second(航班次數) 大於零則往下遞迴,而整體在JFK開始的航班會有兩班 一個是SFO 另一個是 LAX
- 第二次搜索時,會從LAX 有一個航班也就是SFO而LAX也只會跑這一次,因為LAX的只有一個目的機場,也就是只會進行一次迭代
- 因為MAP的緣故,裡面的鍵值對都是有序的,但假設是從SFO開始,因為SFO沒有對應的目的機場,所以也不會進入到for迴圈當中
- 所以以這個簡單的例子最後會得出JFK → LAX → SFO的路徑
-
51.N皇后
自己看到题目的第一想法
先去看皇后的移動路徑,在其橫直斜都不能有東西,否則會被攻擊,用回溯有思考到應該就是一層一層去遞迴但如果是直跟橫,我想像的到可能可以用記錄used來避免直的
如果我的判斷式橫得很好避免,就是一層只能有一個位子被選中,直的也很好避免,使用used來避免直的撞車,那橫的我現在想到是每一層跑一次for回圈檢查 當前位置的cur + i cur -i 的位置?
這邊我真的想到有點困擾
看完代码随想录之后的想法
看完發現其實比起之前的題目,這題更多的是考察思路,代碼本身不難,跟著代碼隨想錄一題題的走下來,理解上不難
其實主要還是三點
終止條件要想清楚 row = n 代表走到底了
單層搜索邏輯
- 確認當前位置是否合法
- 合法→ 放置皇后, 不合法 → 跳過
- 進行遞迴
- 進行回溯
isValid
檢查chessboard的列以及135與45度角是否有Q存在,如果是則return false,如果全部檢查都不是return true
- hint: 因為每一層遞迴都會是底所以只要查看135度角以及45度角是否有數值
37.解数独
自己看到题目的第一想法
當下想一想以為跟N皇后很像,但發現N皇后每次只專注在一層,但在數獨上,需要考慮的不僅僅是橫的也需要考慮列,在實現上有困難,思路沒有打開
看完代码随想录之后的想法
學到了二層遞迴的概念,因為一題題的理解,雖然目前沒有辦法自己單獨寫出來,但一刷就跟著卡哥的思路一步步地做下來
332.重新安排行程 - 實作
思路
- 紀錄映射關係:unorder_map<string, map<string, int>> unorder_map < 出發機場,<到達機場,行班>
- 回溯函數
- 參數
- vector<string>& result → 紀錄航班的路徑
- ticketNum 代表有多少航班
- backtracking 設為bool 假設找到一個路徑是合法的,直接將這個路徑一路往上傳
- 終止條件 result.size() == ticketNums + 1
- 單層搜索的邏輯
- 選擇要在map中選擇哪個到達機場
- target.second > 0 判斷是否還可以飛
- 處理節點
- 遞迴 ,假設找到一個路徑符合條件,一路回傳回去
- 回溯
- 參數
- 主函數
- 建立results
- 將航班插入到unordered_map中進行映射
- 遞迴
- 回傳結果
Code
class Solution {
public:
unordered_map<string, map<string, int>> target;
bool backtracking(vector<string>& result, int ticketNums) {
if(result.size() == ticketNums + 1 ) return true;
for(pair<const string, int>& target : target[result[result.size()-1]]) {
if(target.second > 0){
result.push_back(target.first);
target.second--;
if(backtracking(result, ticketNums)) return true;
result.pop_back();
target.second++;
}
}
return false;
}
vector<string> findItinerary(vector<vector<string>>& tickets) {
target.clear();
vector<string> result;
for(const vector<string>& vec : tickets) {
target[vec[0]][vec[1]]++;
}
result.push_back("JFK");
backtracking(result, tickets.size());
return result;
}
};
51.N皇后 - 實作
思路
- 建立results 數組儲存結果
- 回溯函數
- 傳入值: n(棋盤長度), row (目前的行數), chessboard(棋盤)
- 終止條件row = n 代表走到底了
- 單層搜索邏輯
- 確認當前位置是否合法
- 合法→ 放置皇后, 不合法 → 跳過
- 進行遞迴
- 進行回溯
- 確認當前位置是否合法
- isValid
- 判斷同列是否有Q
- 判斷45度角是否有Q
- 判斷135度角是否有Q
- 主函數
- 建立chessboard
- 回溯函數遞迴
- return results
Code
class Solution {
public:
vector<vector<string>> results;
void backtracking(int n, int row, vector<string>& chessboard) {
if(row == n) {
results.push_back(chessboard);
return;
}
for(int col = 0; col < n; col++) {
if(isValid(row, col, chessboard, n)){
chessboard[row][col] = 'Q';
backtracking(n, row + 1, chessboard);
chessboard[row][col] = '.';
}
}
}
bool isValid(int row, int col, vector<string>& chessboard, int n) {
// 判斷上下是否有Q
for(int i = 0; i < row; i++) {
if(chessboard[i][col] == 'Q') return false;
}
for(int i = row - 1, j = col - 1 ;i >= 0 && j >= 0; i--, j--) {
if(chessboard[i][j] == 'Q') return false;
}
for(int i = row - 1, j = col + 1 ; i >= 0 && j < n ; i--, j++) {
if(chessboard[i][j] == 'Q') return false;
}
return true;
}
vector<vector<string>> solveNQueens(int n) {
results.clear();
vector<string> chessboard(n, string(n, '.'));
backtracking(n, 0, chessboard);
return results;
}
};
37.解数独 - 實作
思路
- 建立results 數組儲存結果
- 回溯函數
- 傳入值: n(棋盤長度), row (目前的行數), chessboard(棋盤)
- 終止條件,因為只求唯一解,可以不設置終止條件,因為假設都沒有符合條件的,就會走到下面的邏輯,一樣會回傳素值
- 二層搜索邏輯
- 兩層for迴圈 一個針對行一個針對列
- 假設當前位置不為’.’ 跳過執行下面的函數
- 當前位置為’.’
- for回圈 迭代’1’ - ‘9’的數值
- 確認當前位置放置k是否合法
- 合法→ 放置, 不合法 → 跳過
- 遞迴
- 回溯
- 確認當前位置放置k是否合法
- for回圈 迭代’1’ - ‘9’的數值
- 如果1~9都不合法return false
- 遍歷完return true;
- isValid
- 判斷同列是否有相同val
- 判斷同行是否有相同val
- 判斷小九宮格裡是否有相同val
- 主函數
- 回溯函數遞迴
Code
class Solution {
public:
bool backtracking(vector<vector<char>>& board) {
for(int i = 0; i < board.size(); i++) {
for(int j = 0; j < board[0].size(); j++) {
if(board[i][j] != '.') continue;
for(char k = '1'; k <= '9'; k++) {
if(isValid(i, j, k, board)){
board[i][j] = k;
if(backtracking(board)) return true;
board[i][j] = '.';
}
}
return false;
}
}
return true;
}
bool isValid(int row, int col, char val, vector<vector<char>>& board){
for(int i = 0; i < 9; i++){
if(board[row][i] == val) return false;
}
for(int j = 0; j < 9; j++) {
if(board[j][col] == val) return false;
}
int startRow = (row/3) * 3;
int startCol = (col/3) * 3;
for(int i = startRow; i < startRow + 3; i++) {
for(int j = startCol; j < startCol + 3; j++) {
if(board[i][j] == val) return false;
}
}
return true;
}
void solveSudoku(vector<vector<char>>& board) {
backtracking(board);
}
};
回溯算法章節總結
感想
回溯算法整體做完後,其實在做的過程中基本上都離不開回溯法的模板,大致都大同小異
其實更多的是對於資料結構的考察熟悉程度,以及如何去看到問題的狀態。
最後想像遞迴的過程中如何嵌套循環與如何想像這個樹形結構長甚麼模樣。
回溯法模板
void backtracking(參數) {
if (終止條件) {
存放結果;
return;
}
for(選擇: 本層有多少元素){
處理節點;
backtracking(路徑,選擇列表) \\\\遞迴
回溯,撤銷處理結果
}
return;
}
能解決的問題
組合: N個數裡面根據一定規則找出K個數的集合
-
77. 组合
看完卡哥的代碼隨想錄講解,其實這題就是套模板,對於回溯算法的一個基礎理解
-
216.组合总和III
其實就跟昨天一樣使用組合,但在這個基礎上加上終止條件pathSum == k
-
17.电话号码的字母组合
在前兩題的基礎上再加上一個對於資料結構的嵌套,會發現代碼其實不難甚至跟之前很像,但是自己對於哈希表的應用、字串的處理、包含要傳入的數值,以及index的實際作用不熟悉,實際做題會發現困難。
-
39. 组合总和
startIndex要記住以下在代碼隨想錄有提到的:
- 如果是一个集合求组合的话,就需要startIndex,例如:77.组合 (opens new window),216.组合总和III (opens new window)。
- 如果是多个集合取组合,各个集合之間互不影響,就不用startIndex,例如:17.电话号码的字母组合。
一開始想成多個集合取組合但是雖然是這樣,但是集合之間會互相影響,就需要使用index了。自己對於這道題目沒有到非常透徹,看完之後才發現原來要排序,因為照原本的解法也可以通過,但是假設先進行排序,就可以在後續的過程中進行剪枝的操作,讓整體的速度再次提升,
-
40.组合总和II
-
一開始就是直接用startIndex來進行去重,應該說我是使用i 來去重比較正確,後看卡哥的used去重,反覆看了代碼隨想錄,逐步掌握used的思路,
主要就是判斷目前這個重複是出現在樹層還是樹枝,如果是樹枝則繼續,如果重複是出現在樹層則跳過
-
切割: 字符串按一定規則有幾種切割方式
- 131.分割回文串
- 有兩個重點如下 startIndex = 切割線 。startIndex i 左閉右閉的區間
- 93.复原IP地址
- 自己的做法比較偏向是加一個path,而卡哥則是在原字串上進行操作,但兩種方法都實現出來了
- 一開始主要錯誤點在終止條件的錯誤設置,判斷Valid IP 的函數處理錯誤,以及回溯操作的錯誤,這幾個錯誤點導致我的程式無法正常執行,但整體的想法是有,但實際執行上在思考上自己有很多部分需要去釐清。
子集: 一個N個數的集合有多少符合條件的子集
- 78.子集
- 在組合以及分割的時候都是有符合某個條件才收集結果
- 擔子集可以想像就是每個節點收集結果,並且這個是一個集合,彼此不能相互干擾,需要使用startIndex 來進行控制
- 90.子集II
- 其實就是跟組合總和II 加上子集的思路一樣,需要進行去重
- 使用卡哥的做法used 或者是跳過重複元素都可以蠻清楚的去解決這個問題
- 491.递增子序列
- 這題其實就跟上面兩題很像但這題的去重邏輯改為利用set去紀錄當層的唯一元素,進行去重的操作
- 因為在這題中並不能對數組進行排序,所以需要額外開闢個空間來避免重複元素出現。
排序: N個數裡面根據一定規則排序,有多少排列方式
- 46.全排列
- 跟組合不一樣但又很相似,之前使用過的元素也可以使用,所以不用用startIndex進行起始位置的調整。
- 但要做一個調整就是如果目前遍歷到當下這個元素,則跳過,使用一個used bool數組配合回溯可以很簡單的去處理這件事情
- 47.全排列 II
- 這題只是全排列基礎上加上一個去重的邏輯,整體不難,一樣是排列的遍歷
棋盤: N皇后、解數獨
- 51.N皇后,題目代碼本身不難,主要是思考上考慮的透不透徹,以及怎麼控制下一層遞迴的條件,與判斷主函數如何去實現
- 37.解数独,如果說前面都是一維的集合處理,這道題就是要將整體的視角切到二維集合,再除了這點,其實跟n皇后也有某部分的相向,只是變成二維的處理方式
图论额外拓展
332.重新安排行程
- 這道題我覺得主要是難在對於資料結構是否夠熟悉,本體不難,但是有套用到二叉數的一個概念甚麼時候需要一個回傳值,假設只是要遍歷出一個結果,那就可以利用回傳值一路將結果回船上去
- 在這裡也比較深刻的了解到自己對於set、map這些資料結構不熟悉,不太會去使用的問題
效能分析: 效能分析目前我無暇去了解,但代碼隨想錄有相關的說明,可以直接去看。
總結
回顧這三十幾天每天的學習,目前終於把回溯算法有個基礎的框架建立起來了,對於程式的理解也更加的透徹
對於回溯的算法,有幾個卡哥提到的回溯算法的本質問題
- 如何理解回溯法的搜索过程? for循環式本層搜索、遞迴是縱向搜索(用遞迴來嵌套for循環)
- 什么时候用startIndex,什么时候不用? 集合之間是否會在意互相影響,如果會,就要使用startIndex
- 如何去重?如何理解“树枝去重”与“树层去重”? 樹枝可以想像在遞迴過程中,我跟上一層是否一致,樹層可以理解為我在for迴圈中,前後是否一致,使用used會更好理解
- 去重的几种方法? 兩種 一種是used作法另一種是startIndex作法
- 如何理解二维递归? 可以想像原本一次只要在一行中進行操作,但在解數獨這個題目當中,要操作的就不僅僅只是一行,而是要遞迴整個棋盤,可以想像要輸出一個九宮格,一次只輸出一個數字,我們無法做出九宮格,因為一層for迴圈只能控制一個維度,需要兩個for迴圈去控制兩個維度。
有些問題卡在自己對於C++的數據結構不熟悉,有些題目卡在觀念不熟悉,但在一刷時可以完整地做完這些題目,很明顯感覺到自己的思考更加清晰了。
回溯算法總結圖: 这个图是 代码随想录知识星球 (opens new window)成员:莫非毛 (opens new window),所画,总结的非常好,分享给大家。
總結
自己实现过程中遇到哪些困难
困難點在於自己對於資料結構的不熟悉,以及回溯算法仍需要多加練習,但因為一步步地跟著進度走,所以除了重新安排行程,N皇后跟解數獨理解比較快,重新安排行程,一步步了解整體概念後,也有一些初步的理解了。
今日收获,记录一下自己的学习时长
加上昨天先預習的部分大概學了4hr,但今天做完總結以及這三題困難的題目,對於回溯算法終於有種掌握了部分的感想,之後仍需要多多練習,讓自己對於這個算法更加清楚。
相關資料
● 332.重新安排行程
● 51. N皇后
● 37. 解数独
● 总结
332.重新安排行程
https://programmercarl.com/0332.重新安排行程.html
51. N皇后
https://programmercarl.com/0051.N皇后.html
视频讲解:这就是传说中的N皇后? 回溯算法安排!| LeetCode:51.N皇后_哔哩哔哩_bilibili
37. 解数独
https://programmercarl.com/0037.解数独.html
视频讲解:回溯算法二维递归?解数独不过如此!| LeetCode:37. 解数独_哔哩哔哩_bilibili
总结
https://programmercarl.com/回溯总结.html