6.算法——搜索,深度优先搜索:695岛屿最大面积,547省份数量,417水流问题;回溯法:46排列,77组合,79单词搜索,51N 皇后;广度优先搜索:934最短的桥,126单词接龙

深度优先搜索和广度优先搜索是两种最常见的优先搜索方法,它们被广泛地运用在图和树等结构中进行搜索。

深度优先搜索(depth-first search,DFS)

深度优先搜索(depth-first seach,DFS)在搜索到一个新的节点时,立即对该新节点进行遍历;因此遍历需要用先入后出的栈来实现,也可以通过与栈等价的递归来实现。对于树结构而言,由于总是对新节点调用遍历,因此看起来是向着“深”的方向前进。
如有简单树:
在这里插入图片描述
我们从 1 号节点开始遍历,假如遍历顺序是从左子节点到右子节点.
假如我们使用递归实现,我们的遍历过程为 1(起始节点)->2(遍历更深一层的左子节点)->4(遍历更深一层的左子节点)->2(无子节点,返回父结点)->1(子节点均已完成遍历,返回父结点)->3(遍历更深一层的右子节点)->1(无子节点,返回父结点)-> 结束程序(子节点均已完成遍历).
如果我们使用栈实现,我们的栈顶元素的变化过程为 1->2->4->3。

深度优先搜索也可以用来检测环路:记录每个遍历过的节点的父节点,若一个节点被再次遍历且父节点不同,则说明有环
有时我们可能会需要对已经搜索过的节点进行标记,以防止在遍历时重复搜索某个节点,这种做法叫做状态记录或记忆化(memoization)。

例题

695. 岛屿的最大面积

给定一个包含了一些 0 和 1 的非空二维数组 grid 。

一个 岛屿 是由一些相邻的 1 (代表土地) 构成的组合,这里的「相邻」要求两个 1 必须在水平或者竖直方向上相邻。你可以假设 grid 的四个边缘都被 0(代表水)包围着。

找到给定的二维数组中最大的岛屿面积。(如果没有岛屿,则返回面积为 0 。)

在这里插入图片描述
递归写法:
一般来说,深度优先搜索类型的题可以分为主函数和辅函数,主函数用于遍历所有的搜索位置,判断是否可以开始搜索,如果可以即在辅函数进行搜索。辅函数则负责深度优先搜索的递归调用。刷题时推荐使用递归式写法,同时也方便进行回溯。而递归相对便于实现。

栈写法:
当然,我们也可以使用栈(stack)实现深度优先搜索,栈与递归的调用原理相同,在实际工程上,直接使用栈可能才是最好的选择,一是因为便于理解,二是更不易出现递归栈满的情况。

遍历技巧
1.对于四个方向的遍历,可以创造一个数组 [-1, 0, 1, 0, -1],每次用现在的位置加上数组中相邻两位即为上下左右四个方向之一,相当于第一次往左(-1,0),第二次往上(0,1),以此类推。(见代码辅函数中对direction的应用)
2.在可以更改这个数组的情况下,把所有遍历的1都直接改成0,这样有以下好处:

  • 首先,防止重复递归,比如我一开始是从中间递归到左边,防止左边这个数又递归的时候算了右边(即刚才中间)这个数。
  • 其次,防止重复遍历,我在遍历第一个1的时候就把所有与他相邻的1全部变成0,这样等接下来我遍历到其他1的时候,已经是0了,没有必要再遍历了。

在辅函数里,一个一定要注意的点是辅函数内递归搜索时,边界条件的判定。先判定是否越界,只有在合法的情况下才进行下一步搜索(即判断放在调用递归函数前);

代码:

vector<int> direction{-1,0,1,0,-1};

int maxAreaOfIsland(vector<vector<int>>& grid) 
    {
        //先判空 二维vector判空[]或者[[]]
        if(grid.empty() ||grid[0].empty()) return 0;
        int max_area=0;
        for(int i=0;i<grid.size();++i)
        {
        	for(int j=0;j<grid[i].size();++j)
        	{
        		if(grid[i][j]==1)
        		{
        			max_area=max(max_area,dfs(grid,i,j));
        		}
        	}
        }
        return max_area;
    }
