递归、搜索与回溯——综合(下)

💁‍♂️个人主页:进击的荆棘

👇作者其它专栏:

《数据结构与算法》《算法》《C++起始之路》


目录

1.综合练习相关题解

2.floodfill算法

3.记忆化搜索


2.floodfill算法

2.1图像渲染

算法思路:

可以利用【深搜】或【宽搜】,遍历到与该点相连的所有【像素相同的点】,然后将其修改成指定的像素即可。

递归函数设计:

●参数:

a.原始矩阵;

b.当前所在的位置;

c.需要修改成的颜色。

●函数体:

a.先将该位置的颜色改成指定颜色(因为进入递归的条件为是需要修改的位置);

b.遍历四个方向上的位置:

▢若当前位置合法,并且与初始颜色相同,进入递归。

cpp 复制代码
class Solution {
    int target;
public:
    vector<vector<int>> floodFill(vector<vector<int>>& image, int sr, int sc, int color) {
        if(image[sr][sc]==color) return image;
    
        target=image[sr][sc];
        dfs(image,sr,sc,color);
        return image;
    }
    int dx[4]={1,-1,0,0};
    int dy[4]={0,0,1,-1};
    void dfs(vector<vector<int>>& image,int i,int j,int color){
        image[i][j]=color;

        for(int k=0;k<4;k++){
            int x=i+dx[k],y=j+dy[k];
            if(x>=0&&x<image.size()&&y>=0&&y<image[0].size()&&image[x][y]==target){
                dfs(image,x,y,color);
            }
        }
    }
};

2.2岛屿数量

算法思路:

遍历整个矩阵,每次找到【一块陆地】的时候:

●说明找到【一个岛屿】,记录到最终结果ret里面;

●并且将与这块陆地相连的所有陆地,也就是这块【岛屿】,全部【变成海洋】。这样的话,下次遍历到这块岛屿的时候,它【已经是海洋】了,不会影响最终结果。(或另创建一个check数组,标记走过的位置);

●其中【变成海洋】的操作,可以利用【深搜】和宽搜】解决,就是 2.1图像渲染这道题。

这样,当遍历完全部矩阵的时候,ret存的就是最终结果。

算法流程(dfs):

1.初始化ret=0,记录目前找到的岛屿数量;

2.双重循环遍历二维数组,每当遇到一块陆地,标记这是一个新的岛屿,然后将这块陆地相连的陆地全部变成海洋。

递归函数的设计:

1.把当前格子标记成水;

2.向上、下、左、右四个方向寻找陆地,只有在下标位置合理的情况下,才会进入递归:

a.下一个位置的坐标合理;

b.并且下一个位置是陆地

cpp 复制代码
class Solution {
    bool check[300][300];
    int ret=0;
public:
    int numIslands(vector<vector<char>>& grid) {
        for(int i=0;i<grid.size();i++){
            for(int j=0;j<grid[0].size();j++){
                if(grid[i][j]=='1'&&!check[i][j]){
                    dfs(grid,i,j);
                    ret++;
                }
            }
        }
        return ret;
    }
    int dx[4]={0,0,1,-1};
    int dy[4]={1,-1,0,0};
    void dfs(vector<vector<char>>& grid,int i,int j){
        if(grid[i][j]=='0') return ;

        for(int k=0;k<4;k++){
            int x=i+dx[k],y=j+dy[k];
            if(x>=0&&x<grid.size()&&y>=0&&y<grid[0].size()&&grid[x][y]=='1'&&!check[x][y]){
                check[x][y]=true;
                dfs(grid,x,y);
            }
        }
    }
};

2.3岛屿的最大面积

算法流程:

●主函数内:

a.遍历整个数组,发现一块没有遍历到的土地后,就用dfs,将与这块土地相连的岛屿的面积求出来;

b.然后将面积更新到最终结果ret中。

●深搜函数dfs中:

a.能够进到dfs函数中,说明是一个没遍历到的位置;

b.标记一下已经遍历过,设置一个变量cnt=1(当前这个位置的面积为1),记录最终的面积;

c.上下左右遍历四个位置:

▢若找到一块没有遍历到的土地,就将与这块土地相连的岛屿面积累积加到cnt上;

d.循环结束后,cnt中存的就是整块岛屿的面积,返回即可。

