回溯part011

今天学习了回溯算法:

  1. 基本知识,关键是那个模板
  2. 组合问题:画树状图+简单的剪枝
  3. 电话号码的组合问题,和经典组合问题的差别在于取不同集合中的组合,注意如何有限制的在for循环之前确定循环哪个数组,通过树状图确定for循环中i的大小。

1. 基本知识

  1. 回溯法也可以叫做回溯搜索法,它是一种搜索的方式。回溯是递归的副产品,只要有递归就会有回溯。

  2. 本质是穷举,穷举所有可能,然后选出我们想要的答案,所以效率不高。

  3. 解决的问题:

    • 组合问题:N个数里面按一定规则找出k个数的集合
    • 切割问题:一个字符串按一定规则有几种切割方式
    • 子集问题:一个N个数的集合里有多少符合条件的子集
    • 排列问题:N个数按一定规则全排列,有几种排列方式
    • 棋盘问题:N皇后,解数独等等
  4. 模板:

    回溯法一般是在集合中递归搜索,集合的大小构成了树的宽度,递归的深度构成的树的深度。for循环就是遍历集合区间,可以理解一个节点有多少个孩子,这个for循环就执行多少次。

    回溯算法理论基础
    void backtracking(参数) {
    if (终止条件) {
    存放结果;
    return;
    }
    for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
    处理节点;
    backtracking(路径,选择列表); // 递归
    回溯,撤销处理结果
    }
    }

2. 77 组合

题目:给定两个整数 n 和 k,返回 1 ... n 中所有可能的 k 个数的组合。

示例: 输入: n = 4, k = 2 输出: [ [2,4], [3,4], [2,3], [1,2], [1,3], [1,4], ]


a. 如何想到回溯

直观的方法,k=2就是两层循环,k=50就是50层循环嵌套。

回溯可以解决很多层for循环嵌套问题。

回溯问题可以抽象为树形结构,如图:

77.组合
  • 每次从集合中选取元素,可选择的范围随着选择的进行而收缩,调整可选择的范围。
  • 图中可以发现n相当于树的宽度,k相当于树的深度。
  • 图中每次搜索到了叶子节点,我们就找到了一个结果。

b. 回溯三部曲

  1. 递归函数的返回值以及参数

在这里要定义两个全局变量,一个用来存放符合条件单一结果,一个用来存放符合条件结果的集合。

vector<vector<int>> result; // 存放符合条件结果的集合
vector<int> path; // 用来存放符合条件结果

函数里一定有两个参数,既然是集合n里面取k个数,那么n和k是两个int型的参数。

然后还需要一个参数,为int型变量startIndex,这个参数用来记录本层递归的中,集合从哪里开始遍历(集合就是[1,...,n] ),防止出现重复的组合。

  1. 回溯函数终止条件

什么时候到达所谓的叶子节点了呢?path这个数组的大小如果达到k,说明我们找到了一个子集大小为k的组合了,在图中path存的就是根节点到叶子节点的路径。

  1. 单层搜索的过程

for循环每次从startIndex开始遍历,然后用path保存取到的节点i。然后backtracking(递归函数)通过不断调用自己一直往深处遍历,总会遇到叶子节点,遇到了叶子节点就要返回。backtracking的下面就是回溯的操作,撤销本次处理的结果。

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;
}
};
  • 时间复杂度: O(n * 2^n)

    对于每一个 n,都有两种选择:要么被选中加入到 path 中,要么不被选中。因此,对于 n 个元素,理论上最多有 2^n 种选择方式。最多k=n,所以再乘一个n。

  • 空间复杂度: O(n)

c. 剪枝优化

如果for循环选择的起始位置之后的元素个数 已经不足 我们需要的元素个数了,那么就没有必要搜索了

77.组合4

