【刷题日记】回溯算法(深度优先搜索)经典题目

深度优先搜索与回溯算法经典实例解析
本文详细介绍了回溯算法在深度优先搜索中的应用,通过多个编程题目,如扑克问题、图像渲染、岛屿周长、员工重要性、被包围的区域、岛屿数量、岛屿最大面积、电话号码的字母组合、二进制手表、组合总和、活字印刷和N皇后问题,阐述了深度优先搜索的逻辑和实现方法。文章旨在帮助读者更好地理解和掌握回溯算法的精髓。

😀大家好,我是白晨,一个不是很能熬夜😫,但是也想日更的人✈。如果喜欢这篇文章,点个赞👍,关注一下👀白晨吧!你的支持就是我最大的动力!💪💪💪

在这里插入图片描述

🎆前言


回溯算法可以称得上“万能算法”,因为它非常符合我们人的逻辑——试错、返回、试错、返回… 直到最后找到符合要求结果,当然,它也是一种比较难的算法(难度很大程度上取决于你的实现思想),要想完全掌握这个算法是很难的,需要大量题目练习以熟悉大多数情况。

这一次,白晨挑选了回溯算法的深度优先搜索分类中比较具有代表性的题目,难度由易到难,范围从矩阵到排列组合等。希望大家通过下面题目的练习,可以更快掌握回溯算法😋。


🎇回溯算法经典题目


🎠1.深度优先搜索


深度优先搜索(Depth First Search)也就是一条道走到黑,发现没有路了,回到上一个路口再选一个方向走,直到找到一条可行的路。具体逻辑这里不详细介绍,本文章主要为题目思路+代码解析。算法思想会再出一篇文章,详细解析。

我们可以抽象出一个通用的深度优先搜索的逻辑:

Dfs(当前这一步的处理逻辑)
{
	//1. 判断边界,是否已经一条道走到黑了:向上回退
	//2. 尝试当下的每一种可能
	//3. 确定一种可能之后,继续下一步 Dfs(下一步)
}

没有基础的同学可能有点难理解这种抽象逻辑,我们可以先看下一道例题,我会详细解析这种逻辑。

🍷1.1 扑克问题


假如有编号为1 ~ 3的3张扑克牌和编号为1 ~ 3的3个盒子,现在需要将3张牌分别放到3个盒子中去,且每个盒子只能放一张牌,一共有多少种不同的放法 ?

这是一道高中最简单的排列组合题目,答案也很简单,为 A 3 3 A^3_3 A33 ,也就是6种放法,但是我们可以用计算机的逻辑想一下应该如何解决这样的问题。

思路详解:

  • 当走到一个盒子面前的时候,到底要放那一张牌呢?在这里应该把所有的牌都尝试一遍。假设这里约定一个顺序,按牌面值从小到大依次尝试。在这样的假定下,当走到第一个盒子的时候,放入1号牌。
  • 放好之后,继续向后走,走到第二个盒子面前,此时还剩2张牌,牌面值最小的为2号牌,按照约定的规则,把2号牌放入第二个盒子。
  • 此时,来到第三个盒子面前,只剩一张牌,放入第三个盒子。此时手中的牌已经用完。
  • 继续向后走,走到了盒子的尽头,后面再也没有盒子,并且也没有可用的牌了,此时,一种放法已经完成了,
  • 但是这只是一种放法,这条路已经走到了尽头,还需要折返,重新回到上一个盒子。
  • 这里回到第三个盒子,把第三个盒子中的牌取出来,再去尝试能否再放其它的牌,这时候手里仍然只有一张3号牌,没有别的选择了,所以还需要继续向后回退,回到2号盒子面前。
  • 收回2号盒子中的2号牌,现在手里有两张牌,2,3,按照约定,再把3号牌放入2号盒子,放好之后,继续向后走,来到3号盒子。
  • 此事手里只有一张2号牌,把它放入3号盒子,继续向后走。
  • 此时这条路又一次走到了尽头,一个新的放法又产生了,继续向上折返,尝试其它可能,按照上述步骤,依次会产生所有结果。

代码如何实现呢?

从上述思路中,我们可以发现,我们需要判断一张牌在不在手上,所以我们需要创建一个bool类型的数组来判断这张牌到底用没用?

// index表示现在走到哪一个盒子面前
// 题目规定手牌总数和盒子数相同,所以我们都用n来表示
void Dfs(int index, int n, vector<int>& book)
{
	for(int i = 1; i <= n; i++)
	{
		if(book[i] == 0) // 第i号牌仍在手上
		{
			book[i] = 1; // 现在第i号牌已经被用了
		}
	}
}