cpp 复制代码
class Solution {
    bool check[50][50];
    int cnt=0;
public:
    int maxAreaOfIsland(vector<vector<int>>& grid) {
        int ret=0;
        for(int i=0;i<grid.size();i++){
            for(int j=0;j<grid[0].size();j++){
                if(grid[i][j]==1){
                    cnt=0;
                    dfs(grid,i,j);
                    ret=max(ret,cnt);
                }
            }
        }
        return ret;
    }
    int dx[4]={1,-1,0,0};
    int dy[4]={0,0,1,-1};
    void dfs(vector<vector<int>>& grid,int i,int j){
        cnt++;
        check[i][j]=true;
        for(int k=0;k<4;k++){
            int x=i+dx[k],y=j+dy[k];
            if(x>=0&&x<grid.size()&&y>=0&&y<grid[0].size()&&grid[x][y]==1&&!check[x][y]){
                dfs(grid,x,y);
            }
        }
    }
};

2.4被围绕的区域

算法思路:

正难则反。

可以先利用dfs将与边缘相连的'O'区域做上标记,然后重新遍历矩阵,将没有标记过的'O'修改为'X'即可。

cpp 复制代码
class Solution {
//直接去修改被围绕的'O'会比较困难,可以先将与边界相邻的'O'修改,那么剩下的'O'一定都是被围绕的
public:
    void solve(vector<vector<char>>& board) {
        for(int i=0;i<board.size();i++){
            if(board[i][0]=='O') dfs(board,i,0);
            if(board[i][board[0].size()-1]=='O') dfs(board,i,board[0].size()-1);
        }
        for(int j=0;j<board[0].size();j++){
            if(board[0][j]=='O') dfs(board,0,j);
            if(board[board.size()-1][j]=='O') dfs(board,board.size()-1,j);
        }
        for(int i=0;i<board.size();i++){
            for(int j=0;j<board[0].size();j++){
                if(board[i][j]=='.')
                    board[i][j]='O';
                else if(board[i][j]=='O')
                    board[i][j]='X';
            }
        }    
    }
    int dx[4]={1,-1,0,0};
    int dy[4]={0,0,1,-1};
    void dfs(vector<vector<char>>& board,int i,int j){
        board[i][j]='.';

        for(int k=0;k<4;k++){
            int x=i+dx[k],y=j+dy[k];
            if(x>=0&&x<board.size()&&y>=0&&y<board[0].size()&&board[x][y]=='O'){
                dfs(board,x,y);
            }
        }
    }
};

2.5太平洋大西洋水流问题

算法思路:

正难则反。

若直接去判断某一个位置是否既能到大西洋也能到太平洋,会重复遍历很多路径。

可以反着来,从大西洋沿岸开始反向dfs,这样就能找出那些点可以流向大西洋;同理,从太平洋沿岸也反向dfs,这样就能找出那些点可以流向太平洋。那么,被标记两次的点,就是要找的结果。

cpp 复制代码
class Solution {
    //直接找会有很多重复的路径,逆向思维,从洋到坐标,看水能逆流到哪里,最后两个洋重叠的区间就是结果
    vector<vector<int>> ret;
public:
    vector<vector<int>> pacificAtlantic(vector<vector<int>>& heights) {
        vector<vector<bool>> check1(heights.size(),vector<bool>(heights[0].size()));;
        vector<vector<bool>> check2(heights.size(),vector<bool>(heights[0].size()));
        //先从太平洋开始找
        for(int j=0;j<heights[0].size();j++){
            dfs(heights,0,j,check1);
        }
        for(int i=0;i<heights.size();i++){
            dfs(heights,i,0,check1);
        }
        //再从大西洋开始找
        for(int i=0;i<heights.size();i++){
            dfs(heights,i,heights[0].size()-1,check2);
        }
        for(int j=0;j<heights[0].size();j++){
            dfs(heights,heights.size()-1,j,check2);
        }
        for(int i=0;i<heights.size();i++){
            for(int j=0;j<heights[0].size();j++){
                if(check1[i][j]&&check2[i][j])
                    ret.push_back({i,j});
            }
        }
        return ret;
    }
    int dx[4]={1,-1,0,0};
    int dy[4]={0,0,1,-1};
    void dfs(vector<vector<int>>& heights,int i,int j,vector<vector<bool>>& check){
        check[i][j]=true;
        for(int k=0;k<4;k++){
            int x=i+dx[k],y=j+dy[k];
            if(x>=0&&x<heights.size()&&y>=0&&y<heights[0].size()&&heights[i][j]<=heights[x][y]&&!check[x][y]){
                dfs(heights,x,y,check);
            }
        }
    }
};