//辅函数dfs
int dfs(vector<vector<int>> grid,int i,int j)
{
	if(grid[i][j]==0) return 0; //如果找到的点是0就返回0
	grid[i][j]=0;//如果找到了1,得把这个点置为0,面积area+1,避免递归回来又把这个点加了一遍,也可避免待会有重新全部递归
	int x,y,area=1;
	for(int p=0;p<4;++p)
	{
		x=r+direction[p],y=c+direction[p+1];
		//相当于第一次往左(-1,0),第二次往上(0,1),以此类推
		//接下来的条件要保证上下左右都没有越界
		if(x>=0 &&x<grid.size() && y>=0 &&y<grid[x].size())
		{	area+=dfs(grid,x,y);	}
	}
	return area;
}

547. 省份数量

在这里插入图片描述
在这里插入图片描述
其实是一个无向图分块的问题。
根据深度优先搜索的思想,每当我找到一个和我有连接的省份,我就接着找哪些城市和他也连接,这样递归回来,就能找到所有和其连接的省份,这样他们都算是一块儿,就是一个省,然后++num;

class Solution {
public:
    int findCircleNum(vector<vector<int>>& isConnected) 
    {
        int n=isConnected.size(),maxnum=0;
        vector<bool> visited(n,false);
        for(int i=0;i<n;++i)
        {
            if(!visited[i]) //这一块儿看找没找过
            {
                dfs(isConnected,i,visited);
                ++maxnum;
            }
            
        }
        return maxnum;
    }
    void dfs(vector<vector<int>>& isConnected,int i,vector<bool>& visited) //所有与之相连的都变成true
    {
        visited[i]=true;
        for(int j=0;j<isConnected.size();++j)
        {
            if(isConnected[i][j]==1 && !visited[j]) dfs(isConnected,j,visited);
        }
    }
};

417. 太平洋大西洋水流问题

给定一个 m x n 的非负整数矩阵来表示一片大陆上各个单元格的高度。“太平洋”处于大陆的左边界和上边界,而“大西洋”处于大陆的右边界和下边界。
规定水流只能按照上、下、左、右四个方向流动,且只能从高到低或者在同等高度上流动。
请找出那些水流既可以流动到“太平洋”,又能流动到“大西洋”的陆地单元的坐标。

在这里插入图片描述
分析:
如果我们从每个点往下找,必定需要遍历所有的点,但是我们逆向思维一下,如果我们从两个洋反向往上面流,那这样的话,我们只需要检查四条边,从四条边往上面找即可。

首先用之前用过的一个vector来代表四个方向:

vector<int> direction{-1,0,1,0,-1};

主函数:

vector<vector<int>> pacificAtlantic(vector<vector<int>>& heights) 
{
	//先判空
	if(heights.empty() || heights[0].empty()) 
		return {};
	//返回的变量要求是vector<vector<int>>
	vector<vector<int>> ans;
	int m=heights.size(),n=heights[0].size();//m行n列
	//记录从边上某点能不能到达某个点vector<vector<bool>> ,大小和heights一样
	vector<vector<bool>> canreach_pacific(m,vector<bool>(n,false));
	vector<vector<bool>> canreach_atlantic(m,vector<bool>(n,false));
	//
	for(int i=0;i<m;++i) //看左边第一列和右边最后一列这两条边
	{
		dfs(heights,canreach_pacific,i,0);//从太平洋第一列出发能不能到某个点
		dfs(heights,canreach_atlantic,i,n-1);//从大西洋最后一列出发看能不能到某个点
	}
	for(int j=0;j<n;++j)//看上下两条边
	{
		dfs(heights,canreach_pacific,0,j);//第一行
		dfs(heights,canreach_atlantic,m-1,j);//最后一行
	}
   //现在只需要遍历每一个点,看哪个点的两个reach坐标都为true,就记录入栈
   for(int i=0;i<m;++i)
   {
   		for(int j=0;j<n;++j)
   		{
   			if(canreach_pacific[i][j] &&canreach_atlantic[i][j])
   			 {ans.push_back(vector<int>{i,j});}
   		}
   }
   return ans;
   
	
}

辅助函数dfs:

void dfs(vector<vector<int>>& heights,vector<vector<bool>>& canreach,int i,int j)
{
	if(canreach[i][j]) return;//如果从这条边能到这个点,且已经到过了,直接返回,不需要再判断
	//之前没有到过,现在到了
	canreach[i][j]=true;
	int x,y;
	for(int p=0;p<4;++p)
	{
		x=i+direction[p],y=j+direction[p+1];
		if(x>=0 &&x<heights.size() && y>=0 &&y<heights[0].size() &&heights[x][y]>=heights[i][j])
		//没有越界且能往上流就一直找
		{dfs(heights,canreach,x,y);}
	}	
}