优化过程如下:

  1. 已经选择的元素个数:path.size();
  2. 所需需要的元素个数为: k - path.size();
  3. 列表中剩余元素(n-i) >= 所需需要的元素个数(k - path.size())
  4. 在集合n中至多要从该起始位置 : i <= n - (k - path.size()) + 1,开始遍历

为什么有个+1呢,因为包括起始位置,我们要是一个左闭的集合。举个例子,n = 4,k = 3, 目前已经选取的元素为0(path.size为0)n - (k - 0) + 14 - ( 3 - 0) + 1 = 2

从2开始搜索都是合理的,可以是组合[2, 3, 4]。

for (int i = startIndex; i <= n - (k - path.size()) + 1; i++) // i为本次搜索的起始位置

3. 316 组合Ⅲ

题目:找出所有相加之和为 n 的 k 个数的组合。组合中只允许含有 1 - 9 的正整数,并且每种组合中不存在重复的数字。

  • 所有数字都是正整数。
  • 解集不能包含重复的组合。

示例 1: 输入: k = 3, n = 7 输出: [[1,2,4]]

示例 2: 输入: k = 3, n = 9 输出: [[1,2,6], [1,3,5], [2,3,4]]


和上一题的组合异曲同工,需要注意的地方:不是所有叶子节点都要,只要sum=target的结果。所以这个判断在代码的if中。

216.组合总和III
class Solution {
private:
vector<vector<int>> result; // 存放结果集
vector<int> path; // 符合条件的结果
void backtracking(int targetSum, int k, int sum, int startIndex) {
if (path.size() == k) {
if (sum == targetSum) result.push_back(path);
return; // 如果path.size() == k 但sum != targetSum 直接返回
}
for (int i = startIndex; i <= 9; i++) {
sum += i; // 处理
path.push_back(i); // 处理
backtracking(targetSum, k, sum, i + 1); // 注意i+1调整startIndex
sum -= i; // 回溯
path.pop_back(); // 回溯
}
}
public:
vector<vector<int>> combinationSum3(int k, int n) {
result.clear(); // 可以不加
path.clear(); // 可以不加
backtracking(n, k, 0, 1);
return result;
}
};

剪枝的方法:

已选元素总和如果已经大于n(图中数值为4)了,那么往后遍历就没有意义了,直接剪掉。

class Solution {
private:
vector<vector<int>> result; // 存放结果集
vector<int> path; // 符合条件的结果
void backtracking(int targetSum, int k, int sum, int startIndex) {
if (sum > targetSum) { // 剪枝操作
return;
}
if (path.size() == k) {
if (sum == targetSum) result.push_back(path);
return; // 如果path.size() == k 但sum != targetSum 直接返回
}
for (int i = startIndex; i <= 9 - (k - path.size()) + 1; i++) { // 剪枝
sum += i; // 处理
path.push_back(i); // 处理
backtracking(targetSum, k, sum, i + 1); // 注意i+1调整startIndex
sum -= i; // 回溯
path.pop_back(); // 回溯
}
}
public:
vector<vector<int>> combinationSum3(int k, int n) {
result.clear(); // 可以不加
path.clear(); // 可以不加
backtracking(n, k, 0, 1);
return result;
}
};
  1. 注意剪枝的两个地方:sum和k
  2. 有了sum,那么每次递归和回溯需要修改sum的值,不是只有path了。

4. 17.电话号码的字母组合

题目:给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。答案可以按 任意顺序 返回。

给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。

img

输入:digits = "23"
输出:["ad","ae","af","bd","be","bf","cd","ce","cf"]

要解决如下三个问题:

  1. 数字和字母如何映射

  2. 两个字母就两个for循环,三个字符就三个for循环,明显是多层for循环问题,可以用回溯解决,属于回溯中的组合问题,和经典组合不同的地方在于求不同集合之间的组合,所以每个for循环需要在固定的个数数字中挑一个,画出树状图:

    17. 电话号码的字母组合
  3. 输入1 * #按键等等异常情况