2.6扫雷游戏

模拟类型的dfs题目。

首先要搞懂题目要求,即游戏规则。

从题目所给的点位置开始,根据游戏规则,来一次dfs即可。

cpp 复制代码
class Solution {
public:
    vector<vector<char>> updateBoard(vector<vector<char>>& board, vector<int>& click) {
        //若是地雷
        if(board[click[0]][click[1]]=='M')
            board[click[0]][click[1]]='X';
        else
            dfs(board,click[0],click[1]);
        return board;
    }
    int dx[8]={1,-1,0,0,-1,-1,1,1};
    int dy[8]={0,0,1,-1,-1,1,-1,1};
    int check(vector<vector<char>>& board,int i,int j){
        int cnt=0;
        for(int k=0;k<8;k++){
            int x=i+dx[k],y=j+dy[k];
            if(x>=0&&x<board.size()&&y>=0&&y<board[0].size()&&board[x][y]=='M')
                cnt++;
        }
        return cnt;
    }
    void dfs(vector<vector<char>>& board,int i,int j){
        //先查看该位置周围是否有地雷
        int cnt=check(board,i,j);
        if(cnt!=0){
            board[i][j]='0'+cnt;
            return ;
        }
        else{
            board[i][j]='B';
            for(int k=0;k<8;k++){
                int x=i+dx[k],y=j+dy[k];
                if(x>=0&&x<board.size()&&y>=0&&y<board[0].size()&&board[x][y]=='E')
                    dfs(board,x,y);
            }
        }
    }
};

2.7衣橱整理

算法流程:

递归函数设计:

a.参数:当前所在位置[i,j],行走的边界[m,n],坐标数位之和的边界k;

b.递归出口:

i.[i,j]的坐标不合法,即已经超出能走的范围;

ii.[i,j]位置已经走过了(因此需要创建一个全局变量bool vis[101][101],来标记当前位置是否走过);

iii.[i,j]坐标的数位之和大于k;

上述情况的任一种都是递归出口。

c.函数体内部:

i.若这个坐标是合法的,就将全局变量ret++;

ii.然后标记一下[i,j]位置是否已经遍历过;

iii.然后去[i,j]位置的上下左右四个方向看看。

辅助函数:

a.检测坐标[i,j]是否合法;

b.计算出i,j的数位之和,然后与k作比较即可。

主函数:

a.调用递归函数,从[0,0]点出发。

●辅助的全局变量:

a.二维数组bool vis[101][101]:标记[i,j]位置是否已经遍历过;

b.变量ret:记录一共到达多少个合法的位置。

c.上下左右的四个坐标变换。

cpp 复制代码
class Solution {
    bool vis[100][100];
    int ret=0;
public:
    int wardrobeFinishing(int m, int n, int cnt) {
        dfs(m,n,cnt,0,0);
        return ret;
    }
    int dx[2]={0,1};
    int dy[2]={1,0};
    bool Check(int x,int y,int cnt){
        int ret=0;
        while(x){
            ret+=x%10;
            x/=10;
        }
        while(y){
            ret+=y%10;
            y/=10;
        }
        return ret<=cnt;
    }
    void dfs(int m,int n,int cnt,int i,int j){
        if(i==m||n==j) return ;
        vis[i][j]=true;
        ret++;

        for(int k=0;k<2;k++){
            int x=i+dx[k],y=j+dy[k];
            if(x>=0&&x<m&&y>=0&&y<n&&!vis[x][y]&&Check(x,y,cnt)){

                dfs(m,n,cnt,x,y);
            }
        }
    }
};

3.记忆化搜索

3.1斐波那契数

算法思路(暴搜->记忆化搜索->动态规划):

暴搜:

a.递归含义:给dfs一个使命,给它一个数n,返回第n个斐波那契数的值;

b.函数体:斐波那契数的递推公式;

c.递归出口:当n==0或n==1时,不用套公式。

记忆化搜索:

a.加上一个备忘录;

b.每次进入递归的时候,去备忘录里面看看;

c.每次返回的时候,将结果加入到备忘录里面。

动态规划:

a.递归含义->状态表示;

b.函数体->状态转移方程;

c.递归出口->初始化。

cpp 复制代码
//法一:记忆化搜索
class Solution {
    int memo[31];//memory备忘录
public:
    int fib(int n) {
        memset(memo,-1,sizeof memo);
        return dfs(n);
    }
    //记忆化搜索
    int dfs(int n){
        if(memo[n]!=-1) 
            return memo[n];//直接去备忘录里取值
        if(n==0||n==1){
            memo[n]=n;//记录到备忘录中
            return n;
        }
        memo[n]=dfs(n-1)+dfs(n-2);//记录到备忘录中
        return memo[n];
    }
};
//法二:动态规划
class Solution {
    int dp[31];
public:
    int fib(int n) {
        //动态规划
        dp[0]=0;dp[1]=1;
        for(int i=2;i<=n;i++){
            dp[i]=dp[i-1]+dp[i-2];
        }
        return dp[n];
    }

};

3.2不同路径

算法思路(暴搜->记忆化搜索->动态规划):

暴搜:

a.递归含义:给dfs一个使命,给它一个下标n,返回从[0,0]位置走到[i,j]位置一共有多少种方法;

b.函数体:只要知道到达上面位置的方法数以及到达左边位置的方法数,然后累加起来即可;

c.递归出口:当下标越界的时候返回0;当位于起点时,返回1。

记忆化搜索:

a.加上一个备忘录;

b.每次进入递归的时候,去备忘录里面看看;

c.每次返回的时候,将结果加入到备忘录里面。

动态规划:

a.递归含义->状态表示;

b.函数体->状态转移方程;

c.递归出口->初始化。

cpp 复制代码
//法一:记忆化搜索:逆向思维,要到达(m,n)有两个方向,(m-1,n)和(m,n-1)即向下和向右
class Solution {
    int memo[101][101];
    int ret=0;
public:
    int uniquePaths(int m, int n) {
        memset(memo,-1,sizeof memo);
        return dfs(m,n); 
    }
    int dx[2]={0,1};
    int dy[2]={1,0};
    int dfs(int i,int j){
        if(memo[i][j]!=-1){
            return memo[i][j];    
        }
        //越界
        if(i==0||j==0) return 0;
        if(i==1&&j==1) return 1;

        memo[i][j]=dfs(i,j-1)+dfs(i-1,j);
        return memo[i][j];
    }
};
//法二:动态规划
class Solution {
public:
    int uniquePaths(int m, int n) {
        vector<vector<int>> dp(m+1,vector<int>(n+1));
        //初始化
        dp[1][1]=1;
        for(int i=1;i<=m;i++){
            for(int j=1;j<=n;j++){
                if(i==1&&j==1)
                    continue;
                dp[i][j]=dp[i-1][j]+dp[i][j-1];
            }
        }
        return dp[m][n]; 
    }
};

3.3最长递增子序列

算法思路(暴搜->记忆化搜索->动态规划):

暴搜:

a.递归含义:给dfs一个使命,给它一个数i,返回以i位置为起点的最长递增子序列的长度;

b.函数体:遍历i后面的所有位置,看谁能加到i这个元素的后面。统计所有情况下的最大值;

c.递归出口:因为是判断之后才进入递归的,因此没有出口。

记忆化搜索:

a.加上一个备忘录;

b.每次进入递归的时候,去备忘录里面看看;

c.每次返回的时候,将结果加入到备忘录里面。

动态规划:

a.递归含义->状态表示;

b.函数体->状态转移方程;

c.递归出口->初始化。