注意:
在判断递归条件时,得先判断是否越界再判断heights大小的问题:
如下面这样的代码是正确的:

if(x>=0 && x<heights.size() && y>=0 && y<heights[0].size()&&heights[x][y]>=heights[i][j] )

但是我如果把height的比较放在最前面,这个代码就会造成越界,跑的时候就会overflow
如下面的写法就是错误的:

if(heights[x][y]>=heights[i][j]&&x>=0 && x<heights.size() && y>=0 && y<heights[0].size() )

因为这个if会首先判断最左边的条件,这个时候它最先得取到heights[x][y]的值,其实已经越界了就取不到这个值了。

完整代码:

class Solution {
public:
    vector<vector<int>> pacificAtlantic(vector<vector<int>>& heights) 
    {
      //empty
      if(heights.empty() ||heights[0].empty() ) return {};
      //m行n列
      int m=heights.size(),n=heights[0].size();
      vector<vector<int>> ans;
      //记录从大洋出发能不能到某个点
      vector<vector<bool>> canreach_p(m,vector<bool>(n,false));
      vector<vector<bool>> canreach_a(m,vector<bool>(n,false));

      //第一行和最后一行
      for(int i=0;i<n;++i)
      {
        dfs(heights,canreach_p,0,i);
        dfs(heights,canreach_a,m-1,i);
      }
      //第一列和最后一列
      for(int j=0;j<m;++j)
      {
          dfs(heights,canreach_p,j,0);
          dfs(heights,canreach_a,j,n-1);
      }
      for(int i=0;i<m;++i)
      {
          for(int j=0;j<n;++j)
          {
              if(canreach_a[i][j] && canreach_p[i][j])
               {ans.push_back(vector<int>{i,j});}
          }
      }
      return ans;
    }
    
    vector<int> direction{-1,0,1,0,-1};
    void dfs(vector<vector<int>>& heights,vector<vector<bool>>& canreach,int i,int j)
    {
        if(canreach[i][j]) return;
        canreach[i][j]=true;
        int x,y;
        for(int p=0;p<4;++p)
        {
            x=i+direction[p],y=j+direction[p+1];
            if(x>=0 && x<heights.size() && y>=0 && y<heights[0].size()&&heights[x][y]>=heights[i][j] )
             {dfs(heights,canreach,x,y);}
        }
    }
};

回溯法(backtracking)

回溯法(backtracking)是优先搜索的一种特殊情况,又称为试探法,常用于需要记录节点状态的深度优先搜索。通常来说,排列、组合、选择类问题使用回溯法比较方便。
顾名思义,回溯法的核心是回溯。在搜索到某一节点的时候,如果我们发现目前的节点(及其子节点)并不是需求目标时,我们回退到原来的节点继续搜索,并且把在目前节点修改的状态还原。
这样的好处是我们可以始终只对图的总状态进行修改,而非每次遍历时新建一个图来储存状态。在具体的写法上,它与普通的深度优先搜索一样,都有 [修改当前节点状态]→[递归子节点] 的步骤,只是多了回溯的步骤,变成了 [修改当前节点状态]→[递归子节点]→[回改当前节点状态]

两个小诀窍,
一是按引用传状态
二是所有的状态修改在递归完成后回改

回溯法修改一般有两种情况,
一种是修改最后一位输出,比如排列组合
一种是修改访问标记,比如矩阵里搜字符串

回溯法之——排列组合问题

例46. 全排列

给定一个 没有重复 数字的序列,返回其所有可能的全排列。
在这里插入图片描述
如何输出所有的排列方式,按照我们之前说的应该分为三步:

  1. 修改当前节点状态:在此处即可以把当前位置与之后的每一个位置交换。(为了便于理解,我们分层来说,以123为例,第一层的数应该有,213,321,132)
  2. 递归子节点:那么我们的第二层子节点就要从第二位开始交换,231,312,123
  3. 回改当前节点状态:为了防止我们每次遍历的时候都要新建子数组存储i之前已经交换好的数字,我们对原数组进行修改之后,递归完成了要再修改回来。