接下来,我们还需要向下一个盒子走,也就是一个盒子放入的手牌确定之后我们要递归调用Dfs。

void Dfs(int index, int n, vector<int>& book)
{
    for (int i = 1; i <= n; i++)
    {
        if (book[i] == 0) // 第i号牌仍在手上
        {
            book[i] = 1; // 现在第i号牌已经被用了
            // 处理下一个盒子
            Dfs(index + 1, n, boxs, book);
            // 从下一个盒子回退到当前盒子,取出当前盒子的牌,
            // 尝试放入其它牌。
            book[i] = 0;
        }
    }
}

现在要考虑走到尽头的问题,如果走到 n+1 个盒子了,我们需要直接返回(这里将每一张手牌遍历一遍后也可以返回,但是时间成本太高,我们可以在前面手动返回)。

void Dfs(int index, int n, vector<int>& book)
{
    if(index == n + 1)
    {
        return;
    }
    
    for (int i = 1; i <= n; i++)
    {
        if (book[i] == 0) // 第i号牌仍在手上
        {
            book[i] = 1; // 现在第i号牌已经被用了
            // 处理下一个盒子
            Dfs(index + 1, n, boxs, book);
            // 从下一个盒子回退到当前盒子,取出当前盒子的牌,
            // 尝试放入其它牌。
            book[i] = 0;
        }
    }
}

从上面的代码可以看出,深度优先搜索的关键是解决**“当下该如何做”下一步的做法和当下的做法是一样的"当下如何做"一般是尝试每一种可能,用for循环遍历,对于每一种可能确定之后,继续走下一步,当前的剩余可能等到从下一步回退之后再处理。**


🍸1.2 图像渲染


在这里插入图片描述

原题链接:图像渲染

在这里插入图片描述

static int des[4][2] = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};// 方向数组,用来调整方向
class Solution {
public:
    void Dfs(vector<vector<int>>& image, int x, int y, int row, int col,
    int oldColor, int newColor,vector<vector<bool>>& book)
    {
        // 修改当前点
        image[x][y] = newColor;
        book[x][y] = true;
        // 遍历四个方向
        for(int i = 0; i < 4; ++i)
        {
            // 得到新坐标
            int newx = x + des[i][0];
            int newy = y + des[i][1];
            // 判断新坐标是否越界
            if(newx >= row || newx < 0 || newy >= col || newy < 0)
                continue;
            // 如果该像素点为旧颜色,并且未被修改过,我们要将其修改并以此点开始继续向四个方向遍历
            if(image[newx][newy] == oldColor && book[newx][newy] == false)
            {             
                Dfs(image, newx, newy, row, col, oldColor, newColor, book);
            }
            
        }
    }
    vector<vector<int>> floodFill(vector<vector<int>>& image, int sr, int sc, int newColor) {
        // 图像的行数
        int row = image.size();
        // 图像的列数
        int col = image[0].size();
        // 要被修改的颜色
        int oldColor = image[sr][sc];
        // 判断像素点是否被修改过
        vector<vector<bool>> book(row, vector<bool>(col, false));
        Dfs(image, sr, sc, row, col, oldColor,newColor, book);

        return image;
    }
};

注意:

  1. 这里没有返回上一层的代码,原因是:能进入深度优先搜索的一定是不越界的坐标(也就是越界的坐标不会进入深度优先搜索),所以这里的返回上一层就是要遍历一个点的四个方向后自动返回。
  2. 这里可能有人会认为判断一个像素点有无被修改过没有用,因为如果被修改过,颜色就是新颜色,就不会进入深度搜索。但是这里其实忽略了一种情况,新颜色和旧颜色相同,比如,[[0,0,0],[0,1,1]] 1 1 1 ,这组数据输入时,由于起始位置(1,1)颜色为 1 ,并且新颜色也为 1,如果没有bool数组判断,而只判断是否为旧颜色,就会陷入死递归,栈溢出。

🍹1.3 岛屿的周长


在这里插入图片描述

原题链接:岛屿的周长

本题与上一题非常类似,我们依然采用深度优先搜索向四个方向搜索,但是本题有个难点,就是面积该怎么算?

这里我们讲两种思路算面积,大家选择自己听得懂的一种就可以:

  1. 减边法

在这里插入图片描述

用代码思维想一下,我们怎么用代码实现呢?

  • 首先,每遇到一块陆地,周长加4。

在这里插入图片描述

  • 上图是深度搜索时的示意图,我们搜索时,肯定会搜索到其余的陆地,我们只需要在每次遇到另一块陆地时将周长减1,也就是将自己的一条边减去,另一块陆地在搜索时也会遇到上一块陆地,再减1,减去另一块陆地的一边,重复上面的操作就可以得到整体的周长。
