在回溯算法:求組合問(wèn)題!中,我們通過(guò)回溯搜索法,解決了n個(gè)數(shù)中求k個(gè)數(shù)的組合問(wèn)題。
文中的回溯法是可以剪枝優(yōu)化的,本篇我們繼續(xù)來(lái)看一下題目77. 組合。
鏈接:https://leetcode-cn.com/problems/combinations/
「看本篇之前,需要先看回溯算法:求組合問(wèn)題!」。
大家先回憶一下[77. 組合]給出的回溯法的代碼:
class Solution {
private:
vector<vector<int>> result; // 存放符合條件結(jié)果的集合
vector<int> path; // 用來(lái)存放符合條件結(jié)果
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); // 處理節(jié)點(diǎn)
backtracking(n, k, i + 1); // 遞歸
path.pop_back(); // 回溯,撤銷處理的節(jié)點(diǎn)
}
}
public:
vector<vector<int>> combine(int n, int k) {
result.clear(); // 可以不寫
path.clear(); // 可以不寫
backtracking(n, k, 1);
return result;
}
};
剪枝優(yōu)化
我們說(shuō)過(guò),回溯法雖然是暴力搜索,但也有時(shí)候可以有點(diǎn)剪枝優(yōu)化一下的。
在遍歷的過(guò)程中有如下代碼:
for (int i = startIndex; i <= n; i++) {
path.push_back(i);
backtracking(n, k, i + 1);
path.pop_back();
}
這個(gè)遍歷的范圍是可以剪枝優(yōu)化的,怎么優(yōu)化呢?
來(lái)舉一個(gè)例子,n = 4,k = 4的話,那么第一層for循環(huán)的時(shí)候,從元素2開始的遍歷都沒(méi)有意義了。在第二層for循環(huán),從元素3開始的遍歷都沒(méi)有意義了。
這么說(shuō)有點(diǎn)抽象,如圖所示:

圖中每一個(gè)節(jié)點(diǎn)(圖中為矩形),就代表本層的一個(gè)for循環(huán),那么每一層的for循環(huán)從第二個(gè)數(shù)開始遍歷的話,都沒(méi)有意義,都是無(wú)效遍歷。
「所以,可以剪枝的地方就在遞歸中每一層的for循環(huán)所選擇的起始位置」。
「如果for循環(huán)選擇的起始位置之后的元素個(gè)數(shù) 已經(jīng)不足 我們需要的元素個(gè)數(shù)了,那么就沒(méi)有必要搜索了」。
注意代碼中i,就是for循環(huán)里選擇的起始位置。
for (int i = startIndex; i <= n; i++) {
接下來(lái)看一下優(yōu)化過(guò)程如下:
- 已經(jīng)選擇的元素個(gè)數(shù):path.size();
- 還需要的元素個(gè)數(shù)為: k - path.size();
- 在集合n中至少要從該起始位置 : n - (k - path.size()) + 1,開始遍歷
為什么有個(gè)+1呢,因?yàn)榘ㄆ鹗嘉恢茫覀円且粋€(gè)左閉的集合。
舉個(gè)例子,n = 4,k = 3, 目前已經(jīng)選取的元素為0(path.size為0),n - (k - 0) + 1 即 4 - ( 3 - 0) + 1 = 2。
從2開始搜索都是合理的,可以是組合[2, 3, 4]。
這里大家想不懂的話,建議也舉一個(gè)例子,就知道是不是要+1了。
所以優(yōu)化之后的for循環(huán)是:
for (int i = startIndex; i <= n - (k - path.size()) + 1; i++) // i為本次搜索的起始位置
優(yōu)化后整體代碼如下:
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++) { // 優(yōu)化的地方
path.push_back(i); // 處理節(jié)點(diǎn)
backtracking(n, k, i + 1);
path.pop_back(); // 回溯,撤銷處理的節(jié)點(diǎn)
}
}
public:
vector<vector<int>> combine(int n, int k) {
backtracking(n, k, 1);
return result;
}
};
總結(jié)
本篇我們針對(duì)求組合問(wèn)題的回溯法代碼做了剪枝優(yōu)化,這個(gè)優(yōu)化如果不畫圖的話,其實(shí)不好理解,也不好講清楚。
所以我依然是把整個(gè)回溯過(guò)程抽象為一顆樹形結(jié)構(gòu),然后可以直觀的看出,剪枝究竟是剪的哪里。