vector<vector<int>> permute(vector<int>& nums) 
{
	vector<vector<int>> ans;
	backtracking(nums,0,ans);
	return ans;
}

void backtraking(vector<int>& nums,int level,vector<vector<int>>& ans)
{
	if(level==nums.size()-1)//如果跑完了
	{
		ans.push_back(nums);//现在的Nums进栈
		return;
	}
	for(int i=level;i<nums.size();++i)
	{
		swap(nums[i],nums[level]);//修改当前节点状态
		backtracking(nums,level+1,ans);//递归子节点
		swap(nums[i],nums[level]);//回改当前节点状态
	}
}

下面的代码可能好理解一些,但是其实一样的:

class Solution {
public:
    vector<vector<int>> permute(vector<int>& nums)
    {
        int s=nums.size();
        vector<vector<int>> ans;
        if(s<2)
        {
            ans.push_back(nums);
            return ans;
        }
        backtracking(nums,0,ans,s);
        return ans;
    }
    void backtracking(vector<int>& nums,int level,vector<vector<int>>& ans,int s)
    {
        if(level==s-1)
        {
            ans.push_back(nums);
            return ;
        }
        for(int i=level;i<s;++i)
        {
            swap(nums[i],nums[level]);//123,213,321 第一层
            backtracking(nums,level+1,ans,s);//123-132, 213-231, 321-312对应的后面递归
            swap(nums[i],nums[level]);//交换回来就很灵性,输出就变成123,132,213,231,321,312
        }
    }
};

77组合

在这里插入图片描述
组合不讲究顺序,如果四个数里取两个数,我们可以想象一下手写的顺序:
1,2
1,3
1,4
2,3
2,4
3,4
总共6种
但是如果是4个数里取3个数呢
1,2,3
1,2,4
1,3,4
2,3,4
总共4种
我们首先明确,每次递归结束的条件,应该是找到了count个不同的数,count==k。然后把本次找打的结果入栈。
讲道理应该需要如下几个指针,第一个指针指当前在哪,指到4就返回,第二个记录已经找了几个数,这个就是上面的count
所以是这样的:
1,2//开始1的递归
1,3
1,4
//结束递归,回溯1,往后走一步
2,3//开始2的递归
2,4
//结束递归,回溯2,往后走一步
3,4//开始2的递归,结束递归

class Solution {
public:
    vector<vector<int>> combine(int n, int k) 
    {
	//这一题应该不需要考虑特殊情况,没啥特殊的,因为它本身就没给我们一个数组,所以我们最好创建一个vector<int>的数组,每次循环终止就传给ans
    vector<int> nums(k,0);//里面最先初始化K个0
	vector<vector<int>> ans;
    int count=0; //代表已经找到了多少个数
    backtracking(ans,nums,count,1,n,k);
    return ans;
    }
    void backtracking(vector<vector<int>>& ans,vector<int>& nums,int &count,int first_pos,int n,int k)
    {
        //首先循环的终止条件很好确定,就是每次count==k
        if(count==k) 
        {
            ans.push_back(nums);
            return;
        }
        for(int i=first_pos;i<=n;++i) //i从1数到n,想输出的是[1,2],[1,3],[1,4],[2,3],[2,4],[3,4],所以对于每一个循环都应该跑到n结束
        {
            nums[count++]=i;//第一步,修改当前节点状态,找到了1个数所以赋值完count要加一,所以对应第三步要回退确定的count数量
            backtracking(ans,nums,count,i+1,n,k);//第二步,递归接下来的子节点
            --count;//第三步,回溯当前节点状态
        }
    }
};

注意:
在递归传参的时候,我们要传进去的参数firstpos是i+1,而不是++i,我在写这个的时候犯了一个沙比错误写成了i++。导致每个循环i自加了两次。
把i+1传进去,i本身的值没有变化。
但是一旦传++i进去,i本身加了一,下次for的时候i又要加一。
警惕不要犯我这样的低级失误。

79. 单词搜索

给定一个 m x n 二维字符网格 board 和一个字符串单词 word 。如果 word 存在于网格中,返回 true ;否则,返回 false 。
单词必须按照字母顺序,通过相邻的单元格内的字母构成,其中“相邻”单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母不允许被重复使用

