在回溯算法:求組合問題!中,我們通過回溯搜索法,解決了n個數中求k個數的組合問題。
文中的回溯法是可以剪枝優化的,本篇我們繼續來看一下題目77. 組合。
鏈接:https://leetcode-cn.com/problems/combinations/
「看本篇之前,需要先看回溯算法:求組合問題!」。
大家先回憶一下[77. 組合]給出的回溯法的代碼:
class Solution {
private:
vector<vector<int>> result; // 存放符合條件結果的集合
vector<int> path; // 用來存放符合條件結果
void backtracking(int n, int k, int startIndex) {
if (path.size() == k) {
result.push_back(path);
return;
}
for (int i = startIndex; i <= n; i++) {
path.push_back(i); // 處理節點
backtracking(n, k, i + 1); // 遞歸
path.pop_back(); // 回溯,撤銷處理的節點
}
}
public:
vector<vector<int>> combine(int n, int k) {
result.clear(); // 可以不寫
path.clear(); // 可以不寫
backtracking(n, k, 1);
return result;
}
};
剪枝優化
我們說過,回溯法雖然是暴力搜索,但也有時候可以有點剪枝優化一下的。
在遍歷的過程中有如下代碼:
for (int i = startIndex; i <= n; i++) {
path.push_back(i);
backtracking(n, k, i + 1);
path.pop_back();
}
這個遍歷的范圍是可以剪枝優化的,怎么優化呢?
來舉一個例子,n = 4,k = 4的話,那么第一層for循環的時候,從元素2開始的遍歷都沒有意義了。在第二層for循環,從元素3開始的遍歷都沒有意義了。
這么說有點抽象,如圖所示:
圖中每一個節點(圖中為矩形),就代表本層的一個for循環,那么每一層的for循環從第二個數開始遍歷的話,都沒有意義,都是無效遍歷。
「所以,可以剪枝的地方就在遞歸中每一層的for循環所選擇的起始位置」。
「如果for循環選擇的起始位置之后的元素個數 已經不足 我們需要的元素個數了,那么就沒有必要搜索了」。
注意代碼中i,就是for循環里選擇的起始位置。
for (int i = startIndex; i <= n; i++) {
接下來看一下優化過程如下:
- 已經選擇的元素個數:path.size();
- 還需要的元素個數為: k - path.size();
- 在集合n中至少要從該起始位置 : n - (k - path.size()) + 1,開始遍歷
為什么有個+1呢,因為包括起始位置,我們要是一個左閉的集合。
舉個例子,n = 4,k = 3, 目前已經選取的元素為0(path.size為0),n - (k - 0) + 1 即 4 - ( 3 - 0) + 1 = 2。
從2開始搜索都是合理的,可以是組合[2, 3, 4]。
這里大家想不懂的話,建議也舉一個例子,就知道是不是要+1了。
所以優化之后的for循環是:
for (int i = startIndex; i <= n - (k - path.size()) + 1; i++) // i為本次搜索的起始位置
優化后整體代碼如下:
class Solution {
private:
vector<vector<int>> result;
vector<int> path;
void backtracking(int n, int k, int startIndex) {
if (path.size() == k) {
result.push_back(path);
return;
}
for (int i = startIndex; i <= n - (k - path.size()) + 1; i++) { // 優化的地方
path.push_back(i); // 處理節點
backtracking(n, k, i + 1);
path.pop_back(); // 回溯,撤銷處理的節點
}
}
public:
vector<vector<int>> combine(int n, int k) {
backtracking(n, k, 1);
return result;
}
};
總結
本篇我們針對求組合問題的回溯法代碼做了剪枝優化,這個優化如果不畫圖的話,其實不好理解,也不好講清楚。
所以我依然是把整個回溯過程抽象為一顆樹形結構,然后可以直觀的看出,剪枝究竟是剪的哪里。