static int des[4][2] = { {1, 0}, {-1, 0}, {0, 1}, {0, -1} };
class Solution {
public:
    void Dfs(vector<vector<int>>& grid, int row, int col, int x, int y, int& c,
        vector<vector<bool>>& book)
    {
        // 每遇到一块土地,周长加4
        c += 4;
        book[x][y] = true;

        for (int i = 0; i < 4; ++i)
        {
            int newX = x + des[i][0];
            int newY = y + des[i][1];

            if (newX < 0 || newX >= row || newY < 0 || newY >= col)
                continue;
            // 遇到为1并且没有搜索过的点,进入搜索
            if (grid[newX][newY] == 1 && book[newX][newY] == false)
                Dfs(grid, row, col, newX, newY, c, book);
            // 遇到搜索过的点,周长减一
            if (grid[newX][newY] == 1 && book[newX][newY] == true)
                --c;
        }
    }
    int islandPerimeter(vector<vector<int>>& grid) {
        int row = grid.size();
        int col = grid[0].size();
        // 记录周长
        int c = 0;
        // 记录岛屿点有没有被深度优先搜索过
        vector<vector<bool>> book(row, vector<bool>(col, false));

        for (int i = 0; i < row; ++i)
        {
            for (int j = 0; j < col; ++j)
            {
                if (grid[i][j] == 1)
                {
                    // 遇到1就搜索,因为只有一块陆地,所以只用搜1次
                    Dfs(grid, row, col, i, j, c, book);
                    goto end;
                }
            }
        }
    end:
        return c;
    }
};
  1. 数边法

在这里插入图片描述

观察上图可知,只要土地块旁边是海域,周长就加1,直到把所有的土地块旁边的海域全部数完。

代码应该如何实现呢?

  • 四个方向寻找时,旁边是0,周长就加1。
  • 如果哪个方向上的坐标越界,周长也加1,我们可以可以如上图所示,在整个地图外围加一圈海域,这样遇到海域周长就加1,也就是越界周长加1。
static int des[4][2] = { {1, 0}, {-1, 0}, {0, 1}, {0, -1} };
class Solution {
public:
    void Dfs(vector<vector<int>>& grid, int row, int col, int x, int y, int& s,
        vector<vector<bool>>& book)
    {
        book[x][y] = true;

        for (int i = 0; i < 4; ++i)
        {
            int newX = x + des[i][0];
            int newY = y + des[i][1];
			// 遇到上述情况,周长加1
            if (newX < 0 || newX >= row || newY < 0 || newY >= col || grid[newX][newY] == 0)
            {
                ++s;
                continue;
            }

            if (grid[newX][newY] == 1 && book[newX][newY] == false)
                Dfs(grid, row, col, newX, newY, s, book);
        }
    }
    int islandPerimeter(vector<vector<int>>& grid) {
        int row = grid.size();
        int col = grid[0].size();
        int s = 0;
        vector<vector<bool>> book(row, vector<bool>(col, false));

        for (int i = 0; i < row; ++i)
        {
            for (int j = 0; j < col; ++j)
            {
                if (grid[i][j] == 1)
                {
                    Dfs(grid, row, col, i, j, s, book);
                    goto end;
                }
            }
        }
    end:
        return s;
    }
};

当然,我们可以换种方式记录这个土地块有没有被搜过,比如:将土地块的数字 1 改为 2,这样空间复杂度就由 O ( r o w ∗ c o l ) O(row * col) O(rowcol) 变为 O ( 1 ) O(1) O(1)

static int des[4][2] = { {1, 0}, {-1, 0}, {0, 1}, {0, -1} };
class Solution {
public:
    void Dfs(vector<vector<int>>& grid, int row, int col, int x, int y, int& s)
    {
        // 将土地记号改为2
        grid[x][y] = 2;

        for (int i = 0; i < 4; ++i)
        {
            int newX = x + des[i][0];
            int newY = y + des[i][1];

            if (newX < 0 || newX >= row || newY < 0 || newY >= col || grid[newX][newY] == 0)
            {
                ++s;
                continue;
            }

            if (grid[newX][newY] == 1)
                Dfs(grid, row, col, newX, newY, s);
        }
    }
    int islandPerimeter(vector<vector<int>>& grid) {
        int row = grid.size();
        int col = grid[0].size();
        int s = 0;

        for (int i = 0; i < row; ++i)
        {
            for (int j = 0; j < col; ++j)
            {
                if (grid[i][j] == 1)
                {
                    Dfs(grid, row, col, i, j, s);
                    goto end;
                }
            }
        }
    end:
        return s;
    }
};