在这里插入图片描述
在这里插入图片描述
注意:这里涉及到不能复用的问题,所以像示例3就是提醒我们,在找的时候,每次深度优先不能回头。所以我们最好在搜索之前先标记当前位置已经被访问,防止向左之后又向右。在完成搜索之后得回溯当前位置为未访问,防止干扰到之后的其他搜索。
代码:

class Solution {
public:
    bool exist(vector<vector<char>>& board, string word) 
    {
        //首先判空
        if(board.empty() || board[0].empty()) return false;
        int m=board.size(),n=board[0].size();//m行n列
        //创建一个访问数组,m行,每行是vector<bool>类型的n个false
        vector<vector<bool>> visited(m,vector<bool>(n,false));
        bool flag=false;
        for(int i=0;i<m;++i) //对每一个点都进行一次深度优先搜索,如果找到了就结束
        {
            for(int j=0;j<n;++j)
            {
                backtracking(i,j,board,word,flag,visited,0);
                if(flag) return flag;
            }
        }
        return flag;
    }
    vector<int> direction{-1,0,1,0,-1};
    //注意要引用传值,一开始写的时候函数漏了flag的引用符号,改bug改半天没看出来
    void backtracking(int i,int j,vector<vector<char>>& board,string& word,bool& flag,vector<vector<bool>>& visited,int pos)
    {
        //先看有没有找出界
        if(i<0 ||i>=board.size() || j<0 ||j>=board[0].size()) return;
        //再看有没有找过这个点,或者不匹配
        if(visited[i][j] ||board[i][j]!=word[pos]) return;
        //pos完成了匹配
        if(pos==word.size()-1) 
        {
            flag=true;
            return;
        }

        //board[i][j]==word[pos]匹配的情况,先把当前节点改为来过
        visited[i][j]=true;
        //递归当前节点
        for(int p=0;p<4;++p)
        {
            int x=i+direction[p],y=j+direction[p+1];
            backtracking(x,y,board,word,flag,visited,pos+1);
        }
        //回溯刚才来过记录
        visited[i][j]=false;

    }
};

注意要引用传值,一开始写的时候函数漏了flag的引用符号,改bug改半天没看出来,低级失误++。

51.N 皇后

n 皇后问题 研究的是如何将 n 个皇后放置在 n×n 的棋盘上,并且使皇后彼此之间不能相互攻击。

给你一个整数 n ,返回所有不同的 n 皇后问题 的解决方案(多解)

每一种解法包含一个不同的 n 皇后问题 的棋子放置方案,该方案中 ‘Q’ 和 ‘.’ 分别代表了皇后和空位。

1 <= n <= 9
皇后彼此不能相互攻击,也就是说:任何两个皇后都不能处于同一条横行、纵行或斜线上
在这里插入图片描述
因为每一行每一列都有且只能有一个Q,所以我们回溯函数里面的主体应该是一个for循环,遍历所有行即可,如果某行某列我放了Q之后接下来无解,就回溯。
注释写的非常详细:

class Solution {
public:
    vector<vector<string>> solveNQueens(int n) 
    {
        vector<vector<string>> ans;
        //先初始化一个一维的输出,由n个字符串组成,每个字符串是n个点,最后将所有borad入ans栈即可
        vector<string> board(n,string(n,'.'));
        //下面初始化记录数组,分别为列,左斜,和右斜,因为我们每行只有一个不必记录
        vector<bool> column(n,false),ldiag(2*n-1,false),rdiag(2*n-1,false);
        backtracking(ans,board,column,ldiag,rdiag,0,n);
        return ans;
    }
    void backtracking(vector<vector<string>>& ans,vector<string>& board,vector<bool>& column,vector<bool>& ldiag,vector<bool>& rdiag,int row,int n)
    {
        //我们考虑,因为不同于之前的深度优先搜索,我们不是四个方向,不会越界,循环终止的条件不太好确定可以先空着
        //最后一次row=3时,找到了最后一个Q,row又被加了一,此时等于n
        if(row==n)
        {
            ans.push_back(board);//此解入栈
            return;
        }
        //遍历第一行所有列
        for(int i=0;i<n;++i)
        {
            //被别的皇后占用,就不可以放
            //这里如何判断[row,i]这个点在哪个左斜行和右斜行
            //左斜行判断:从左下到右上标0123456左斜行的话,我们计算每个[i,j]的差i-j,发现是递减的,但是我们要斜行数递增,用j-i+4即可,所以左斜行即为j-i+n,在我们的[row,i]里是i-row+n-1
            //右斜行判断:从右下到左上标0123456,2*n-(row+i)-2
            if(column[i] || ldiag[i-row+n-1] ||rdiag[2*n-(row+i)-2])
            {
                continue;
            }
            //修改当前状态
            board[row][i]='Q';
            column[i] = ldiag[i-row+n-1] =rdiag[2*n-(row+i)-2]=true;
            //递归接下来所有行
            backtracking(ans,board,column,ldiag,rdiag,row+1,n);
            //回溯
            board[row][i]='.';
            column[i] = ldiag[i-row+n-1] =rdiag[2*n-(row+i)-2]=false;

        }
    }
};

