文章目录
一、算法介绍
(结合力扣101教材和代码随想录)
深度优先搜索和广度优先搜索是两种最常见的优先搜索方法,它们被广泛地运用在图和树等结构中进行搜索。
二、深度优先搜索
深度优先搜索(depth-first search,DFS)在搜索到一个新的节点时,立即对该新节点进行遍历;因此遍历需要用先入后出的栈来实现,也可以通过与栈等价的递归来实现。对于树结构而言,由于总是对新节点调用遍历,因此看起来是向着“深”的方向前进。
考虑如下一棵简单的树。我们从1号节点开始遍历,假如遍历顺序是从左子节点到右子节点,那么按照优先向着“深”的方向前进的策略,假如我们使用递归实现,我们的遍历过程为1
(起始节点)->2
(遍历更深一层的左子节点)->4
(遍历更深一层的左子节点)->2
(无子节点,返回父节点)->1
(子节点均已完成遍历,返回父节点)->3
(遍历更深一层的右子节点)->1
(无子节点,返回父节点)->结束程序(子节点均已完成遍历)。如果我们用栈实现,我们的栈顶元素的变化过程为1->2->4->3
。
深度优先搜索也可以用来检测环路:记录每个遍历过的节点的父节点,若一个节点被再次遍历且父节点不同,则说明有环。我们也可以用之后会讲到的拓扑排序判断是否有环路,若最后存在入度不为0的点,则证明有环。
有时我们可能会需要对已经搜索过的节点进行标记,以防止在遍历时重复搜索某个节点,这种做法叫做状态记录或记忆化。
岛屿问题汇总
岛屿问题建议参考力扣作者nettee的题解,我觉得模板更为清晰易懂,对于小白如我也能很容易地理解~在此搬运大佬的DFS代码框架,改成C++版本:
// 标记已遍历过的岛屿,不做重复遍历
void dfs(vector<vector<int>>& grid, int r, int c) {
if (!(0 <= r && r < grid.size() && 0 <= c && c < grid[0].size())) {
return 0;
}
// 已遍历过(值为2)的岛屿在这里会直接返回,不会重复遍历
if (grid[r][c] != 1) {
return 0;
}
grid[r][c] = 2; // 将方格标记为"已遍历",不做这一步可能会导致DFS不停地兜圈子
dfs(grid, r - 1, c);
dfs(grid, r + 1, c);
dfs(grid, r, c - 1);
dfs(grid, r, c + 1);
}
695. 岛屿的最大面积
给你一个大小为 m x n
的二进制矩阵 grid
。
岛屿 是由一些相邻的 1 (代表土地)
构成的组合,这里的「相邻」要求两个 1
必须在 水平或者竖直的四个方向上
相邻。你可以假设 grid 的四个边缘都被 0(代表水)
包围着。
岛屿的面积是岛上值为 1
的单元格的数目。
计算并返回 grid 中最大的岛屿面积。如果没有岛屿,则返回面积为 0
。
示例 1:
输入:grid = [[0,0,1,0,0,0,0,1,0,0,0,0,0],[0,0,0,0,0,0,0,1,1,1,0,0,0],[0,1,1,0,1,0,0,0,0,0,0,0,0],[0,1,0,0,1,1,0,0,1,0,1,0,0],[0,1,0,0,1,1,0,0,1,1,1,0,0],[0,0,0,0,0,0,0,0,0,0,1,0,0],[0,0,0,0,0,0,0,1,1,1,0,0,0],[0,0,0,0,0,0,0,1,1,0,0,0,0]]
输出:6
解释:答案不应该是 11 ,因为岛屿只能包含水平或垂直这四个方向上的 1 。
示例 2:
输入:grid = [[0,0,0,0,0,0,0,0]]
输出:0
思路:
使用模板解题,需要补充的部分为“没遍历到一个格子,需要将面积加1”。
代码:
class Solution {
public:
int maxAreaOfIsland(vector<vector<int>>& grid) {
if (grid.empty() || grid[0].empty()){
return 0;
}
//每遍历到一个格子,就将面积加1
int res = 0;
//r为row(行),c为column(列)
for (int r = 0;r < grid.size();r++){
for (int c = 0;c < grid[0].size();c++){
if (grid[r][c] == 1){
int a = area(grid,r,c);
res = max(res,a);
}
}
}
return res;
}
int area(vector<vector<int>>& grid, int r, int c){
//判断base case
//如果坐标(r,c)超出了网格范围,返回0
if (!inArea(grid,r,c)){
return 0;
}
//如果这个格子不是岛屿,返回0
if (grid[r][c] != 1){
return 0;
}
//将格子标记为【已遍历过】
grid[r][c] = 2;
return 1 + area(grid,r+1,c) + area(grid,r-1,c) + area(grid,r,c+1) + area(grid,r,c-1);
}
bool inArea(vector<vector<int>>& grid, int r, int c){
return 0 <= r && r < grid.size() && 0 <= c && c < grid[0].size();
}
};
200. 岛屿数量
给你一个由 '1'(陆地)
和 '0'(水)
组成的的二维网格,请你计算网格中岛屿的数量。
岛屿总是被水包围,并且每座岛屿只能由水平方向和/或竖直方向上相邻的陆地连接形成。
此外,你可以假设该网格的四条边均被水包围。
示例 1:
输入:grid = [
[“1”,“1”,“1”,“1”,“0”],
[“1”,“1”,“0”,“1”,“0”],
[“1”,“1”,“0”,“0”,“0”],
[“0”,“0”,“0”,“0”,“0”]
]
输出:1
示例 2:
输入:grid = [
[“1”,“1”,“0”,“0”,“0”],
[“1”,“1”,“0”,“0”,“0”],
[“0”,“0”,“1”,“0”,“0”],
[“0”,“0”,“0”,“1”,“1”]
]
输出:3
思路:
套用模板即可。当遍历到海洋或者超出网格边界就停止执行dfs。
代码:
class Solution {
public:
int numIslands(vector<vector<char>>& grid) {
if(grid.empty() || grid[0].empty()){
return 0;
}
int num = 0;
for(int r = 0;r < grid.size();r++){
for (int c = 0;c < grid[0].size();c++){
if (grid[r][c] == '1'){
num++;
dfs(grid,r,c);
}
//num = nums + dfs(grid,r,c);
}
}
return num;
}
void dfs(vector<vector<char>>& grid, int r, int c){
if(!(0 <= r && r < grid.size() && 0 <= c && c <grid[0].size())){
return;
}
if(grid[r][c] != '1'){
return;
}
grid[r][c] = '2';
dfs(grid,r+1,c);
dfs(grid,r-1,c);
dfs(grid,r,c+1);
dfs(grid,r,c-1);
}
};
463.岛屿的周长
给定一个 row x col
的二维网格地图 grid
,其中:grid[i][j] = 1
表示陆地, grid[i][j] = 0
表示水域。
网格中的格子 水平和垂直
方向相连(对角线方向不相连)。整个网格被水完全包围,但其中恰好有一个岛屿(或者说,一个或多个表示陆地的格子相连组成的岛屿)。
岛屿中没有“湖”(“湖” 指水域在岛屿内部且不和岛屿周围的水相连)。格子是边长为 1 的正方形。网格为长方形,且宽度和高度均不超过 100 。计算这个岛屿的周长。
示例 1:
输入:grid = [[0,1,0,0],[1,1,1,0],[0,1,0,0],[1,1,0,0]]
输出:16
解释:它的周长是上面图片中的 16 个黄色的边
示例 2:
输入:grid = [[1]]
输出:4
示例 3:
输入:grid = [[1,0]]
输出:4
思路1-数学法:
对于岛屿,直接用数学的方法求周长(题目中明确 恰好只有一个岛屿)。一块土地给整个岛屿的周长贡献4条边长,每当这块土地与另一块土地接壤,岛屿的周长就需要减掉两个边长,所以总周长 = 4 * 土地的块数 - 2 * 接壤的边数
,遍历二维数组,如果是“1”,则为土地,land++
;如果其右方或者下方也是土地,则border++
;遍历结束代入上述公式。
代码1:
class Solution {
public:
int islandPerimeter(vector<vector<int>>& grid) {
int land = 0;
int border = 0;
for (int r = 0;r < grid.size();r++){
for (int c = 0;c < grid[0].size();c++){
if (grid[r][c] == 1){
land++;
if (c < grid[0].size() - 1 && grid[r][c+1] == 1){
border++;
}
if (r < grid.size() - 1 && grid[r+1][c] == 1){
border++;
}
}
}
}
return (4 * land - 2 * border);
}
};
思路2-DFS法:
在模板的基础上,我们需要进一步判断不是岛屿的格子是海洋还是已经遍历过的岛屿格子,如果grid[r][c] == 0
,为海洋格子;如果grid[r][c] == 2
,为遍历过的岛屿格子。
岛屿的周长可以分为两种:一种是与网格边界相邻的周长,另一种是与海洋相邻格子的周长。当DFS遍历到[r][c]
超出范围时,经过了一条类型Ⅰ的周长,当DFS遍历到当前格子是海洋格子的时候,经过了一条类型Ⅱ的周长。
代码2:
class Solution {
public:
int islandPerimeter(vector<vector<int>>& grid) {
if (grid.empty() || grid[0].empty()){
return 0;
}
int len = 0;
for (int r = 0;r < grid.size();r++){
for (int c = 0;c < grid[0].size();c++){
if (grid[r][c] == 1){
len = dfs(grid,r,c);
}
}
}
return len;
}
int dfs(vector<vector<int>>& grid, int r,int c){
if (!(0 <= r && r < grid.size() && 0 <= c && c < grid[0].size())){
return 1;
}
if (grid[r][c] == 0){
return 1;
}
else if (grid[r][c] == 2){
return 0;
}
grid[r][c] = 2;
return dfs(grid,r+1,c) + dfs(grid,r-1,c) + dfs(grid,r,c+1) + dfs(grid,r,c-1);
}
};
827. 最大人工岛
给你一个大小为 n x n
二进制矩阵 grid
。最多 只能将一格 0
变成 1
。
返回执行此操作后,grid
中最大的岛屿面积是多少?
岛屿 由一组上、下、左、右四个方向相连的 1
形成。
示例 1:
输入: grid = [[1, 0], [0, 1]]
输出: 3
解释: 将一格0变成1,最终连通两个小岛得到面积为 3 的岛屿。
示例 2:
输入: grid = [[1, 1], [1, 0]]
输出: 4
解释: 将一格0变成1,岛屿的面积扩大为 4。
示例 3:
输入: grid = [[1, 1], [1, 1]]
输出: 4
解释: 没有0可以让我们变成1,面积依然为 4。
思路:
框架仍然用nettee大佬的DFS框架,我也找到了大佬的题解但是较为简略。代码部分参考题解1,图解部分参考题解2。
图解思路:
第一次DFS:
第二次DFS:
代码:
class Solution {
public:
int largestIsland(vector<vector<int>>& grid) {
int res = 0;//储存最大岛屿面积
int index = 2;//index表示岛屿的编号,0是海洋1是陆地,为避免混淆从2开始遍历
unordered_map<int, int> find;//岛屿编号:岛屿面积
int area = 0;//储存每个岛屿面积
//计算每个岛屿的面积,并标记是第几个岛屿
for(int i = 0; i < grid.size(); i++){
for(int j = 0; j < grid[i].size(); j++){
if(grid[i][j] == 1){
area = dfs(grid, i, j, index);
res = max(res, area);
find[index] = area;
++index;
}
}
}
if(res == 0) return 1;//如果res = 0表示网格中没有陆地,那么造一块,返回1即可
//遍历海洋格子,假设这个格子填充,那么就把上下左右是陆地的格子所在的岛屿连接起来
for(int i = 0; i < grid.size(); i++){
for(int j = 0; j < grid[i].size(); j++){
if(grid[i][j] == 0){
// 记录搜集当前海洋格子上下左右有陆地的下标 并用set去重(set最主要的作用就是自动去重并按升序排序)
unordered_set<int> set = find_outside_area_index(grid, i, j);
if(set.empty()) continue;周围没有陆地,所以continue
area = 1;//此处直接将我们填海造陆的1计算进去
for(const auto& set_i : set){//该格子填充,则上下左右的陆地的都连接了,通过序号获得面积,加上面积
area += find[set_i];
}
res = std::max(res, area);
}
}
}
return res == 0 ? 1 : res;
}
private:
bool out_range(vector<vector<int>>& map, int i, int j){//超出范围的情况
return (i >= map.size() || j >= map[0].size() || i < 0 || j < 0);
}
int dfs(vector<vector<int>>& map, int i, int j, int index){
if(out_range(map, i, j) || map[i][j] != 1) return 0;
map[i][j] = index;
return 1 + dfs(map, i+1, j, index) + dfs(map, i-1, j, index) + dfs(map, i, j+1, index) + dfs(map, i, j-1, index);
}
// 对于海洋格子,找到上下左右每个方向,都要确保不是out_range以及是陆地格子,则表示是该海洋格子的陆地邻居
std::unordered_set<int> find_outside_area_index(vector<vector<int>>& map, int i, int j){
unordered_set<int> set;//需要注意这里的写法
if(!out_range(map, i+1, j) && map[i+1][j] != 0) set.insert(map[i+1][j]);//右
if(!out_range(map, i, j+1) && map[i][j+1] != 0) set.insert(map[i][j+1]);//下
if(!out_range(map, i-1, j) && map[i-1][j] != 0) set.insert(map[i-1][j]);//左
if(!out_range(map, i, j-1) && map[i][j-1] != 0) set.insert(map[i][j-1]);//上
return set;
}
};
547. 省份数量
有 n
个城市,其中一些彼此相连,另一些没有相连。如果城市 a
与城市 b
直接相连,且城市 b
与城市 c
直接相连,那么城市 a
与城市 c
间接相连。
省份 是一组直接或间接相连的城市,组内不含其他没有相连的城市。
给你一个 n x n 的矩阵 isConnected
,其中 isConnected[i][j] = 1
表示第 i
个城市和第 j
个城市直接相连,而 isConnected[i][j] = 0
表示二者不直接相连。
返回矩阵中 省份
的数量。
示例 1:
输入:isConnected = [[1,1,0],[1,1,0],[0,0,1]]
输出:2
示例 2:
输入:isConnected = [[1,0,0],[0,1,0],[0,0,1]]
输出:3
思路:
1.建立一个bool类型容器记录已经搜索过的城市;
2.遍历,遇到没有搜索过的城市DFS,搜索与它相连的所有城市并更新哈希表;
3.记录DFS的次数,返回。
代码:
class Solution {
public:
int findCircleNum(vector<vector<int>>& net) {
if (net.empty() || net[0].empty()){
return 0;
}
int num = 0;
int n = net.size();
vector<bool> visited(n,false);
for (int i = 0;i < net.size();i++){
if (!visited[i]){
dfs(net,i,visited);
num++;
}
}
return num;
}
void dfs(vector<vector<int>>& net, int i, vector<bool>& visited){
visited[i] = true;
int n = net.size();
for (int j = 0;j < n;j++){
if (!visited[j] && net[i][j]){
dfs(net,j,visited);
}
}
}
};
417. 太平洋大西洋水流问题
有一个 m × n
的矩形岛屿,与 太平洋
和 大西洋
相邻。 “太平洋” 处于大陆的左边界和上边界,而 “大西洋” 处于大陆的右边界和下边界。
这个岛被分割成一个由若干方形单元格组成的网格。给定一个 m x n
的整数矩阵 heights
, heights[r][c]
表示坐标 (r, c)
上单元格 高于海平面的高度
。
岛上雨水较多,如果相邻单元格的高度 小于或等于
当前单元格的高度,雨水可以直接向北、南、东、西流向相邻单元格。水可以从海洋附近的任何单元格流入海洋。
返回网格坐标 result
的 2D 列表 ,其中 result[i] = [ri, ci]
表示雨水从单元格 (ri, ci)
流动 既可流向太平洋也可流向大西洋 。
示例 1:
输入: heights = [[1,2,2,3,5],[3,2,3,4,4],[2,4,5,3,1],[6,7,1,4,5],[5,1,1,2,4]]
输出: [[0,4],[1,3],[1,4],[2,2],[3,0],[3,1],[4,0]]
示例 2:
输入: heights = [[2,1],[1,2]]
输出: [[0,0],[0,1],[1,0],[1,1]]
思路:
参考力扣题解,逆向思维,改为找可以从太平洋或大西洋逆流而上的点。
- 找到所有从太平洋可以到达的点;
- 找到所有从大西洋可以到达的点;
- 两个重复的点即为要找的点。
代码:
class Solution {
public:
//P 用于记录从太平洋出发所能达到的点
//A 用于记录从大西洋出发所能到达的点
vector<vector<int> > P,A,ans;
int row,col;
vector<vector<int>> pacificAtlantic(vector<vector<int>>& heights) {
row = heights.size();
col = heights[0].size();
P = A = vector<vector<int> > (row, vector<int>(col,0));
//左右两边 和 上下两边 出发深搜
//左右两边深搜
for (int r = 0;r < row;r++){
dfs(heights,P,r,0);//左边是太平洋
dfs(heights,A,r,col - 1);//右边是大西洋
}
//上下两边深搜
for (int c = 0;c < col;c++){
dfs(heights,P,0,c);//上边是太平洋
dfs(heights,A,row - 1,c);//下边是大西洋
}
return ans;
}
//这里的visited是用引入的方法传参 实际上代表P A
void dfs(vector<vector<int>>& heights, vector<vector<int>>& visited, int r, int c){
//如果某个点已经遍历过 返回
if (visited[r][c]){
return;
}
visited[r][c] = 1;
//每遍历完一个点后检查这个点是否能从 P 和 A 到达
if (P[r][c] && A[r][c]) ans.push_back({r,c});
//上下左右深搜 水往高处流,只有下一个点大于或等于当前点时,水才能流过去
if (r - 1 >= 0 && heights[r - 1][c] >= heights[r][c]) dfs(heights,visited,r - 1,c);
if (r + 1 < row && heights[r + 1][c] >= heights[r][c]) dfs(heights,visited,r + 1,c);
if (c - 1 >= 0 && heights[r][c - 1] >= heights[r][c]) dfs(heights,visited,r,c - 1);
if (c + 1 < col && heights[r][c + 1] >= heights[r][c]) dfs(heights,visited,r,c + 1);
}
};
三、回溯法
回溯法是优先搜索的一种特殊情况,又称为试探法,常用于需要记录节点状态的深度优先搜索。通常来说,排列、组合、选择类问题使用回溯法比较方便。
回溯法的核心是回溯。当搜索到某一节点时,如果我们发现目前的节点(及其子节点)并不是需求目标时,我们回退到原来的节点继续搜索,并且把在目前节点修改的状态还原。这样的好处是我们可以始终只对图的总状态进行修改,而非每次遍历时新建一个图来储存状态。在具体的写法上,它与普通的深度优先搜索一样,都有[修改当前节点状态]—[递归子节点]的步骤,只是多了回溯的步骤,变成了[修改当前节点状态]—[递归子节点]—[回改当前节点状态]。
回溯法修改一般有两种情况:一种是修改最后一位输出,比如排列组合;一种是修改访问标记,比如矩阵里搜字符串。
以上为力扣101书中介绍,个人认为较为简略,对于小白来说理解很吃力。这里采用代码随想录的解题思路。
白话版: 深度优先搜索是可着一个方向去搜,不到黄河不回头,直到撞到南墙,搜不下去了,再换方向(换方向的过程就涉及到了回溯)。
DFS的关键点:
- 搜索方向:认准一个方向搜,直到碰壁后换方向
- 换方向:撤销原路径,改为节点链接的下一个路径,称为回溯。
代码框架:
有递归的地方就有回溯,代码如下:
void dfs(参数) {
处理节点
dfs(图,选择的节点); // 递归
回溯,撤销处理结果
}
回溯法的代码框架:
void backtracking(参数) {
if (终止条件) {
存放结果;
return;
}
for (选择:本层集合中元素(树中节点***的数量就是集合的大小)) {
处理节点;
backtracking(路径,选择列表); // 递归
回溯,撤销处理结果
}
}
DFS的代码框架:
void dfs(参数) {
if (终止条件) {
存放结果;
return;
}
for (选择:本节点所连接的其他节点) {
处理节点;
dfs(图,选择的节点); // 递归
回溯,撤销处理结果
}
}
DFS三部曲:
一、确认递归函数、参数
void dfs(参数)
一般情况下,DFS需要二维数组
保存所有路径,需要一维数组
保存单一路径。比如:
vector<vector<int>> result; // 保存符合条件的所有路径
vector<int> path; // 起点到终点的路径
void dfs (图,目前搜索的节点)
二、确认终止条件
DFS代码出现死循环、栈溢出等等问题往往都是因为终止条件没有想清楚。
if (终止条件) {
存放结果;
return;
}
终止条件不仅是结束本层递归,同时也是得到最终结果的时刻。
三、处理目前搜索节点出发的路径
一般这里为一个for
循环,取遍历目前搜索节点所能到的所有节点。
for (选择:本节点所连接的其他节点) {
处理节点;
dfs(图,选择的节点); // 递归
回溯,撤销处理结果
}
回溯算法的题目包括:组合、切割、子集、排列、棋盘问题等。
组合
77. 组合
给定两个整数 n
和 k
,返回范围 [1, n]
中所有可能的 k
个数的组合。
你可以按 任何顺序
返回答案。
示例 1:
输入:n = 4, k = 2
输出:
[
[2,4],
[3,4],
[2,3],
[1,2],
[1,3],
[1,4],
]
示例 2:
输入:n = 1, k = 1
输出:[[1]]
思路:
注意:
举一个例子,n = 4
,k = 4
的话,那么第一层for
循环的时候,从元素2
开始的遍历都没有意义了。 在第二层for
循环,从元素3
开始的遍历都没有意义了。
这么说有点抽象,如图所示:
图中每一个节点(图中为矩形),就代表本层的一个for
循环,那么每一层的`for循环从第二个数开始遍历的话,都没有意义,都是无效遍历。
所以,可以剪枝的地方就在递归中每一层的for
循环所选择的起始位置。
如果for
循环选择的起始位置之后的元素个数 已经不足
我们需要的元素个数了,那么就没有必要搜索了。
为什么有个+1呢,因为包括起始位置,我们要是一个左闭的集合。
举个例子,n = 4
,k = 3
, 目前已经选取的元素为0
(path.size
为0
),n - (k - 0) + 1
即 4 - ( 3 - 0) + 1 = 2
。
从2
开始搜索都是合理的,可以是组合[2, 3, 4]
。
对代码可以进行如下优化:
for (int i = startIndex; i <= n; i++)
已经选择的元素个数:path.size()
;
还需要的元素个数为: k - path.size()
;
在集合n中至多要从该起始位置 : n - (k - path.size()) + 1
,开始遍历。
代码:
class Solution {
public:
vector<vector<int>> res;
vector<int> path;
vector<vector<int>> combine(int n, int k) {
res.clear();
path.clear();
backtracking(n,k,1);
return res;
}
void backtracking(int n, int k, int startIndex){
if (path.size() == k){
res.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();
}
}
};
39. 组合总和
给你一个 无重复元素 的整数数组 candidates
和一个目标整数 target
,找出 candidates
中可以使数字和为目标数 target
的 所有
不同组合 ,并以列表形式返回。你可以按 任意顺序
返回这些组合。
candidates
中的 同一个 数字可以 无限制重复被选取
。如果至少一个数字的被选数量不同,则两种组合是不同的。
对于给定的输入,保证和为 target
的不同组合数少于 150 个。
示例 1:
输入:candidates = [2,3,6,7], target = 7
输出:[[2,2,3],[7]]
解释:
2 和 3 可以形成一组候选,2 + 2 + 3 = 7 。注意 2 可以使用多次。
7 也是一个候选, 7 = 7 。
仅有这两种组合。
示例 2:
输入: candidates = [2,3,5], target = 8
输出: [[2,2,2,2],[2,3,3],[3,5]]
示例 3:
输入: candidates = [2], target = 1
输出: []
优化:
对总集合排序之后,如果下一层的sum
(就是本层的 sum + candidates[i]
)已经大于target
,就可以结束本轮for
循环的遍历。
代码-优化版:
class Solution {
public:
//思路:
vector<vector<int>> res;
vector<int> path;
vector<vector<int>> combinationSum(vector<int>& nums, int target) {
res.clear();
path.clear();
sort(nums.begin(),nums.end());//首先排序
backtracking(nums,target,0,0);
return res;
}
void backtracking(vector<int>& nums, int target, int sum, int startIndex){
//终止条件
if (sum == target){
res.push_back(path);
return;
}
if (sum > target){
return;
}
for (int i = startIndex;i < nums.size() && sum + nums[i] <= target;i++){
sum += nums[i];
path.push_back(nums[i]);
backtracking(nums,target,sum,i);//此处不是i+1 表示可以重复读取当前的数
sum -= nums[i];
path.pop_back();
}
}
};
40. 组合总和 II
给定一个候选人编号的集合 candidates
和一个目标数 target
,找出 candidates
中所有可以使数字和为 target
的组合。
candidates
中的每个数字在每个组合中只能使用 一次 。
注意:解集不能包含重复的组合
。
示例 1:
输入: candidates = [10,1,2,7,6,1,5], target = 8,
输出:
[
[1,1,6],
[1,2,5],
[1,7],
[2,6]
]
示例 2:
输入: candidates = [2,5,2,1,2], target = 5,
输出:
[
[1,2,2],
[5]
]
思路:
和全排列中的去重思路一致。
代码:
class Solution {
public:
//和39的区别是每个元素只能使用一次 且最后不能有重复的组合 需要加一个used
//不能先求出最后结果然后使用set去重 因为很容易造成超时
vector<vector<int>> res;
vector<int> path;
vector<vector<int>> combinationSum2(vector<int>& nums, int target) {
res.clear();
path.clear();
sort(nums.begin(),nums.end());//去重注意先排序
vector<bool> used(nums.size(),false);
backtracking(nums,used,target,0,0);
return res;
}
void backtracking(vector<int>& nums, vector<bool>& used, int target, int sum, int startIndex){
//终止条件
if (sum == target){
res.push_back(path);
return;
}
if (sum > target){
return;
}
//处理节点
for (int i = startIndex;i < nums.size() && sum + nums[i] <= target;i++){//剪枝优化
if (i > 0 && nums[i] == nums[i-1] && used[i-1] == false) continue;//这里注意是i-1
if (used[i] == false){
used[i] = true;
sum += nums[i];
path.push_back(nums[i]);
backtracking(nums,used,target,sum,i+1);
sum -= nums[i];
used[i] = false;
path.pop_back();
}
}
}
};
set去重超时测试用例:
[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1]
30
216. 组合总和 III
找出所有相加之和为 n
的 k
个数的组合,且满足下列条件:
只使用数字1
到9
每个数字 最多使用一次
返回 所有可能的有效组合的列表
。该列表不能包含相同的组合两次,组合可以以任何顺序返回。
示例 1:
输入: k = 3, n = 7
输出: [[1,2,4]]
解释:
1 + 2 + 4 = 7
没有其他符合的组合了。
示例 2:
输入: k = 3, n = 9
输出: [[1,2,6], [1,3,5], [2,3,4]]
解释:
1 + 2 + 6 = 9
1 + 3 + 5 = 9
2 + 3 + 4 = 9
没有其他符合的组合了。
示例 3:
输入: k = 4, n = 1
输出: []
解释: 不存在有效的组合。
在[1,9]范围内使用4个不同的数字,我们可以得到的最小和是1+2+3+4 = 10,因为10 > 1,没有有效的组合。
思路:
与77类似。
代码:
class Solution {
public:
//此处的n相当于target k限制数字的个数 是backtracking的参数
vector<vector<int>> res;
vector<int> path;
vector<vector<int>> combinationSum3(int k, int n) {
res.clear();
path.clear();
backtracking(k,n,0,1);
return res;
}
void backtracking(int k, int n, int sum, int startIndex){
//终止条件
if (sum == n && path.size() == k){
res.push_back(path);
return;
}
if (sum > n){
return;
}
for (int i = startIndex;i <= 9 - (k - path.size()) + 1;i++){//剪枝优化
sum += i;
path.push_back(i);
backtracking(k,n,sum,i+1);
sum -= i;
path.pop_back();
}
}
};
17. 电话号码的字母组合
给定一个仅包含数字 2-9
的字符串,返回所有它能表示的字母组合。答案可以按 任意顺序
返回。
给出数字到字母的映射如下(与电话按键相同)。注意 1
不对应任何字母。
示例 1:
输入:digits = “23”
输出:[“ad”,“ae”,“af”,“bd”,“be”,“bf”,“cd”,“ce”,“cf”]
示例 2:
输入:digits = “”
输出:[]
示例 3:
输入:digits = “2”
输出:[“a”,“b”,“c”]
思路:
与之前的组合题不同,该题是不同集合之间的组合,重点需要解决三个问题:
- 数字和字母如何映射:此处采用
string
字符串储存,也可以使用map
储存; - 两个字母就两个
for
循环,三个字符就是三个for
循环,以此类推,当循环层数增多时无法使用暴力手段枚举所有可能的情况,此时只能使用回溯方法; - 输入
1 * #
按键等等异常情况:虽然题目中已经限制了digits
只能在2-9
中取,但是面试中需要注意这些特殊情况。
代码:
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:
//不同集合的组合 不需要startIndex 但需要index 指向待处理的数字
vector<string> res;
string path;
vector<string> letterCombinations(string digits) {
res.clear();
path.clear();
if (digits.size() == 0){
return res;
}
backtracking(digits,0);
return res;
}
void backtracking(string digits, int index){
if (index == digits.size()){
res.push_back(path);
return;
}
int digit = digits[index] - '0';//将index指向的数字转为int
string letters = letterMap[digit];//取数字对应的字符集
for (int i = 0;i < letters.size();i++){
path.push_back(letters[i]);
backtracking(digits,index+1);//递归,注意index + 1,下层要处理下一个数字
path.pop_back();
}
}
};
分割
这里首先说明一下string
、string&
、const string&
的区别:参考博客。
131. 分割回文串
给你一个字符串 s
,请你将 s
分割成一些子串,使每个子串都是 回文串
。返回 s
所有可能的分割方案。
回文串
是正着读和反着读都一样的字符串。
示例 1:
输入:s = “aab”
输出:[[“a”,“a”,“b”],[“aa”,“b”]]
示例 2:
输入:s = “a”
输出:[[“a”]]
思路:
代码:
class Solution {
private:
vector<vector<string>> result;
vector<string> path; // 放已经回文的子串 注意这里是vector<string>类型
void backtracking (const string& s, int startIndex) {
// 如果起始位置已经大于s的大小,说明已经找到了一组分割方案了
if (startIndex >= s.size()) {
result.push_back(path);
return;
}
for (int i = startIndex; i < s.size(); i++) {
if (isPalindrome(s, startIndex, i)) { // 是回文子串
// 获取[startIndex,i]在s中的子串
string str = s.substr(startIndex, i - startIndex + 1);
path.push_back(str);
} else { // 不是回文,跳过
continue;
}
backtracking(s, i + 1); // 寻找i+1为起始位置的子串
path.pop_back(); // 回溯过程,弹出本次已经填在的子串
}
}
bool isPalindrome(const string& s, int start, int end) {
for (int i = start, j = end; i < j; i++, j--) {//注意i和j的取值
if (s[i] != s[j]) {
return false;
}
}
return true;
}
public:
vector<vector<string>> partition(string s) {
result.clear();
path.clear();
backtracking(s, 0);
return result;
}
};
93. 复原 IP 地址
有效 IP
地址 正好由四个整数(每个整数位于 0
到 255
之间组成,且不能含有前导 0
),整数之间用 ‘.’ 分隔。
例如:“0.1.2.201” 和 “192.168.1.1” 是 有效 IP 地址,但是 “0.011.255.245”、“192.168.1.312” 和 “192.168@1.1” 是 无效 IP 地址。
给定一个只包含数字的字符串 s
,用以表示一个 IP 地址,返回所有可能的有效 IP 地址,这些地址可以通过在 s
中插入 ‘.’ 来形成。你 不能
重新排序或删除 s
中的任何数字。你可以按 任何
顺序返回答案。
示例 1:
输入:s = “25525511135”
输出:[“255.255.11.135”,“255.255.111.35”]
示例 2:
输入:s = “0000”
输出:[“0.0.0.0”]
示例 3:
输入:s = “101023”
输出:[“1.0.10.23”,“1.0.102.3”,“10.1.0.23”,“10.10.2.3”,“101.0.2.3”]
思路:
代码:
class Solution {
public:
vector<string> restoreIpAddresses(string s) {
res.clear();
if (s.size() > 12) return res;//字符串最长只能为12 可以看作是剪枝操作
backtracking(s,0,0);
return res;
}
private:
vector<string> res;
void backtracking(string& s, int startIndex, int pointNum){
//终止条件:pointNum为3 证明字符串分成了4段 验证一下第四段是否合法,如果合法就加入到结果集中
if (pointNum == 3){
if (isValid(s, startIndex, s.size()-1)){
res.push_back(s);
}
return;
}
for (int i = startIndex;i < s.size();i++){
if (isValid(s, startIndex, i)){
s.insert(s.begin() + i + 1, '.');// 在i的后面插入一个逗点
pointNum++;
backtracking(s, i + 2, pointNum);// 插入逗点之后下一个子串的起始位置为i+2
pointNum--;// 回溯
s.erase(s.begin() + i + 1);// 回溯删掉逗点
}
else{
break;//不合法,直接结束本层循环
}
}
}
bool isValid(const string& s, int start, int end){
//因为字符串的长度每次会增加一位,但是分割线要后移两位,这就存在越界的问题。正常的递归中,有for作判断,而递归终止的时候,就是靠这个if来做越界的判断
if (start > end){
return false;
}
//数字不含前导“0
if (s[start] == '0' && start != end){
return false;
}
//每段的数字应该在0-225之间
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){
return false;
}
}
return true;
}
};
子集
78. 子集
给你一个整数数组 nums
,数组中的元素 互不相同 。返回该数组所有可能的子集(幂集)。
解集 不能
包含重复的子集。你可以按 任意顺序
返回解集。
示例 1:
输入:nums = [1,2,3]
输出:[[],[1],[2],[1,2],[3],[1,3],[2,3],[1,2,3]]
示例 2:
输入:nums = [0]
输出:[[],[0]]
思路:
个人思路:子集中必定包含空集和自身,可以额外添加。参考组合问题中的77(在[1,n]
中找出有k
个数的组合),我们可以将子集问题看作找出1
个数的组合、找出2
个数的组合、…找出(k - 1)
个数的组合。
代码随想录思路:组合问题和分割问题都是收集树的叶子节点,而子集问题是找树的所有节点!抽象为树形结构为:
求取子集问题,不需要任何剪枝!因为子集就是要遍历整棵树。
代码1:
class Solution {
public:
vector<vector<int>> res;
vector<int> path;
//子集一定包含空集和自身
//结合组合题目:在[1,n]中找到K个数的组合,这里是将k从1变到nums.size() - 1
vector<vector<int>> subsets(vector<int>& nums) {
res.clear();
path.clear();
res.push_back({});
for (int k = 1;k < nums.size();k++){
backtracking(nums,k,0);//startIndex为0
}
res.push_back(nums);
return res;
}
void backtracking(vector<int>& nums, int k, int startIndex){
//终止条件
if (path.size() == k){
res.push_back(path);
return;
}
for (int i = startIndex;i < nums.size();i++){
path.push_back(nums[i]);
backtracking(nums,k,i+1);
path.pop_back();
}
}
};
代码2:
class Solution {
public:
vector<vector<int>> res;
vector<int> path;
//收集所有的节点
vector<vector<int>> subsets(vector<int>& nums) {
res.clear();
path.clear();
backtracking(nums,0);
return res;
}
void backtracking(vector<int>& nums, int startIndex){
res.push_back(path);//收集子集,要放在终止添加的上面,否则会漏掉自己
//终止条件:可以不需要加终止条件,因为startIndex >= nums.size(),本层for循环本来也结束了。
if (startIndex >= nums.size()){
return;
}
for (int i = startIndex;i < nums.size();i++){
path.push_back(nums[i]);
backtracking(nums,i+1);
path.pop_back();
}
}
};
90. 子集 II
给你一个整数数组 nums
,其中可能包含重复元素,请你返回该数组所有可能的子集(幂集)。
解集 不能
包含重复的子集。返回的解集中,子集可以按 任意顺序
排列。
示例 1:
输入:nums = [1,2,2]
输出:[[],[1],[1,2],[1,2,2],[2],[2,2]]
示例 2:
输入:nums = [0]
输出:[[],[0]]
思路:
参考组合类型中的40题。需要注意的是当有重复元素时,往往需要增加used
数组判断是否已经访问过某元素。
// used[i - 1] == true,说明同一树支candidates[i - 1]使用过
// used[i - 1] == false,说明同一树层candidates[i - 1]使用过
// 而我们要对同一树层使用过的元素进行跳过
if (i > 0 && nums[i] == nums[i-1] && used[i - 1] == false) continue;
本题也可以不用used
数组,参考代码随想录题解。但为统一风格以及便于理解,采用used
数组。
代码:
class Solution {
public:
//与78的不同之处在于数组中存在重复元素,最终集合不能包含重复的子集,所以需要使用used数组
vector<vector<int>> res;
vector<int> path;
vector<vector<int>> subsetsWithDup(vector<int>& nums) {
res.clear();
path.clear();
sort(nums.begin(),nums.end());
vector<bool> used(nums.size(),false);
backtracking(nums,used,0);
return res;
}
void backtracking(vector<int>& nums, vector<bool>& used, int startIndex){
//终止条件:startIndex >= nums.size()
res.push_back(path);
if (startIndex >= nums.size()){
return;
}
for (int i = startIndex;i < nums.size();i++){
if (i > 0 && nums[i] == nums[i-1] && used[i - 1] == false) continue;
if (used[i] == false){
used[i] = true;
path.push_back(nums[i]);
backtracking(nums,used,i+1);
used[i] = false;
path.pop_back();
}
}
}
};
排列
46. 全排列
给定一个不含重复数字的数组 nums
,返回其 所有可能的全排列
。你可以 按任意顺序
返回答案。
示例 1:
输入:nums = [1,2,3]
输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]
示例 2:
输入:nums = [0,1]
输出:[[0,1],[1,0]]
示例 3:
输入:nums = [1]
输出:[[1]]
代码:
class Solution {
public:
vector<vector<int>> res;
vector<int> path;
vector<vector<int>> permute(vector<int>& nums) {
int n = nums.size();
res.clear();
path.clear();
vector<bool> used(n,false);//用来统计是否遍历过某节点
backtracking(nums,used);
return res;
}
void backtracking(vector<int>& nums, vector<bool>& used){
int n = nums.size();
//终止条件
if (path.size() == n){
res.push_back(path);//收集结果
return;
}
for (int i = 0;i < n;i++){
//处理节点
if (used[i] == true) continue;//path中已经收录了该元素 直接跳过
used[i] = true;//对于没有收录的元素,当遍历到该元素时赋值为true
path.push_back(nums[i]);
//递归函数
backtracking(nums,used);
//回溯操作
path.pop_back();
used[i] = false;//撤回赋值操作
}
}
};
47. 全排列 II
给定一个可包含重复数字的序列 nums
,按任意顺序 返回所有不重复的全排列
。
示例 1:
输入:nums = [1,1,2]
输出:
[[1,1,2],
[1,2,1],
[2,1,1]]
示例 2:
输入:nums = [1,2,3]
输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]
思路:
- 对
res
进行去重:vector
去重三部曲,unique()
函数将相邻且重复的元素放到vector
的尾部,然后返回指向第一个重复元素的迭代器,再用erase
函数擦除从这个元素到最后元素的所有的元素。所以可以先进行排序,这样重复元素就会堆在一起,然后调用unique()
函数,再调用erase
函数删除重复。也可以使用set
进行去重。
sort(res.begin(),res.end());//默认根据第一列排序
res.erase(unique(res.begin(),res.end()),res.end());
class Solution {
public:
vector<vector<int>> res;
set<vector<int>> ans;
vector<int> path;
vector<vector<int>> permuteUnique(vector<int>& nums) {
......
for (auto &a : ans){
res.push_back(a);
}
return res;
}
void backtracking(vector<int>& nums, vector<bool>& used){
int n = nums.size();
//终止条件
if (path.size() == n){
ans.insert(path);//收集结果
return;
}
......
}
};
- 回溯剪枝去重
假设当前到达了元素b
,那么前一个元素a
一定是已经处理过了的,它的used[a] == true
理应成立。但如果used[a]==false
说明什么??说明nums[a]
已经被从当前组合集合中撤销掉了!!
既然现在nums[a]
被撤销,而nums[b]
被选择,也就是说:现在nums[b]
被顶替在了之前nums[a]
的位置上。如果nums[a] == nums[b]
,那么当前组合结果和之前不撤销nums[a]
的那个组合有什么区别?是没有区别的,这样就导致两个组合是重复的!这也就是我们需要剪枝去重的情况。
代码:
class Solution {
public:
vector<vector<int>> res;
vector<int> path;
vector<vector<int>> permuteUnique(vector<int>& nums) {
res.clear();
path.clear();
sort(nums.begin(),nums.end());//需要注意对nums排序
vector<bool> used(nums.size(),false);
backtracking(nums,used);
return res;
}
void backtracking(vector<int>& nums, vector<bool>& used){
if (path.size() == nums.size()){
res.push_back(path);
return;
}
for (int i = 0;i < nums.size();i++){
if (i > 0 && nums[i] == nums[i-1] && used[i-1] == false){//去重
continue;
}
if (used[i] == false){
used[i] = true;
path.push_back(nums[i]);
backtracking(nums,used);
path.pop_back();
used[i] = false;
}
}
}
};
797. 所有可能的路径
给你一个有 n
个节点的 有向无环图(DAG),请你找出所有从节点 0
到节点 n-1
的路径并输出(不要求按特定顺序)
graph[i]
是一个从节点 i
可以访问的所有节点的列表(即从节点 i
到节点 graph[i][j]
存在一条有向边)。
示例 1:
输入:graph = [[1,2],[3],[3],[]]
输出:[[0,1,3],[0,2,3]]
解释:有两条路径 0 -> 1 -> 3 和 0 -> 2 -> 3
示例2:
输入:graph = [[4,3,1],[3,2,4],[3],[4],[]]
输出:[[0,4],[0,3,4],[0,1,3,4],[0,1,2,3,4],[0,1,4]]
思路:
一、确认递归函数,参数
DFS函数一定要存一个图 ,用来遍历的,还要存一个当前正在遍历的节点,定义为x
;单一路径和路径集合可以放在全局变量,代码为:
vector<vector<int> > result;//收集符合路径的条件
vector<int> path;//节点0到终点的路径
//x:当前遍历的点
//graph:存当前的图
void dfs(vector<vector<int> >& graph, int x)
二、确认终止条件
当目前遍历的节点是最后一个节点时,我们就找到了一条从出发点到终止点的路径。
当前遍历的节点我们定义为x
,最后一个节点即为graph.size() - 1
,所以当x = graph.size() - 1
的时候就找到一条有效路径,代码为:
if (x == graph.size() - 1){//找到符合条件的一条路径
result.push_back(path);//收集有效路径
return;
}
三、处理目前搜索节点出发的路径
接下来是走 当前遍历节点x
的下一个节点,首先是找x
节点链接的节点:
for (int i = 0;i < graph.size();i++)
然后是将 选中的x
节点链接的节点加入到 单一路径 中:
path.push_back(graph[x][i]);
当前遍历的节点就是graph[x][i]
了,需要进入下一层递归:
dfs(graph,graph[x][i]);
最后就是回溯的过程,撤销本次添加节点的操作。该过程的整体代码为:
for (int i = 0; i < graph[x].size(); i++) { // 遍历节点n链接的所有节点
path.push_back(graph[x][i]); // 遍历到的节点加入到路径中来
dfs(graph, graph[x][i]); // 进入下一层递归
path.pop_back(); // 回溯,撤销本节点
}
代码:
class Solution {
private:
vector<vector<int>> result; // 收集符合条件的路径
vector<int> path; // 0节点到终点的路径
// x:目前遍历的节点
// graph:存当前的图
void dfs (vector<vector<int>>& graph, int x) {
// 要求从节点 0 到节点 n-1 的路径并输出,所以是 graph.size() - 1
if (x == graph.size() - 1) { // 找到符合条件的一条路径
result.push_back(path);
return;
}
for (int i = 0; i < graph[x].size(); i++) { // 遍历节点n链接的所有节点
path.push_back(graph[x][i]); // 遍历到的节点加入到路径中来
dfs(graph, graph[x][i]); // 进入下一层递归
path.pop_back(); // 回溯,撤销本节点
}
}
public:
vector<vector<int>> allPathsSourceTarget(vector<vector<int>>& graph) {
path.push_back(0); // 无论什么路径已经是从0节点出发
dfs(graph, 0); // 开始遍历
return result;
}
};
棋盘
51. N 皇后
按照国际象棋的规则,皇后可以攻击与之处在同一行或同一列或同一斜线上的棋子。
n
皇后问题 研究的是如何将 n
个皇后放置在 n×n
的棋盘上,并且使皇后彼此之间不能相互攻击。
给你一个整数 n
,返回所有不同的 n
皇后问题 的解决方案。
每一种解法包含一个不同的 n
皇后问题 的棋子放置方案,该方案中 ‘Q’ 和 ‘.’ 分别代表了皇后和空位。
示例 1:
输入:n = 4
输出:[[“.Q…”,“…Q”,“Q…”,“…Q.”],[“…Q.”,“Q…”,“…Q”,“.Q…”]]
解释:如下图所示,4 皇后问题存在两个不同的解法。
示例 2:
输入:n = 1
输出:[[“Q”]]
思路:
需要注意判断是否可以放置皇后时无需判断是否在同行,因为书写代码的逻辑就是每行只放一个皇后。
图解(以其中一个树枝为例):
代码:
class Solution {
public:
//N皇后问题:如何将 n 个皇后放置在 n×n 的棋盘上,并且使皇后彼此之间不能相互攻击。皇后可以攻击与之处在同一行或同一列或同一斜线上的棋子。
//不能在同一行,不能在同一列,不能在对角线
vector<vector<string>> res;
vector<vector<string>> solveNQueens(int n) {
res.clear();
std::vector<std::string> chessboard(n, std::string(n,'.'));
backtracking(n,0,chessboard);//第二个参数为行数
return res;
}
void backtracking(int n, int row, vector<string>& chessboard){
//终止条件
if (row == n){
res.push_back(chessboard);
return;
}
for (int col = 0;col < n;col++){
if (isValid(row,col,n,chessboard)){
chessboard[row][col] = 'Q';
backtracking(n,row+1,chessboard);
chessboard[row][col] = '.';
}
}
}
bool isValid(int row, int col, int n, vector<string>& chessboard){
//检查列
for (int i = 0;i < row;i++){
if (chessboard[i][col] == 'Q'){
return false;
}
}
//检查45度角是否有皇后
for (int i = row - 1, j = col - 1;i >= 0 && j >= 0;i--,j--){
if (chessboard[i][j] == 'Q'){
return false;
}
}
//检查135度角是否有皇后
for (int i = row - 1, j = col + 1;i >= 0 && j >= 0;i--,j++){
if (chessboard[i][j] == 'Q'){
return false;
}
}
return true;
}
};
37. 解数独
编写一个程序,通过填充空格来解决数独问题。
数独的解法需 遵循如下规则:
数字 1-9 在每一行只能出现一次。
数字 1-9 在每一列只能出现一次。
数字 1-9 在每一个以粗实线分隔的 3x3 宫内只能出现一次。(请参考示例图)
数独部分空格内已填入了数字,空白格用 ‘.’ 表示。
示例 1:
输入:board = [[“5”,“3”,“.”,“.”,“7”,“.”,“.”,“.”,“.”],[“6”,“.”,“.”,“1”,“9”,“5”,“.”,“.”,“.”],[“.”,“9”,“8”,“.”,“.”,“.”,“.”,“6”,“.”],[“8”,“.”,“.”,“.”,“6”,“.”,“.”,“.”,“3”],[“4”,“.”,“.”,“8”,“.”,“3”,“.”,“.”,“1”],[“7”,“.”,“.”,“.”,“2”,“.”,“.”,“.”,“6”],[“.”,“6”,“.”,“.”,“.”,“.”,“2”,“8”,“.”],[“.”,“.”,“.”,“4”,“1”,“9”,“.”,“.”,“5”],[“.”,“.”,“.”,“.”,“8”,“.”,“.”,“7”,“9”]]
输出:[[“5”,“3”,“4”,“6”,“7”,“8”,“9”,“1”,“2”],[“6”,“7”,“2”,“1”,“9”,“5”,“3”,“4”,“8”],[“1”,“9”,“8”,“3”,“4”,“2”,“5”,“6”,“7”],[“8”,“5”,“9”,“7”,“6”,“1”,“4”,“2”,“3”],[“4”,“2”,“6”,“8”,“5”,“3”,“7”,“9”,“1”],[“7”,“1”,“3”,“9”,“2”,“4”,“8”,“5”,“6”],[“9”,“6”,“1”,“5”,“3”,“7”,“2”,“8”,“4”],[“2”,“8”,“7”,“4”,“1”,“9”,“6”,“3”,“5”],[“3”,“4”,“5”,“2”,“8”,“6”,“1”,“7”,“9”]]
解释:输入的数独如上图所示,唯一有效的解决方案如下所示:
思路:
递归单层搜索逻辑示意图:
与其他回溯问题较为不同的点在于,这里用的是“二维递归”。
代码:
class Solution {
public:
//二维递归
void solveSudoku(vector<vector<char>>& board) {
backtracking(board);
}
//递归函数的返回值是bool类型:解数独找到一个符合的条件(就在数的叶子节点上)立刻就返回。
//相当于找从根节点到叶子节点一条唯一路径,所以需要使用bool返回值。
private:
bool backtracking(vector<vector<char>>& board){
//递归不用终止条件,解数独是要遍历整个树形结构寻找可能的叶子节点就立刻返回。
//没有终止条件会不会死循环:不会。递归的下一层的棋盘一定比上一层的棋盘多一个数,等数填满了棋盘自然就终止。(?)
for (int i = 0;i < board.size();i++){ //遍历行
for (int j = 0;j < board[0].size();j++){ //遍历列
if (board[i][j] != '.') continue;
for (char k = '1';k <= '9';k++){ //(i,j)这个位置放k是否合适
if (isValid(i,j,k,board)){
board[i][j] = k; //放置k
if (backtracking(board)) return true; //如果找到合适的一组 立刻返回
board[i][j] = '.'; //回溯,撤销k
}
}
return false; //9个数都试完了 都不行 那么就返回false
}
}
return true; //遍历完没有返回false,说明找到合适棋盘位置了
}
//判断棋盘是否合法
bool isValid(int row, int col, char val, vector<vector<char>>& board){
//判断行是否重复
for (int i = 0;i < 9;i++){
if (board[row][i] == val){
return false;
}
}
//判断列是否重复
for (int j = 0;j < 9;j++){
if (board[j][col] == val){
return false;
}
}
//判断九格内是否有重复
int startRow = (row / 3) * 3;
int startCol = (col / 3) * 3;
for (int i = startRow;i < startRow + 3;i++){
for (int j = startCol;j < startCol + 3;j++){
if (board[i][j] == val){
return false;
}
}
}
return true;
}
};
其他
491.递增子序列
给你一个整数数组 nums
,找出并返回所有该数组中不同的递增子序列,递增子序列中 至少有两个元素
。你可以按 任意顺序
返回答案。
数组中可能含有重复元素
,如出现两个整数相等,也可以视作递增序列的一种特殊情况。
示例 1:
输入:nums = [4,6,7,7]
输出:[[4,6],[4,6,7],[4,6,7,7],[4,7],[4,7,7],[6,7],[6,7,7],[7,7]]
示例 2:
输入:nums = [4,4,3,2,1]
输出:[[4,4]]
思路: 代码随想录
与90.子集Ⅱ
的思路有一定类似之处,但需要注意:
子集中去重的方法是先sort
后采用if
条件判断,即下面代码:
//主函数
sort(nums.begin(),nums.end());
//回溯函数
if (i > 0 && nums[i-1] == nums[i] && used[i-1] == false) continue;
本题中不可以使用这种方法去重,因为如果先sort
后得到的子集一定都是有序的。
所以我们使用set
去重:
unordered_set<int> uset;
图解为:
其中需要关注的点为,回溯撤回为何不对set
进行撤回操作,摘自评论。
一直对于作者所说的“本层”不太理解。今天好像懂了一点。因为
uset
是每一次调用回溯函数就生成一次,所以每一次的uset
都是全新的。又因为每一次调用回溯函数就是对某一层进行选择,所有操作都是关乎这一层的。所以即为作者所说的unordered_set<int> uset;
是记录本层元素是否重复使用,新的一层uset
都会重新定义(清空),所以要知道uset
只负责本层!
代码:
class Solution {
public:
vector<vector<int>> res;
vector<int> path;
vector<vector<int>> findSubsequences(vector<int>& nums) {
res.clear();
path.clear();
backtracking(nums,0);
return res;
}
void backtracking(vector<int>& nums, int startIndex){
//res.push_back(path);在求子集中 这一步的目的是将本身放进去
//终止条件:startIndex >= nums.size()
if (path.size() >= 2){
res.push_back(path);
//注意这里不加return 因为要取树上所有节点
}
//单层搜索的逻辑:同一父节点下的同层上使用过的元素就不能再使用了
//uset是记录本层元素是否重复使用,新的一层uset都会重新定义(清空)
unordered_set<int> uset; //使用set对本层元素进行去重
for (int i = startIndex;i < nums.size();i++){
//path.back()是返回path的最后一个元素
//两个判断条件:
//1.path不为空且当前数组元素比path最后一个元素小,跳过本次循环
//2.当前数组元素已经在本层使用过了,跳过本次循环
if ((!path.empty() && nums[i] < path.back()) || uset.find(nums[i]) != uset.end()){
continue;
}
uset.insert(nums[i]);//记录当前数组元素在本层已经使用过了,本层后面若出现相同的元素,不能再使用了
path.push_back(nums[i]);
backtracking(nums,i+1);
path.pop_back();
}
}
};
332.重新安排行程
给你一份航线列表 tickets
,其中 tickets[i] = [fromi, toi]
表示飞机出发和降落的机场地点。请你对该行程进行重新规划排序。
所有这些机票都属于一个从 JFK(肯尼迪国际机场)
出发的先生,所以该行程必须从 JFK
开始。如果存在多种有效的行程,请你按字典排序返回最小的行程组合。
例如,行程 ["JFK", "LGA"]
与 ["JFK", "LGB"]
相比就更小,排序更靠前。
假定所有机票至少存在一种合理的行程。且所有的机票 必须都用一次
且 只能用一次
。
示例 1:
输入:tickets = [[“MUC”,“LHR”],[“JFK”,“MUC”],[“SFO”,“SJC”],[“LHR”,“SFO”]]
输出:[“JFK”,“MUC”,“LHR”,“SFO”,“SJC”]
示例 2:
输入:tickets = [[“JFK”,“SFO”],[“JFK”,“ATL”],[“SFO”,“ATL”],[“ATL”,“JFK”],[“ATL”,“SFO”]]
输出:[“JFK”,“ATL”,“JFK”,“SFO”,“ATL”,“SFO”]
解释:另一种有效的行程是 [“JFK”,“SFO”,“ATL”,“JFK”,“ATL”,“SFO”] ,但是它字典排序更大更靠后。
思路:
本题的难点在于:
- 要求字母排序靠前的排在前面,如何记录映射关系?
- 这是一个图不是一棵树,深搜/回溯的终止条件是什么?
- 回溯的过程中,如何遍历一个城市所对应的所有城市?
解决方案:
一、 映射关系:set
以及map
存放映射关系可定义为:
unordered_map<string, multiset<string>> targets;
:unordered_map<出发城市, 到达城市的集合> targets;
unordered_map<string, map<string,int>> targets;
:unordered_map<出发城市, map<到达城市,航班次数>> targets;
选择第二种,因为回溯的过程中需要不断地增删multiset
里的元素,但是multiset
一旦删除元素,迭代器就失效了。
二、深搜/回溯终止条件:
根据题目中的示例,
输入:tickets = [[“MUC”,“LHR”],[“JFK”,“MUC”],[“SFO”,“SJC”],[“LHR”,“SFO”]]
输出:[“JFK”,“MUC”,“LHR”,“SFO”,“SJC”]
输入:tickets = [[“JFK”,“SFO”],[“JFK”,“ATL”],[“SFO”,“ATL”],[“ATL”,“JFK”],[“ATL”,“SFO”]]
输出:[“JFK”,“ATL”,“JFK”,“SFO”,“ATL”,“SFO”]
解释:另一种有效的行程是 [“JFK”,“SFO”,“ATL”,“JFK”,“ATL”,“SFO”] ,但是它字典排序更大更靠后。
示例1机票数量为4,行程中的机场个数(有可能有重复)为5;示例2机票数量为5,行程中的机场个数(有可能有重复)为6;因此若行程中的机场个数 = 机票数量 + 1,达到了终止条件。
三、回溯的过程中,如何遍历一个城市对应的所有城市:
为什么一定要增删元素,原因是:即使是出发机场和到达机场,也是会重复的(如示例2),搜索的过程中没有及时删除元素就会陷入死循环。
遍历过程为:
for (pair<const string, int>& target : targets[result[result.size() - 1]]) {
if (target.second > 0 ) {
result.push_back(target.first);
target.second--;
if (backtracking(ticketNum, index + 1, result)) return true;
result.pop_back();
target.second++;
}
}
图解:
解释:
机场路线图:
unordered_map<string, map<string, int>> targets;
上述代码对应图为:
代码大致流程:
…
代码:
class Solution {
private:
// unordered_map<出发城市, map<到达城市, 航班次数>> targets
unordered_map<string, map<string, int>> targets;
bool backtracking(int ticketNum, vector<string>& result) {
if (result.size() == ticketNum + 1) {
return true;
}
for (pair<const string, int>& target : targets[result[result.size() - 1]]) {
if (target.second > 0 ) { // 使用int字段来记录到达城市是否使用过了
result.push_back(target.first);
target.second--;
if (backtracking(ticketNum, result)) return true;
result.pop_back();
target.second++;
}
}
return false;
}
public:
vector<string> findItinerary(vector<vector<string>>& tickets) {
vector<string> result;
for (const vector<string>& vec : tickets) {
targets[vec[0]][vec[1]]++; // 记录映射关系
}
result.push_back("JFK");
backtracking(tickets.size(), result);
return result;
}
};
79. 单词搜索
给定一个 m x n
二维字符网格 board
和一个字符串单词 word
。如果 word
存在于网格中,返回 true
;否则,返回 false
。
单词必须按照字母顺序,通过相邻的单元格内的字母构成,其中“相邻”单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母不允许被重复使用。
示例 1:
输入:board = [[“A”,“B”,“C”,“E”],[“S”,“F”,“C”,“S”],[“A”,“D”,“E”,“E”]], word = “ABCCED”
输出:true
示例 2:
输入:board = [[“A”,“B”,“C”,“E”],[“S”,“F”,“C”,“S”],[“A”,“D”,“E”,“E”]], word = “SEE”
输出:true
示例 3:
输入:board = [[“A”,“B”,“C”,“E”],[“S”,“F”,“C”,“S”],[“A”,“D”,“E”,“E”]], word = “ABCB”
输出:false
思路:
这道题的感觉和岛屿问题非常像,但是比岛屿问题多了回溯的部分。因此代码主要框架仍然使用nettee大佬的框架。
设函数 backtracking(r,c,index)
表示判断以网格的 (r,c)
位置出发,能否搜索到单词 word[index..]
,其中 word[index..]
表示字符串 word
从第 index
个字符开始的后缀子串。如果能搜索到,则返回 true
,反之返回 false
。函数 backtracking(r,c,index)
的执行步骤如下:
- 如果①
board[r][c] != word[index]
,或②[r,c]
超出边界,或③已经访问过该位置,则当前字符不匹配,直接返回false
。 - 如果当前已经访问到字符串的末尾,且对应字符依然匹配,此时直接返回
true
。 - 否则,遍历当前位置的所有相邻位置。如果从某个相邻位置出发,能够搜索到子串
word[index+1..]
,则返回true
,否则返回false
。
这样,我们对每一个位置 (r,c)
都调用函数 backtracking(r,c,index)
进行检查:只要有一处返回 true
,就说明网格中能够找到相应的单词,否则说明不能找到。
为了防止重复遍历相同的位置,需要额外维护一个与 board
等大的 used
数组,用于标识每个位置是否被访问过。每次遍历相邻位置时,需要跳过已经被访问的位置。
代码:
class Solution {
public:
bool backtracking(vector<vector<char>>& board, const string& word, vector<vector<int>>& used, int r, int c, int index){
//剪枝
if (r < 0 || r >= board.size() || c < 0 || c >= board[0].size() || used[r][c] || board[r][c] != word[index]) return false;
if (index == word.size() - 1 && word[index] == board[r][c]) return true;//注意==
used[r][c] = 1;
if (backtracking(board, word, used, r + 1, c, index + 1) ||
backtracking(board, word, used, r - 1, c, index + 1) ||
backtracking(board, word, used, r, c + 1, index + 1) ||
backtracking(board, word, used, r, c - 1, index + 1)) return true;
used[r][c] = 0;//回溯
return false;
}
bool exist(vector<vector<char>>& board, string word) {
vector<vector<int>> used(board.size(),vector<int>(board[0].size(),0));
for (int r = 0;r < board.size();r++){
for (int c = 0;c < board[0].size();c++){
if (board[r][c] != word[0]) continue;
if (backtracking(board, word, used, r, c, 0)) return true;
}
}
return false;
}
};
这篇文章太长了,写到这里markdown编辑器已经非常卡顿了,更多搜索相关的题目见Leetcode搜索合集(下)~