今天终于结束了二叉树,开始回溯章节。主要内容是回溯的理论基础和基本代码模板,需要注意一定要将一直保持不变的vector设置为全局变量,否则有可能超时。
回溯的理论基础在二叉树的后序递归遍历和相关题目中已经涉及到,这里主要总结下回溯法的应用场景(整理自代码随想录):
- 组合问题:N个数里面按一定规则找出k个数的集合(与顺序无关)
- 排列问题:N个数按一定规则全排列,有多少种排列方式(与顺序有关)
- 分割问题:一个字符串按一定规则有多少种分割方式
- 子集问题:一个N个数的集合里有多少符合条件的子集
- 棋盘问题:N皇后,数独
其中“组合”与“排列”问题的区别在于:组合不区分顺序,只要内部元素相同,就认定为同一个组合;而对于排列,即便元素相同,只要内部顺序不同,就是2个不同的排列。
今天仅有的一道题(77. 组合)是回溯法的模板。分别用全局变量path和res保存当前路径中数字和所有结果。递归函数出口为当前path的长度已经达到k。回溯函数参数除n和k以外,还需当前层的循环起始值begin,每层回溯函数都从begin开始遍历到n为止。
public:
vector<int> path;
vector<vector<int>> res;
void back(int begin, int n, int k) {
if (path.size() == k) {
res.push_back(path);
return;
}
for (int i = begin; i <= n; ++i) {
path.push_back(i);
back(i + 1, n, k);
path.pop_back();
}
return;
}
vector<vector<int>> combine(int n, int k) {
path.clear();
res.clear();
back(1, n, k);
return res;
}
};
由于之前已经接触过回溯,这一题实现比较容易。需要注意在回溯前后要分别向path中添加,删除 元素。
自己之前在其他题目中还实现过另外一种回溯方法,将上面的“在每次回溯中横向遍历所有下一个数字的可能”变为了“将每个数字都分为‘取’和‘不取’这2种可能”。相比第一种方法,递归出口还需多一个当前下标超过n。起初实现后当数字n和k取较大值时运行超时,找到原因是因为将path也作为了参数传递,如果像上面一样把path作为全局变量,就不会超时。
class Solution {
public:
vector<vector<int>> res;
vector<int> path;
void back(int begin, int n, int k) {
if (path.size() == k) {
res.push_back(path);
return;
}
if (begin > n) {
return;
}
path.push_back(begin);
back(begin + 1, n, k);
path.pop_back();
back(begin + 1, n, k);
return;
}
vector<vector<int>> combine(int n, int k) {
path.clear();
res.clear();
back(1, n, k);
return res;
}
};
在自己的编译器中用全局变量cnt统计了n和k分别取20和16时,递归函数的运行次数,发现该方法的cnt约是第一种方法cnt的2倍左右。分析原因,发现这种方法在不取某个值时,也会进行递归,而第一种方法则不会,效率更高。所以之后的回溯将使用第一种方法。最后,还需要注意第一种方法回溯函数的形参begin对应的实参应该是i + 1,而第2种是begin + 1。这是因为第一种方法在遍历到i时,说明i及之前的数都不应该再取到了,而第2种方法只是继续决定下一个数取或者不取。