🍺1.4 员工的重要性


在这里插入图片描述

原题链接:员工的重要性

从起始位置累加,最后返回总忠诚度即可。

/*
// Definition for Employee.
class Employee {
public:
    int id;
    int importance;
    vector<int> subordinates;
};
*/

class Solution {
public:
    int DFS(unordered_map<int, Employee*>& info, int id)
    {
        int curImpo = info[id]->importance;
        for (const auto& sid : info[id]->subordinates)
        {
            curImpo += DFS(info, sid);
        } 
        return curImpo;
    } 
    int getImportance(vector<Employee*> employees, int id) {
        if (employees.empty())
            return 0;
        unordered_map<int, Employee*> info;
        for (const auto& e : employees)
        {
            info[e->id] = e;
        } 
        return DFS(info, id);
    }
};

🍻1.5 被包围的区域


在这里插入图片描述

原题链接:被围绕的区域

思路详解:

  • 本题的意思被包围的区间不会存在于边界上,所以边界上的 O 以及与 O 联通的都不算做包围,只要把边界上的 O 以及与之联通的 O 进行特殊处理,剩下的 O 替换成 X 即可。故问题转化为,如何寻找和边界联通的 O
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iWKfpTVf-1646285708815)(C:\Users\李若尘\Desktop\素材\回溯算法\被包围的区域1.png)]
  • 从每一个边缘的 O 开始,只要和边缘的 O 联通,则它就没有被包围。也就是要从边缘的 O 点开始四面搜索,将所有与边缘 O 点相连的 O 全部找到。
    1. 首先寻找边上的每一个 O ,如果没有,表示所有的 O 都被包围
    2. 对于边上的每一个 O 进行深度优先搜索,进行扩散,先把边上的每一个 O 用特殊符号标记,比如 *,#
    3. 把和它相邻的 O 都替换为特殊符号,每一个新的位置都做相同的深度优先搜索。
    4. 所有扩散结束之后,把特殊符号的位置(和边界连通)还原为 O ,原来为 O 的位置(和边界不连通)替换为 X 即可。
static int des[4][2] = { {1, 0}, {-1, 0}, {0, 1}, {0, -1} };
class Solution {
public:
    void Dfs(vector<vector<char>>& board, int row, int col, int x, int y)
    {
        // 修改标记
        board[x][y] = '*';
		// 向四个方向找
        for (int i = 0; i < 4; ++i)
        {
            int newX = x + des[i][0];
            int newY = y + des[i][1];
			//  判断新坐标是否越界
            if (newX < 0 || newX >= row || newY < 0 || newY >= col)
                continue;
            // DFS搜索
            if (board[newX][newY] == 'O')
                Dfs(board, row, col, newX, newY);
        }
    }
    void solve(vector<vector<char>>& board) {
        int row = board.size();
        int col = board[0].size();
		// 搜索首尾两列有无O
        for (int i = 0; i < row; ++i)
        {
            if (board[i][0] == 'O')
                Dfs(board, row, col, i, 0);
            if (board[i][col - 1] == 'O')
                Dfs(board, row, col, i, col - 1);
        }
		// 搜索首尾两行有无O
        for (int j = 1; j < col - 1; ++j)
        {
            if (board[0][j] == 'O')
                Dfs(board, row, col, 0, j);
            if (board[row - 1][j] == 'O')
                Dfs(board, row, col, row - 1, j);
        }
		// 替换标记,*->O,O->X
        for (int i = 0; i < row; ++i)
        {
            for (int j = 0; j < col; ++j)
            {
                if (board[i][j] == '*')
                    board[i][j] = 'O';
                // 这些 O 都是没有与边缘有连接的
                else if (board[i][j] == 'O')
                    board[i][j] = 'X';
            }
        }

    }
};

🥂1.6 岛屿数量


在这里插入图片描述

原题链接:岛屿数量

具体思路:

  • 本题的意思是十字连在一起的陆地都算做一个岛屿,与前几道题相似

在这里插入图片描述

  • 本题可以采用类似渲染的做法,尝试以每个点作为渲染的起点,可以渲染的陆地都算做一个岛屿,最后看渲染了多少次,即深度优先算法执行了多少次

写法一:

由于每一次调用Dfs都可以遍历一座岛的 1 ,为了方便,我们直接把遍历到的 1 改为 0 ,省去了再判断这个岛有没有遍历过的过程。

