代码随想录二刷|第七章:回溯算法

回溯三部曲:

  • 回溯函数模板返回值以及参数
  • 回溯函数终止条件
  • 回溯搜索的遍历过程

强调,回溯法中递归函数参数很难一次性确定下来,一般先写逻辑,需要啥参数了,填什么参数。

树的宽度就是集合的大小,树的深度就是递归的深度。

77. 组合

代码随想录二刷|第七章:回溯算法_第1张图片
startIndex来记录下一层递归,搜索的起始位置。
如果没有startIndex,输出的结果将会是[[1,1],[1,2],[1,3],[1,4],[2,1],[2,2],[2,3],[2,4],[3,1],[3,2],[3,3],[3,4],[4,1],[4,2],[4,3],[4,4]]
剪枝:
已选择的元素个数:path.size(),还需要的元素个数:k - path.size(),在集合n中至多从i = n - (k - path.size()) + 1开始遍历:
for (int i = startIndex; i <= n - (k - path.size()) + 1; i++)

216.组合总和III

本题:传入sum
剪枝:
1、

if (sum > targetSum) { // 剪枝操作
    return;
}

2、

for (int i = startIndex; i <= 9 - (k - path.size()) + 1; i++) { // 剪枝

17.电话号码的字母组合

1、把string放进path里可以用 path += string,可是怎么拿出来呢?pop_back()
也不用+=,用push_back(i),但这个i必须是char类型的
2、我的错误:当我尝试使用 digits[startIndex] 作为键时,实际上是用字符(如 ‘2’,‘3’ 等)而不是整数。因此需要先- ‘0’。
3、可以不用map,直接用数组
4、传入index:用来记录遍历到第几个数字,同时也表示树的深度。

39. 组合总和

集合内元素不重复,但是可以重复取用
1、我应该添加一个条件来确保递归调用在 sum 超过 target 时停止。否则会无限递归。
216.组合总和III这道题即使不加这个也不会无限递归,因为它的子集个数必须是k,而本题对子集个数没有限制。
2、虽然本题可以重复取值,但依然要用startIndex,因为它是保证树层上的去重的,而startIndex = i + 1才是树枝上去重的。
剪枝:(要先排序)
排序之后,如果下一层的sum(即本层的sum + candidates[i])大于target,就不进入下一层递归

for (int i = startIndex; i < candidates.size() && sum + candidates[i] <= target; i++)

40.组合总和II

集合中有重复元素,并且不可以重复取用
1、排序+used数组:保证没有两个组合是相同的

131. 分割回文串

本质上还是组合问题
1、切割线用什么表示?可以用i吗?不可以,就是用startIndex
2、切割的长度用什么表示?可以也用i吗?不可以,[startIndex, i]就是要截取的子串
3、递归如何终止?
优化:
动态规划思想:给定一个字符串s, 长度为n, 它成为回文字串的充分必要条件是s[0] == s[n-1]且s[1:n-1]是回文字串。
isPalindrome[i][j] 代表 si:j是否是回文字串

93. 复原 IP 地址

1、pointNum:用于记录添加".“的数量,本题不需要真的切割出子串,在原本的字符串上添加”."即可
2、终止条件:本题不能用切割线切到最后为终止条件,因为字符串只会分为4段
3、如何判断每一段是否合法?

    // 判断字符串s在左闭又闭区间[start, end]所组成的数字是否合法
    bool isValid(const string& s, int start, int end) {
        if (start > end) {
            return false;
        }
        if (s[start] == '0' && start != end) { // 0开头的数字不合法
                return false;
        }
        int num = 0;
        for (int i = start; i <= end; i++) {
            if (s[i] > '9' || s[i] < '0') { // 遇到非数字字符不合法
                return false;
            }
            num = num * 10 + (s[i] - '0');
            if (num > 255) { // 如果大于255了不合法
                return false;
            }
        }
        return true;
    }

4、往字符串里加入、删除不用push_back、pop_back,因为不是单个字符,用insert、erase

78.子集

收集树形结构中每一个节点的结果,因此不需要任何剪枝!
1、怎么把根节点[]放进去?
把收集子集 result.push_back(path);放在终止条件的上面
2、终止条件:

if (startIndex >= nums.size()) {
    return;
}

其实可以不加终止条件,因为startIndex >= nums.size(),本层for循环本来也结束了。

90. 子集 II

集合中有重复元素,并且不可以重复取用

491. 递增子序列

1、不排序也可以用used数组吗?不能!!使用set来对本层元素进行去重
为什么递归函数上面的uset.insert(nums[i]);,下面却没有对应的pop之类的操作?而used数组中就需要?
这是因为unordered_set uset; 是记录本层元素是否重复使用,新的一层uset都会重新定义(清空),所以要知道uset只负责本层!而used数组一直是同一个。
2、怎么保证递增?
nums[i] >= path.back() 不是path.top()

if ((!path.empty() && nums[i] < path.back())
                    || uset.find(nums[i]) != uset.end()) {
                    continue;
            }

3、终止条件:

if (path.size() > 1) {
    result.push_back(path);
    // 注意这里不要加return,因为要取树上的所有节点
}

优化:
不用set,用数组来做去重操作:
int used[201] = {0}; // 题目说数值范围[-100, 100]
数组,set,map都可以做哈希表,而且数组干的活,map和set都能干,但如果数值范围小的话能用数组尽量用数组。

46. 全排列

1、怎么不取上次已取的?怎么避免这种情况:[[0,0],[0,1],[1,0],[1,1]]
使用used数组,记录此时path里都有哪些元素使用了,一个排列里一个元素只能使用一次。
这里的used数组和前面的排序+used数组的作用不一样。

47. 全排列 II

集合中有重复元素,并且不可以重复取用
1、怎么样把上面的used数组两种作用结合起来?

332. 重新安排行程

深度优先搜索
1、如何记录映射关系?
unordered_map> targets:unordered_map<出发机场, map<到达机场, 航班次数>> targets
此时tickets已被映射到targets上,因此在回溯函数中不用传入tickets,而是传入tickets.size()
2、注意函数返回值我用的是bool!
因为我们只需要找到一个行程,就是在树形结构中唯一的一条通向叶子节点的路线
3、回溯的过程中,如何遍历一个机场所对应的所有机场呢?

for (pair<const string, int>& target : targets[result[result.size() - 1]])

4、为什么string前面必须加const?否则会报错
在 C++ 中,当你遍历一个 map 的时候,迭代器指向的元素类型是 pair。这里的 const表明键(key)是不可变的。这是因为std::map` 的基础实现是红黑树(一种自平衡二叉搜索树),在这种数据结构中,修改键的值可能会破坏树的排序,从而使整个数据结构无效。

因此,当你尝试使用非 const 引用(例如 pair&)来绑定到这样的键值对时,编译器会报错,因为你试图绑定到一个带有 const 的键。

所以,正确的做法是使用 const 在引用类型中,如下所示:

for (const pair<const string, int>& target : targets[result[result.size() - 1]]) {
    // ...
}

也可以使用 auto

for (const auto& target : targets[result[result.size() - 1]]) {
    // ...
}

这样做的好处是代码更简洁,并且避免了类型匹配错误。在这个例子中,auto 将自动推断为 const pair& 类型。

51. N 皇后

代码随想录二刷|第七章:回溯算法_第2张图片
棋盘的宽度就是for循环的长度,递归的深度就是棋盘的高度
1、为什么没有在同行进行检查呢?
因为在单层搜索的过程中,每一层递归,只会选for循环(也就是同一行)里的一个元素,所以不用去重了。
2、chessboard如何初始化?
vector chessboard(n, string(n, ‘.’));
3、isValid中的遍历必须要倒着写,因为应该是从当前点发散去找它的同列、对角线

37. 解数独

二维递归,比n皇后多了一维
1、怎么判断九方格里是否重复

int startRow = (row / 3) * 3;
    int startCol = (col / 3) * 3;
    for (int i = startRow; i < startRow + 3; i++) { // 判断9方格里是否重复
        for (int j = startCol; j < startCol + 3; j++) {
            if (board[i][j] == val ) {
                return false;
            }
        }
    }

组合问题
如果是一个集合来求组合的话,就需要startIndex。
如果是多个集合取组合,各个集合之间相互不影响,那么就不用startIndex,例如:17.电话号码的字母组合

排列问题
for不是从startIndex开始,而是从0开始了。
怎么不重复取本次排列里的元素?used数组

集合中有重复元素
树枝去重:同一个组合内不能有重复的元素
树层去重:同一组合内可以重复,但两个组合不能相同(排序+used数组

组合问题和分割问题
收集叶子节点的结果
子集问题
收集树的所有节点的结果

你可能感兴趣的:(算法,leetcode,数据结构)