广度优先搜索(breadth-first search,BFS)

广度优先搜索(breadth-first search,BFS)不同与深度优先搜索,它是一层层进行遍历的,因此需要用先入先出的队列而非先入后出的栈进行遍历。由于是按层次进行遍历,广度优先搜索时按照“广”的方向进行遍历的,也常常用来处理最短路径等问题。

例题:

934. 最短的桥(最短路径问题)

在给定的二维二进制数组 A 中,存在两座岛。(岛是由四面相连的 1 形成的一个最大组。)
现在,我们可以将 0 变为 1,以使两座岛连接起来,变成一座岛。
返回必须翻转的 0 的最小数目。(可以保证答案至少是 1 。)
在这里插入图片描述

分析:本题实际上是求两个岛屿之间的最短路径,我们只要先找到一个岛,然后通过广度优先搜索,找其与另一个岛的最短距离即可。

我们第一步明确了这个问题的核心是最短路径问题。
那么如何先找到第一个岛?
利用深度优先搜索即可。但凡找到1个1,就把与其所有相连的1并作一个岛,因为要区分两个岛,我们可以把我们找到的第一个岛上所有点都变成2。
然后把第一个岛周边一圈的0全部入队,开始bfs
level既代表层数,也代表与第一个岛屿的最短距离。
从第一个岛开始,遍历所有队列中的0元素,将队列中的0元素改成2,并判断其周边元素,如果是1说明已经找到了第二个岛,如果是2代表是之前的第一个岛,如果是0说明在下一层(即与第一个岛的距离加一)

class Solution {
public:
    int shortestBridge(vector<vector<int>>& A) 
    {
        int m=A.size(),n=A[0].size();
        queue<pair<int,int>> points;//创建一个队列points,里面每个元素是pair类型的数对
        //接下来需要用dfs找到第一个岛屿,为了其与第二个岛屿区分,我们把找到的点全都改成2,并且把与之相邻的0全部入队列
        bool flag=false; //找到第一个岛的标志
        for(int i=0;i<m;++i)
        {
            //如果已经找完了第一个岛,下面的for跳出了,第一个for也直接结束
            if(flag) break;
            for(int j=0;j<n;++j)
            {
                if(A[i][j]==1)
                {
                    dfs(A,points,m,n,i,j);
                    flag=true;
                    break;
                }
            }
        }
        //接下来需要用bfs找到第二个岛屿,将所有经过的0都变成2
        int x,y;
        int level=0;
        while(!points.empty()) //只要刚才找到的0队列非空
        {
            ++level;//层数即代表与第一个岛屿的距离
            int n_points=points.size();//记录points个数
            //对所有的Points
            while(n_points)
            {
                --n_points;
                //每次访问队首元素,先记录下来,再出队
                auto [r,c]=points.front();
                points.pop();
                //bfs
                for(int p=0;p<4;++p)//从这个0元素周边找一圈
                {
                    x=r+direction[p],y=c+direction[p+1];
                    //保证没有出界
                    if(x>=0 && x<m && y>=0 && y<n)
                    {
                        if(A[x][y]==2) continue;//说明找到的是第一个岛,不操作
                        if(A[x][y]==1) return level;//找到第二个岛,层数就是最短路径
                        //找到的是0,说明在下一层,比上一层的距离多1
                        points.push({x,y});//将下一层的0入队
                        A[x][y]=2;//扩展第一个岛屿
                    }
                }

                
            }
        }
        return level;
    }
    vector<int> direction{-1,0,1,0,-1};
    void dfs(vector<vector<int>>& A,queue<pair<int,int>>& points,int m,int n,int i,int j)
    {
        //由于可以上下左右找,先判断是否出界
        if(i<0 ||i>=m||j<0||j>=n) return;
        //已经来过
        if(A[i][j]==2) return;
        //如果这个点是0,我们将其加入队列
        if(A[i][j]==0) 
        {
            points.push({i,j});
            return;
        }

        //这个点是1,将其改为2,递归子节点
        A[i][j]=2;
        for(int p=0;p<4;++p)
        {
            int x=i+direction[p],y=j+direction[p+1];
            dfs(A,points,m,n,x,y);
        }
    }
};