static int des[4][2] = { {1, 0}, {-1, 0}, {0, 1}, {0, -1} };
class Solution {
public:
    void Dfs(vector<vector<char>>& grid, int row, int col, int x, int y)
    {
        // 将 1 --> 0
        grid[x][y] = '0';

        for (int i = 0; i < 4; ++i)
        {
            int newX = x + des[i][0];
            int newY = y + des[i][1];

            if (newX < 0 || newX >= row || newY < 0 || newY >= col)
                continue;
            // 如果新坐标的点为 1 ,进入搜索
            if (grid[newX][newY] == '1')
                Dfs(grid, row, col, newX, newY);
        }
    }
    int numIslands(vector<vector<char>>& grid) {
        int row = grid.size();
        int col = grid[0].size();
        int num = 0;

        for (int i = 0; i < row; ++i)
        {
            for (int j = 0; j < col; ++j)
            {
                // 每遇到1就调用一次搜索,搜索次数就是岛的数量
                if (grid[i][j] == '1')
                {
                    Dfs(grid, row, col, i, j);
                    ++num;
                }
            }
        }

        return num;
    }
};

写法二:

如果我们不想破坏题目给的条件,那么我们可以创建一个bool类型的数组,记录这些点有没有被遍历过。如果没有遍历过并且该点为 1 ,则进入搜索。

static int des[4][2] = { {1, 0}, {-1, 0}, {0, 1}, {0, -1} };
class Solution {
public:
    void Dfs(vector<vector<char>>& grid, int row, int col, int x, int y, vector<vector<bool>>& book)
    {
        // 将该点的状态改为已经遍历
        book[x][y] = true;

        for (int i = 0; i < 4; ++i)
        {
            int newX = x + des[i][0];
            int newY = y + des[i][1];

            if (newX < 0 || newX >= row || newY < 0 || newY >= col)
                continue;
            // 如果没有遍历过该店并且该点为 1 ,则进入搜索
            if (grid[newX][newY] == '1' && book[newX][newY] == false)
                Dfs(grid, row, col, newX, newY, book);
        }
    }
    int numIslands(vector<vector<char>>& grid) {
        int row = grid.size();
        int col = grid[0].size();
        int num = 0;
        // 初始化bool类型的数组为全未遍历
        vector<vector<bool>> book(row, vector<bool>(col, false));

        for (int i = 0; i < row; ++i)
        {
            for (int j = 0; j < col; ++j)
            {
                // 如果没有遍历过该店并且该点为 1 ,则进入搜索
                if (grid[i][j] == '1' && book[i][j] == false)
                {
                    Dfs(grid, row, col, i, j, book);
                    ++num;
                }
            }
        }

        return num;
    }
};

🥃1.7 岛屿的最大面积


在这里插入图片描述

原题链接:岛屿的最大面积

这个题就是上边题目的相似题目,各位可以自行练习,思路比较简单,所以我们重点就放在代码实现上。

static int des[4][2] = { {1, 0}, {-1, 0}, {0, 1}, {0, -1} };
class Solution {
public:
    void Dfs(vector<vector<int>>& grid, int row, int col, int x, int y, int& s)
    {
        // 遇到一块陆地,面积加一,将其改为2,代表已经遍历过
        ++s;
        grid[x][y] = 2;

        for (int i = 0; i < 4; ++i)
        {
            int newX = x + des[i][0];
            int newY = y + des[i][1];
            //  判断新坐标是否越界
            if (newX < 0 || newX >= row || newY < 0 || newY >= col)
                continue;
            // 如果这个方向的土地还没有被搜索
            if (grid[newX][newY] == 1)
                Dfs(grid, row, col, newX, newY, s);
        }
    }
    int maxAreaOfIsland(vector<vector<int>>& grid) {
        int Max = 0;
        int row = grid.size();
        int col = grid[0].size();

        for (int i = 0; i < row; ++i)
        {
            for (int j = 0; j < col; ++j)
            {
                int s = 0;
                if (grid[i][j] == 1)
                    Dfs(grid, row, col, i, j, s);
                // 保存面积最大值
                Max = max(s, Max);
            }
        }

        return Max;
    }
};

🥛1.8 电话号码的字母组合


在这里插入图片描述

原题链接:电话号码的字母组合

这是一道非常经典的回溯算法题,思路其实也很简单:

  1. 遍历题目给的数字字符串,按照数字找到对应的字母字符串。
  2. 按遍历的元素顺序在当前后面加字符,直到遍历完成。
  3. 当遍历完题目给的数字字符串时,把此时得到的字符串保存。

在这里插入图片描述