cpp 复制代码
//法一:记忆化搜索
class Solution {
    int ret;
public:
    int lengthOfLIS(vector<int>& nums) {
        vector<int> memo(nums.size());

        for(int i=0;i<nums.size();i++){
            ret=max(ret,dfs(nums,i,memo));
        }
        return ret;
    }
    int dfs(vector<int>& nums,int pos,vector<int>& memo){
        if(memo[pos]!=0) return memo[pos];

        int ret=1;
        for(int i=pos+1;i<nums.size();i++){
            if(nums[i]>nums[pos]){
                ret=max(dfs(nums,i,memo)+1,ret);
            }
        }
        memo[pos]=ret;
        return ret;
    }
};
//法二:动态规划
class Solution {
public:
    int lengthOfLIS(vector<int>& nums) {
        vector<int> dp(nums.size(),1);
        int ret=0;
        //填表从后往前,因为要找最长的子序列,需向后查找
        for(int i=nums.size()-1;i>=0;i--){
            for(int j=i+1;j<nums.size();j++){
                if(nums[i]<nums[j]){
                    dp[i]=max(dp[i],dp[j]+1);
                }
            }
            ret=max(ret,dp[i]);
        }
        return ret;
    }
};

3.4猜数字大小 II

算法思路(暴搜->记忆化搜索):

暴搜:

a.递归含义:给dfs一个使命,给它一个区间[left,right],返回在这个区间上能完胜的最小费用;

b.函数体:选择[left,right]区间上的任意一个数作为头节点,然后递归分析左右子树,求出所有情况下的最小值;

c.递归出口:当left>=right的时候,直接返回0。

记忆化搜索:

a.加上一个备忘录;

b.每次进入递归的时候,去备忘录里面看看;

c.每次返回的时候,将结果加入到备忘录里面。

cpp 复制代码
class Solution {
    int memo[201][201];
public:
    int getMoneyAmount(int n) {
        return dfs(1,n);
    }
    int dfs(int left,int right){
        if(left>=right) return 0;
        if(memo[left][right]!=0) return memo[left][right];

        int ret=INT_MAX;
        for(int head=left;head<=right;head++){//选择头节点
            int l=dfs(left,head-1);
            int r=dfs(head+1,right);
            ret=min(ret,head+max(l,r));
        }
        memo[left][right]=ret;
        return ret;
    }
};

3.5矩阵中的最长递增路径

算法思路(暴搜->记忆化搜索):

暴搜:

a.递归含义:给dfs一个使命,给它一个下标[i,j],返回从这个位置开始的最长递增路径的长度;

b.函数体:上下左右四个方向看一看,哪里能过去就过去,统计四个方向上的最大长度;

c.递归出口:因为是先判断再进入递归,因此没有出口。

记忆化搜索:

a.加上一个备忘录;

b.每次进入递归的时候,去备忘录里面看看;

c.每次返回的时候,将结果加入到备忘录里面。

cpp 复制代码
class Solution {
    int memo[201][201];
public:
    int longestIncreasingPath(vector<vector<int>>& matrix) {
        int ret=0;
        for(int i=0;i<matrix.size();i++){
            for(int j=0;j<matrix[0].size();j++){
                ret=max(ret,dfs(matrix,i,j));
            }
        }
        return ret;
    }
    int dx[4]={1,-1,0,0};
    int dy[4]={0,0,1,-1};
    int dfs(vector<vector<int>>& matrix,int i,int j){
        if(memo[i][j]!=0) return memo[i][j];
        int ret=1;
        for(int k=0;k<4;k++){
            int x=i+dx[k],y=j+dy[k];
            if(x>=0&&x<matrix.size()&&y>=0&&y<matrix[0].size()&&matrix[x][y]>matrix[i][j]){
                ret=max(ret,dfs(matrix,x,y)+1);
            }
        }
        memo[i][j]=ret;
        return ret;    
    }
};
相关推荐
不知名的忻1 小时前
归并排序(Java)
java·算法·排序算法
代码中介商3 小时前
C++ STL 容器完全指南(二):vector 深入与 stringstream 实战
开发语言·c++
YUDAMENGNIUBI4 小时前
day20_逻辑回归
算法·机器学习·逻辑回归
澈2078 小时前
C++并查集:高效解决连通性问题
java·c++·算法
郝学胜-神的一滴9 小时前
Qt 入门 01-01:从零基础到商业级客户端实战
开发语言·c++·qt·程序人生·软件构建
宏笋10 小时前
C++ thread的detach()方法详解
c++
旖-旎10 小时前
深搜练习(单词搜索)(12)
c++·算法·深度优先·力扣
企客宝CRM11 小时前
2026年中小企业CRM选型指南:企客宝CRM处于什么位置?
android·算法·企业微信·rxjava·crm
橙淮11 小时前
二叉树核心概念与Java实现详解
数据结构·算法