126. 单词接龙 II(双向最短路径)

给定两个单词(beginWord 和 endWord)和一个字典 wordList,找出所有从 beginWord 到 endWord 的最短转换序列。转换需遵循如下规则:
每次转换只能改变一个字母
转换后得到的单词必须是字典中的单词(暗示最后一个endword必须在wordlist里)。
说明:
如果不存在这样的转换序列,返回一个空列表。
所有单词具有相同的长度。
所有单词只由小写字母组成。
字典中不存在重复的单词。
你可以假设 beginWord 和 endWord 是非空的,且二者不相同。

在这里插入图片描述
我们可以把起始字符串、终止字符串、以及单词表里所有的字符串想象成节点。若两个字符串只有一个字符不同,那么它们相连。因为题目需要输出修改次数最少的所有修改方式,因此我们可以使用广度优先搜索,求得起始节点到终止节点的最短距离

我们同时还使用了一个小技巧:我们并不是直接从起始节点进行广度优先搜索,直到找到终止节点为止;
而是从起始节点和终止节点分别进行广度优先搜索,每次只延展当前层节点数最少的那一端,这样我们可以减少搜索的总结点数。举例来说,假设最短距离为 4,如果我们只从一端搜索 4 层,总遍历节点数最多是 1 + 2 + 4 + 8 + 16 = 31;而如果我们从两端各搜索两层,总遍历节点数最多只有 2 × (1 + 2 + 4) = 14。

还有就是,他输出所有可能,这样的话我们需要用到回溯法

哦豁,太难了。。。最后没写出来。。。下次再议。

练习

130 被围绕区域

在这里插入图片描述

class Solution {
public:
    void solve(vector<vector<char>>& board) 
    {
        //题中提示到,所有不在边界上,且不与边界上o相连的o都会被填充
        //言下之意,我们就从边界上找,哪些o与这些o相连,给一个标记,然后便利整个数组,把所有没有被标记的o改成x即可
        int m=board.size(),n=board[0].size();
        if(m<=1) return;
        vector<vector<bool>> flag(m,vector<bool>(n,false));
        //边界拓展标记,深度优先
        //第一行与最后一行
        for(int i=0;i<n;++i)
        {
            dfs(board,m,n,flag,0,i);
            dfs(board,m,n,flag,m-1,i);
        }
        //第一列与最后一列
        for(int j=1;j<m-1;++j)
        {
            dfs(board,m,n,flag,j,0);
            dfs(board,m,n,flag,j,n-1);
            
        }       
        for(int i=0;i<m;++i)
        {
            for(int j=0;j<n;++j)
            {
                if(board[i][j]=='O' && flag[i][j]==false)
                {
                    board[i][j]='X';
                }
            }
        }

    }
    vector<int> direction{-1,0,1,0,-1};
    int x,y;
    void dfs(vector<vector<char>>& board,int m,int n,vector<vector<bool>>& flag,int i,int j)
    {
        if(i<0 ||i>=m ||j<0 ||j>=n|| flag[i][j]==true||board[i][j]=='X')//越界或者来过,或者是x,都直接返回
        {
            return;
        }
        //下面就是找到的是O的情况
        flag[i][j]=true;
        for(int p=0;p<4;++p)
        {
            x=i+direction[p],y=j+direction[p+1];
            dfs(board,m,n,flag,x,y);
        }
        
        
    }
};

47排序(含重复数,对比46)

在这里插入图片描述
之前无重复数,我们采取的是回溯法,将每个数依次交换,然后递归,最后改回来。
我们想一下应该只多了一个判定,每个数和自己还是要交换的,但是如果数和别的数交换的时候,这两个数不能相等。入栈之前检查一下栈里面是不是已经有这个元素了。