// 哈希表用来对照
static string harsh[] = {"", "", "abc", "def", "ghi", "jkl", "mno",
"pqrs", "tuv", "wxyz"};
class Solution {
public:
    void Dfs(string& digits, int index, string curStr, vector<string>& ret)
    {
        // 当当前的字符串长度与digits相等,如果当前字符串不为空,将其插入
        if(curStr.size() == digits.size())
        {
            if(!curStr.empty())
                ret.push_back(curStr);
            return;
        }
        // 找到当前digits对应的数字
        int curInd = digits[index] - '0';
        // 遍历这个哈希表中对应的字符串
        for(auto& ch: harsh[curInd])
        {
            // 尝试每一种结果,index为digits下标,每次加一
            // 每次在当前字符串后加入当前对应的哈希表字符,尝试所有可能性
            Dfs(digits, index + 1, curStr + ch, ret);
        }
    }
    vector<string> letterCombinations(string digits) {
        vector<string> ret;
        Dfs(digits, 0, "", ret);
        return ret;
    }
};

🧃1.9 二进制手表


在这里插入图片描述

原题链接:二进制手表

这道题虽然标的是简单难度,但是只是针对于官方给出的答案,很多人看到这道题的第一反应依然是回溯算法,而不会想到官方的思路,这里先卖个关子,待会放到后面讲述。其实这里如果要拿回溯算法想的话,难度甚至要高于前面所有题目。

在这里插入图片描述

法一:回溯算法+深度优先搜索

首先,我们先要确定这次深度优先的搜索方式:

我们以亮灯数量为2来举例:

在这里插入图片描述

可见,依旧是依次遍历,只不过由亮灯数来决定递归的层数(大家可以在纸上仿照这种依次遍历的顺序画一下亮灯数为3的情况)。

其次,我们需要一个数组来保存上面的二进制的小时和分钟。

我们选择将小时和分钟保存到一个数组中,当访问下标为 0 ~ 3 的元素时,记为遍历的是小时,当访问 4 ~ 9 下标的元素时,记为遍历的是分钟。

int Time[10] = { 1, 2, 4, 8, 1, 2, 4, 8, 16, 32 };

再者,我们需要判断当前递归是否到达最大的亮灯数:

int num; // num为 最大亮灯数 - 当前亮灯数

还需要记录当前遍历的小时数和分钟数:

int curHour;
int curMinute;

最后,由上动图得,下一次递归调用时,开始遍历的位置在上一次递归调用遍历位置的下一个(保证不重复亮灯),所以,我们还得传递下一次递归调用的位置。

int pos;

代码实现如下:

int Time[10] = { 1, 2, 4, 8, 1, 2, 4, 8, 16, 32 };
class Solution {
public:
	
    void Dfs(vector<string>& ret, int num, int pos, int curHour, int curMinute)
    {
        // 当前时间超出手表所能显示的范围就直接返回
        if (curHour >= 12 || curMinute >= 60)
            return;
        // 当 num == 0 时,说明已经亮起的灯数已经到达最大的亮灯数
        if (num == 0)
        {
            // 插入时间
            ret.push_back(to_string(curHour) + ":" + 
                          (curMinute > 9 ? to_string(curMinute) : "0" + to_string(curMinute)));
            return;
        }

        for (int i = pos; i < 10; ++i)
        {        
            if (i < 4)
            {
                // 现在小时数加上该灯的小时,就代表点亮这个灯
                curHour += Time[i];
                // num 更新为 num - 1
                // pos 更新为当前位置的后一个位置
                Dfs(ret, num - 1, i + 1, curHour, curMinute);
                // 遍历结束,将这个灯熄灭
                curHour -= Time[i];
            }
            else
            {
                // 现在分钟数加上该灯的分钟,就代表点亮这个灯
                curMinute += Time[i];
                // num 更新为 num - 1
                // pos 更新为当前位置的后一个位置
                Dfs(ret, num - 1, i + 1, curHour, curMinute);
                // 遍历结束,将这个灯熄灭
                curMinute -= Time[i];
            }
        }

    }
    vector<string> readBinaryWatch(int turnedOn) {
        // 要返回的顺序表
        vector<string> ret;
        Dfs(ret, turnedOn, 0, 0, 0);

        return ret;
    }
};

法二:十进制枚举遍历

由于二进制手表只能显示 0 到 11 的小时数,以及 0 到 59 的分钟数,所以我们可以直接遍历二进制手表所能表现的全部时间,统计 当前小时数二进制表示中 1 的个数 和 当前分钟数二进制中 1 的个数(二进制中的 1 就相当于是亮灯, 0 相等于灭灯),加起来等于最大亮灯数就记录这个时间。