a. 数字和字母如何映射

映射问题,可以使用map或者定义一个二维数组,例如:string letterMap[10],定义一个二维数组,代码如下:

const string letterMap[10] = {
"", // 0
"", // 1
"abc", // 2
"def", // 3
"ghi", // 4
"jkl", // 5
"mno", // 6
"pqrs", // 7
"tuv", // 8
"wxyz", // 9
};

b. 回溯法解决n个for循环的问题

  1. 确定回溯函数参数

全局:字符串s来收集叶子节点的结果,然后用一个字符串数组result保存起来。

参数:参数指定是有题目中给的string digits,然后还要有一个参数就是int型的index。

注意这个index可不是 前两道题中的startIndex了,这个index是记录遍历第几个数字了,即用来遍历digits的(题目中给出数字字符串),同时index也表示树的深度。

  1. 确定终止条件

例如输入用例"23",两个数字,那么根节点往下递归两层就可以了,叶子节点就是要收集的结果集。那么终止条件就是如果index 等于 输入的数字个数(digits.size)了(本来index就是用来遍历digits的)。然后收集结果,结束本层递归。

  1. 确定单层遍历逻辑

首先要取index指向的数字,并找到对应的字符集(手机键盘的字符集)。然后for循环来处理这个字符集,for循环中递归调用和回溯。

class Solution {
private:
const string letterMap[10] = {
"", // 0
"", // 1
"abc", // 2
"def", // 3
"ghi", // 4
"jkl", // 5
"mno", // 6
"pqrs", // 7
"tuv", // 8
"wxyz", // 9
};
public:
vector<string> result;
string s;
void backtracking(const string& digits, int index) {
if (index == digits.size()) {
result.push_back(s);
return;
}
int digit = digits[index] - '0'; // 将index指向的数字转为int
string letters = letterMap[digit]; // 取数字对应的字符集
for (int i = 0; i < letters.size(); i++) {
s.push_back(letters[i]); // 处理
backtracking(digits, index + 1); // 递归,注意index+1,一下层要处理下一个数字了
s.pop_back(); // 回溯
}
}
vector<string> letterCombinations(string digits) {
s.clear();
result.clear();
if (digits.size() == 0) {
return result;
}
backtracking(digits, 0);
return result;
}
};
  1. 二位数组可以做到数字和字母映射
  2. 递归回溯问题,可以试试画树状图,从而可以看出for循环中循环多少次
  3. 此题中,index很重要,在for循环前,通过index确定取的是二维数组中哪个数字对应的字母
  4. 注意此题空间复杂度,递归问题的空间复杂度和递归层数有关系

今日古诗

八声甘州·记玉关踏雪事清游

张炎〔宋代〕

辛卯岁,沈尧道同余北归,各处杭、越。逾岁,尧道来问寂寞,语笑数日。又复别去。赋此曲,并寄赵学舟。

记玉关踏雪事清游,寒气脆貂裘。傍枯林古道,长河饮马,此意悠悠。短梦依然江表,老泪洒西州。一字无题处,落叶都愁。
载取白云归去,问谁留楚佩,弄影中洲?折芦花赠远,零落一身秋。向寻常、野桥流水,待招来,不是旧沙鸥。空怀感,有斜阳处,却怕登楼。

这首词上片以“记”字领起,气势较为开阔、笔力劲峭。写词人前年冬季赴北写经的旧事,展现了一幅冲风踏雪的北国羁旅图;旧事重提之后,中片则续写北地回归之光景;下片从眼前的离别写起,表现出零落如秋叶的心情。全词先悲后壮,先友情而后国恨,惯穿始终的,是一股荡气回肠的“词气”,词中写身世飘萍和国事之悲感哀婉动人,令人如闻断雁惊风、哀猿啼月。

posted @   YueHuai  阅读(542)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 25岁的心里话
· 按钮权限的设计及实现
点击右上角即可分享
微信分享提示