class Solution {
public:
    vector<vector<int>> permuteUnique(vector<int>& nums) 
    {
        vector<vector<int>> ans;//不断修改nums得到结果就入ans栈
        //过小的情况
        if(nums.size()<2)
        {
            ans.push_back(nums);
            return ans;
        }
        backtracking(ans,nums,0);
        return ans;
    }
    void backtracking(vector<vector<int>>& ans,vector<int>& nums,int flag)
    {
        //终止条件为如果跑到最后一位
        if(flag==nums.size()-1)
        {
            int n;
            n=count(ans.begin(),ans.end(),nums);
            if(n==0)
            {
            ans.push_back(nums);
            return;
            }
        }
        for(int i=flag;i<nums.size();++i)
        {
            if(i==flag || nums[i]!=nums[flag])
            {
                swap(nums[i],nums[flag]);//修改当前节点
                backtracking(ans,nums,flag+1);//递归子节点
                swap(nums[flag],nums[i]);//回改当前状态
            }
        }
    }
};

40组合(含重复数,对比77)

在这里插入图片描述

class Solution {
public:
    vector<vector<int>> combinationSum2(vector<int>& candidates, int target) 
    {
        sort(candidates.begin(),candidates.end());
        vector<vector<int>> ans;
        vector<int> oneof_ans;
        int c=0;//来记录当前入oneof_ans栈数的和
        int flag=0;//当前位置
        backtracking(ans,oneof_ans,c,flag,candidates,target);
        return ans;
    }
    void backtracking(vector<vector<int>>& ans,vector<int>& oneof_ans,int c,int flag,vector<int>& candidates, int target)
    {
        //结束条件:如果c正好等于target或者flag跑完了整个数组都没有找到,或者这个数就等于target
        if(c==target || flag>=candidates.size())
        {
            if(c==target)
            {
                int n=count(ans.begin(),ans.end(),oneof_ans);
                if(n==0)
                {
                    ans.push_back(oneof_ans);
                    return;
                }
            }
            return;
        }
        
        for(int i=flag;i<candidates.size();++i)
        {
            if(candidates[i]>target || c>target) continue;
            //修改当前状态,入栈,求和
            oneof_ans.push_back(candidates[i]);
            c+=candidates[i];
            //递归子节点
            backtracking(ans,oneof_ans,c,i+1,candidates,target);
            //回溯
            oneof_ans.pop_back();
            c-=candidates[i];
        }
    }
};

37 数独(对比51N皇后)

在这里插入图片描述
在这里插入图片描述
这题讲道理和N皇后问题应该是一样一样的啊。注意flag在选填数字1-9时要发挥作用,因为即使最后一个for跑完了,flag为true了,这时候返回上一层,上一层的for还没有结束,所以要让flag在这里使所有上层for结束。

class Solution {  
public:
    vector<pair<int,int>> points;//需要填的点
    bool flag=false;
    bool line[9][10];
    bool column[9][10];
    bool box[3][3][10];
    void solveSudoku(vector<vector<char>>& board) 
    {
        //扫描记录每行每列每格出现了哪些数,把需要填写的位置记录下来
        for(int i=0;i<9;++i)
        {
            for(int j=0;j<9;++j)
            {
                if(board[i][j]=='.')   points.emplace_back(i,j);
                else 
                {
                    int num=board[i][j]-'0';
                    line[i][num]=column[j][num]=box[i/3][j/3][num]=true;
                }
            }
        } 
        backtracking(board,flag,0);            
    }
    
    void backtracking(vector<vector<char>>& board,bool& flag,int n)//n为points的指针
    {
        if(n==points.size())//终止条件
        {
            flag=true;
            return;
        }
        auto[i,j]=points[n];//接收每一个需要填的点
        for(int x=1;x<=9 && !flag;++x) //注意这里的!flag,要防止已经找到正确解之后,返回上一层的for继续跑
        {
            if(!line[i][x] && !column[j][x] && !box[i/3][j/3][x]) //如果都没有出现过
            {
                //修改
                board[i][j]=x+'0';
                line[i][x]=column[j][x]=box[i/3][j/3][x]=true;
                cout<<"在"<<i<<' '<<j<<"填入"<<board[i][j]<<endl;
                //递归子节点
                backtracking(board,flag,n+1);
                //回溯
                //board[i][j]='.';
                line[i][x]=column[j][x]=box[i/3][j/3][x]=false;
                cout<<"在"<<i<<' '<<j<<"回填"<<board[i][j]<<endl;
            }
        }
        
       
        
    }
};
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值