如 7:32 ,7的二进制为 0111 ,相当于二进制手表小时灯亮了3个,32的二进制为 100000 ,相当于二进制手表分钟灯亮了1个,加起来就是亮了4个,如果此时最大亮灯数为4,就将7:32 记录。

由于这个方法的遍历次数是确定的,所以时间复杂度为 O ( 1 ) O(1) O(1)

int hour[4] = { 1, 2, 4, 8 };
int minute[6] = { 1, 2, 4, 8, 16, 32 };
class Solution {
public:
    // 统计十进制数字二进制形式中1的个数
    int Binary_1(int x)
    {
        int cnt = 0;
        while (x)
        {
            x = x & (x - 1);
            cnt++;
        }
        return cnt;
    }
    vector<string> readBinaryWatch(int turnedOn) {
        vector<string> ret;

        for (int h = 0; h < 12; h++)
        {
            for (int min = 0; min < 60; min++)
            {
                if (Binary_1(h) + Binary_1(min) == turnedOn)
                {
                    ret.push_back(to_string(h) + ":" + (min < 10 ? "0" + to_string(min) : to_string(min)));
                }
            }
        }
        return ret;
    }
};

法三:直接枚举法

这种方法是因为有些人不满官方的枚举遍历法,而衍生出的一种娱乐方法,直接枚举全部最大亮灯数对应的所有时间。此方法就是调侃,就不举代码例子了,感兴趣的同学可以下去试试。


☕1.10 组合总和


在这里插入图片描述

原题链接:组合总和

具体思路:

此题相加的元素可以重复,所以去下一个元素的位置可以从当前位置开始, DFS + 回溯为了保证组合不重复(顺序不同,元素相同,也算重复),不再从当前位置向前看。例如,[2, 3, 5] ,先使用 2 这个数字,下一个DFS时也可以从 2 开始使用,但是如果第一个位置的数字 2 所有的组合已经遍历完,接下来 3 作为第一个数字,DFS调用时就不能使用 2 ,只能使用 3 及 3 以后的数字。

  • 实现思路:
  1. 从第一个元素开始相加

  2. 让局部和继续累加候选的剩余值

  3. 局部和等于目标值,保存组合,向上回退,寻找其它组合

class Solution {
public:
    // curWay用来记录当前的数字序列
    // ret用来存放符合题目条件的数字序列
    // curSum是当前数字序列的和
    // prev是存放这次函数调用开始遍历数字的下标
    void Dfs(vector<int>& candidates, int target, vector<int> curWay, vector<vector<int>>& ret,
    int curSum, int prev)
    {
        // 由于candidate中数字都大于0,当 curSum 大于等于 target 时,说明此时可以停止
        if(curSum >= target)
        {
            // 当curSum与target相等时,说明这就是一个符合题意的序列
            if(curSum == target)
                ret.push_back(curWay);
            return;
        }

        for(int i = prev; i < candidates.size(); ++i)
        {
            if(candidates[i] > target)
                continue;
            // 先加入当前序列
            curWay.push_back(candidates[i]);
            // DFS
            Dfs(candidates, target, curWay, ret, curSum + candidates[i], i);
            // 将这个元素弹出序列
            curWay.pop_back();
        }
    }
    vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
        vector<int> curWay;
        vector<vector<int>> ret;
        Dfs(candidates, target, curWay, ret, 0, 0);
        return ret;
    }
};

🍵1.11 活字印刷


在这里插入图片描述

原题链接:活字印刷

此题组合的长度不唯一,最小组合长度为1, 最大组合长度为 tiles 的长度。
按照题意 tiles 中每一个位置的字符在组合中只能出现一次, 所以可以用一个标记辅助
当去组合新的组合时,可以与 tiles 中的每一个位置组合,但是如果当前位置已经在当前组合中出现过,则跳过
虽然此题中每一个位置的字符在组合中只能出现一次,但是tiles中可能有相同的字符,所以需要考虑重复的组合
unordered_set 可以天然去重,可以用其去重

DFS + 回溯:

1.当前组合不为空, 则插入set
2.继续给当前组合拼接新的组合,尝试拼接tiles每一个位置的字符
3.如果当前位置已在组合中出现过,返回到2,否则标记当前位置,继续拼接更长的组合
4.回溯,尝试组合其它位置,返回2
当所有位置都已经使用过时,当前递归就结束了,继续向上层DFS回退
最终返回set的大小即为组合数目。

class Solution {
public:
	void dfs(string& tiles, string curStr, vector<int>& usedIdx, unordered_set<string>&
		totalString)
	{
		if (!curStr.empty())
		{
			totalString.insert(curStr);
		} 
		//标记保证所有位都用完之后,就结束了
		for(int i = 0; i < tiles.size(); ++i)
		{
			//当前位置的字符已用过,直接跳过
			if (usedIdx[i])
				continue;
			usedIdx[i] = 1;
			dfs(tiles, curStr + tiles[i], usedIdx, totalString);
			//回溯,尝试其它字符
			usedIdx[i] = 0;
		}
	} 
	int numTilePossibilities(string tiles) {
		if (tiles.empty())
			return 0;
		unordered_set<string> totalString;
		//标记全部初始化为未使用
		vector<int> usedIdx(tiles.size(), 0);
		dfs(tiles, "", usedIdx, totalString);
		return totalString.size();
	}
};

🧉1.12 N皇后


在这里插入图片描述

原题链接:N 皇后

🍭算法思想

  • 根据题意,皇后的同行,同列以及所处的两个斜线不能有皇后,所以这次我们可以选择按行搜索。
  • 在每一行选择一个位置放皇后,每层放置皇后时,要根据当前皇后放置情况判断是否能放置,能放置才能进入下一层递归。
  • 具体皇后放置判断标准:
    • 当有皇后在同一行,同一列,同一斜线上时,返回 false
    • 斜线判断:同一上斜线的皇后坐标:行数 + 列数 = 定值
    • 同一下斜线的皇后坐标:行数 - 列数 = 定值
    • 满足在同一条斜线上的直接 false

🍡代码实现

class Solution {
public:
    // allRet代表所有方案  curv为当前皇后放置的位置  
    // cur为当前查找的行数,当然也能理解为当前皇后的数量  n代表棋盘行数
    void DFS(vector<vector<pair<int, int>>>& allRet, vector<pair<int, int>>& curv, int curRow, int n)
    {
        // 有n位皇后则返回
        if(curRow == n)
        {
            allRet.push_back(curv);
            return;
        }
        // 按列查找
        for(int i = 0; i < n; ++i)
        {
            // 判断放置位置是否合法
            if(isVaildPos(curv, curRow, i))
            {
                curv.emplace_back(curRow, i);
                DFS(allRet, curv, curRow + 1, n);
                curv.pop_back();
            }
        }
    }
	// 判断皇后位置是否合法
    bool isVaildPos(vector<pair<int, int>>& curv, int i, int j)
    {
        for(auto& pos : curv)
        {
            // 当有皇后在同一行,同一列,同一斜线上时,返回false
            // 斜线判断:同一上斜线 -- 行数 + 列数 == 定值
            // 同一下斜线 == 行数 - 列数 == 定值
            if(pos.first == i || pos.second == j || pos.first + pos.second == i + j 
            || pos.first - pos.second == i - j)
                return false;
        }
        return true;
    }
	// 将全部可能的组合转换为字符串数组
    vector<vector<string>> transString(vector<vector<pair<int, int>>>& allRet, int n)
    {
        vector<vector<string>> vv;
        for(auto& vpos : allRet)
        {
            vector<string> vs(n, string(n, '.'));
            for(auto& pos : vpos)
            {
                vs[pos.first][pos.second] = 'Q';
            }
            vv.push_back(vs);
        }
        return vv;
    }

    vector<vector<string>> solveNQueens(int n) {
        vector<vector<pair<int, int>>> allRet;
        vector<pair<int, int>> curv;
        DFS(allRet, curv, 0, n);
        return transString(allRet, n);
    }
};

✨后记


相信做过了上述题目,大家对于回溯算法的逻辑有了很深的体会:

Dfs(当前这一步的处理逻辑)
{

  1. 判断边界,是否已经一条道走到黑了:向上回退

  2. 尝试当下的每一种可能

  3. 确定一种可能之后,继续下一步 Dfs(下一步)

}

下一篇刷题日记,我将会带来回溯算法的广度优先搜索的题目,上述逻辑同样适用。所以,一定要在做题中体会上述逻辑,多总结多思考。


这是一个新的系列 ——【刷题日记】,白晨开这个系列的初衷是为了分享一些不同算法经典题型,以便于大家更好的学习编程。如果大家喜欢这个专栏的话——点击查看【刷题日记】所有文章。

如果解析有不对之处还请指正,我会尽快修改,多谢大家的包容。

如果大家喜欢这个系列,还请大家多多支持啦😋!

如果这篇文章有帮到你,还请给我一个大拇指 👍和小星星 ⭐️支持一下白晨吧!喜欢白晨【刷题日记】系列的话,不如关注👀白晨,以便看到最新更新哟!!!

我是不太能熬夜的白晨,我们下篇文章见。

评论 70
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

白晨并不是很能熬夜

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值