本文参考:
深度优先搜索
深度优先遍历中,大量的使用了“递归”这种思想,我之前的一篇文章中也谈到了一些自己的理解,希望能带给你一些帮助。引路->算法学习-递归思想
「一条路走到底,不撞南墙不回头」是对「深度优先遍历」的最直观描述。对应着「递归」来说,走到底的意思就是递归栈不断开拓下去,南墙即递归的终止条件或者称为base情况
,回头就是递归栈返回,return
回来。一般都是走到底递归栈return回来,但也会碰到需要「回溯」的情况。
但在刷题的过程中也会出现这样一种情况,没有很明显的return的base情况
,反倒希望dfs把节点可行的方向都执行一遍,在外部定义一个全局变量,dfs内部通过访问数组进行标记从而避免重复访问,最后递归栈还是会自动return,不过是全部执行了一遍以后。参考题目6139.受限条件下可到达节点的数目。
一些心得:
vis
不一定是必须的,可以通过「污染」原来地图上的数据,达到不重复访问的目的。以下题目类型只是根据该题目最浅显的特征进行区分,有些题目可能是多种类型的结合,需要灵活运用。同时搜索问题本质上是一样的,有些题目也可以通过广度优先遍历进行求解。
隐式是指一下子分辨不出来它究竟是属于图、树、还是二维平面上的搜索问题,这种情况本质上还是用DFS这种搜索方法去遍历所有的情况,触底了就将符合条件的结果收集起来。
直观的,多种划分情况暴力递归,将接下来的情况交给子递归去做,base情况为划分点能够到达数组末尾。
下面做法是过不了的,TLE但方法没错,原因是暴力递归想象成树,单节点分支为3,层数O(N)
,复杂度O(3^N)
,N为20就接近1*10^9
。可以尝试在这个上面加记忆化。
class Solution {
public boolean validPartition(int[] nums) {
return dfs(nums,0);
}
//表示从index(包括index),能够划分nums
public boolean dfs(int[]nums,int index){
if(index==nums.length) return true;
int len=nums.length;
boolean ans1=false;
boolean ans2=false;
boolean ans3=false;
if(index<=len-2&&nums[index]==nums[index+1]){
if(dfs(nums,index+2)) ans1=true;
}
if(index<=len-3&&nums[index]==nums[index+1]&&nums[index+1]==nums[index+2]){
if(dfs(nums,index+3)) ans2=true;
}
if(index<=len-3&&nums[index]+1==nums[index+1]&&nums[index+1]+1==nums[index+2]){
if(dfs(nums,index+3)) ans3=true;
}
return ans1||ans2||ans3;
}
}
由于搜索状态就是「当前划分序号是否满足要求」,会出现很多子状态重复计算,可以进行记忆化DFS,通过数组m
进行状态标记。
class Solution {
int[]m;
public boolean validPartition(int[] nums) {
int len=nums.length;
m=new int[len+1];
return dfs(nums,0);
}
//表示从index(包括index),能够划分nums
public boolean dfs(int[]nums,int index){
//如果状态已经被保存,直接返回
if(m[index]==1) return true;
if(m[index]==-1) return false;
if(index==nums.length) return true;
int len=nums.length;
boolean ans1=false;
boolean ans2=false;
boolean ans3=false;
if(index<=len-2&&nums[index]==nums[index+1]){
if(dfs(nums,index+2)) ans1=true;
}
if(index<=len-3&&nums[index]==nums[index+1]&&nums[index+1]==nums[index+2]){
if(dfs(nums,index+3)) ans2=true;
}
if(index<=len-3&&nums[index]+1==nums[index+1]&&nums[index+1]+1==nums[index+2]){
if(dfs(nums,index+3)) ans3=true;
}
//记录状态
m[index]=ans1||ans2||ans3?1:-1;
return ans1||ans2||ans3;
}
}
由于状态结果是唯一的,所以记忆化搜索其实可以改为「动态规划」解法。状态定义dp[index]
表示是否能到达数组边界index
,由后面的状态往前推导。
class Solution {
public boolean validPartition(int[] nums) {
int len=nums.length;
//dp[index]代表数组边界到index开始,是否可以正确划分
boolean[]dp=new boolean[len+1];
dp[len]=true;
for(int index=len-1;index>=0;index--){
if(index<=len-2&&nums[index]==nums[index+1]) dp[index]|=dp[index+2];
if(index<=len-3&&nums[index]==nums[index+1]&&nums[index+1]==nums[index+2]){
dp[index]|=dp[index+3];
}
if(index<=len-3&&nums[index]+1==nums[index+1]&&nums[index+1]+1==nums[index+2]){
dp[index]|=dp[index+3];
}
}
return dp[0];
}
}
参考宫水三叶的题解,s
可以被可恰好划分为多个特殊的子串item
,当且仅当某个子串满足1和0个数相等就可以,因为原始序列s
可以保证第二个性质。第二个性质也表明,特殊子串item
必然为1...0
的模式。
因此可将本题构造分两步进行:1.对每个item
进行重排,使其本身调整为字典序最大;2.对不同item
之间进行重排,使最终s
整体字典序最大。对于1,我们可以对 item
中的 1...0
中的非边缘部分进行调整(递归处理子串部分),同时用list
将该子串进行收集。对于2,我们利用对list
进行重排,重排参考179.最大数,最终组合出结果。
这里在一个DFS递归中,调用了多次递归,并将每次的结果收集起来,
class Solution {
public String makeLargestSpecial(String s) {
int len=s.length();
return dfs(s,0,len-1);
}
public String dfs(String s,int start,int end){
if(start>end) return "";
ArrayList<String> list=new ArrayList<>();
int count=0;
int split=start;
for(int i=start;i<=end;i++){
if(s.charAt(i)=='1') count++;
else count--;
if(count==0){
//步骤一,item本身重排
list.add("1"+dfs(s,split+1,i-1)+"0");
split=i+1;
}
}
//步骤二,item整体重排
//对ArrayList调用Collections.sort()
Collections.sort(list,(a,b)->(b+a).compareTo(a+b));
StringBuilder sb=new StringBuilder();
for(String ss:list){
sb.append(ss);
}
return sb.toString();
}
}
其中递归应该把握的点,从上往下分析问题:
return
。其中判断错误的点应该把握:
参考笨猪爆破组的题解,其中将设置边界条件以及递推关系讲的很清楚。先将当前不能继续递归的条件进行return false
。其中需要考虑单词复原的回溯处理。
class Solution {
int n,m;
public boolean exist(char[][] board, String word) {
n=board.length;
m=board[0].length;
boolean[][] vis=new boolean[n][m];
for(int i=0;i<n;i++){
for(int j=0;j<m;j++){
if(dfs(board,vis,word,i,j,0)) return true;
}
}
return false;
}
public boolean dfs(char[][] board,boolean[][] vis,String word,int i,int j,int idx){
//越界或者已经访问过
if(i<0||i>=n||j<0||j>=m||vis[i][j]==true) return false;
//如果不匹配
if(word.charAt(idx)!=board[i][j]) return false;
//匹配到最后一个了,上面没有return false,就说明当前是正确的
if(idx==word.length()-1) return true;
//继续递归
vis[i][j]=true;
boolean res=dfs(board,vis,word,i+1,j,idx+1)||dfs(board,vis,word,i-1,j,idx+1)||dfs(board,vis,word,i,j-1,idx+1)||dfs(board,vis,word,i,j+1,idx+1);
if(res) return true;
//回溯
vis[i][j]=false;
return false;
}
}
从海洋的四周出发逆向DFS标记可以访问到的格子,递归下去的条件是heights
大于等于四周格子,将两种海洋访问格子的交集返回。可以设置不同的全局数组分别进行可访问标记,递归函数对两种数组进行标记,同时只需要返回void。由于不断递归下去将数组进行标记,不需要额外的回溯操作。参考题解水往高处流
class Solution {
int m,n;
public List<List<Integer>> pacificAtlantic(int[][] heights) {
m=heights.length;
n=heights[0].length;
boolean[][] reachPac=new boolean [m][n];
boolean[][] reachAtl=new boolean [m][n];
for(int i=0;i<m;i++){
dfs(heights,reachPac,i,0);
dfs(heights,reachAtl,i,n-1);
}
for(int i=0;i<n;i++){
dfs(heights,reachPac,0,i);
dfs(heights,reachAtl,m-1,i);
}
List<List<Integer>> ans=new ArrayList<>();
for(int i=0;i<m;i++){
for(int j=0;j<n;j++){
if(reachPac[i][j]&&reachAtl[i][j]){
ans.add(Arrays.asList(i,j));
}
}
}
return ans;
}
public void dfs(int[][]heights,boolean[][]canReach,int i,int j){
//已经标记直接return
if(canReach[i][j]) return;
//进行标记并判断能否继续搜索
canReach[i][j]=true;
if(i-1>=0&&heights[i-1][j]>=heights[i][j]) dfs(heights,canReach,i-1,j);
if(i+1<=m-1&&heights[i+1][j]>=heights[i][j]) dfs(heights,canReach,i+1,j);
if(j-1>=0&&heights[i][j-1]>=heights[i][j]) dfs(heights,canReach,i,j-1);
if(j+1<=n-1&&heights[i][j+1]>=heights[i][j]) dfs(heights,canReach,i,j+1);
return;
}
}
以下是岛屿问题,参考了以下优秀题解:
岛屿类问题的通用解法、DFS 遍历框架
一套模板,解决五个岛屿问题
核心是:
class Solution {
int m,n;
public int numIslands(char[][] grid) {
int ans=0;
m=grid.length;
n=grid[0].length;
for(int i=0;i<m;i++){
for(int j=0;j<n;j++){
//这句很妙,碰到陆地就从它开始搜索,将这片区域变成已访问
if(grid[i][j]=='1'){
dfs(grid,i,j);
ans++;
}
}
}
return ans;
}
public void dfs(char[][]grid,int i,int j){
//把越界定义在前面,防止下面数组下标访问出错
if(i<0||i>=m||j<0||j>=n) return;
if(grid[i][j]!='1') return;
grid[i][j]='2';
dfs(grid,i+1,j);
dfs(grid,i-1,j);
dfs(grid,i,j+1);
dfs(grid,i,j-1);
// int[]dx={1,0,-1,0};
// int[]dy={0,1,0,-1};
// for(int k=0;k<4;k++){
// dfs(grid,i+dx[k],j+dy[k]);
// }
}
}
注意dfs函数定义的返回值为int,以及结束条件的返回值
class Solution {
int m,n;
public int maxAreaOfIsland(int[][] grid) {
m=grid.length;
n=grid[0].length;
int ans=0;
for(int i=0;i<m;i++){
for(int j=0;j<n;j++){
if(grid[i][j]==1){
int res=dfs(grid,i,j);
ans=Math.max(ans,res);
}
}
}
return ans;
}
public int dfs(int[][]grid,int i,int j){
if(i<0||i>=m||j<0||j>=n) return 0;
if(grid[i][j]!=1) return 0;
grid[i][j]=2;
return 1+dfs(grid,i-1,j)+dfs(grid,i+1,j)+dfs(grid,i,j-1)+dfs(grid,i,j+1);
}
}
充分利用了终止条件,碰到海洋、陆地、已访问过的节点返回值都不一样。
class Solution {
int m,n;
public int islandPerimeter(int[][] grid) {
m=grid.length;
n=grid[0].length;
for(int i=0;i<m;i++){
for(int j=0;j<n;j++){
if(grid[i][j]==1) return dfs(grid,i,j);
}
}
return 0;
}
public int dfs(int[][]grid,int i,int j){
if(i<0||i>=m||j<0||j>=n) return 1;
if(grid[i][j]==0) return 1;
if(grid[i][j]==2) return 0;
grid[i][j]=2;
//grid[i][j]=1 本次不记录,继续往下搜索
return dfs(grid,i+1,j)+dfs(grid,i-1,j)+dfs(grid,i,j+1)+dfs(grid,i,j-1);
}
}
参考岛屿问题DFS没有那么难,独立写出来的一次hard,这道题实际上是对网格做了两遍遍历:第一遍 DFS 遍历陆地格子,计算每个岛屿的面积并标记岛屿序号;第二遍直接循环遍历,找到海洋格子,观察每个海洋格子相邻的陆地格子,将对应岛屿序号的面积加起来。
class Solution {
int n;
public int largestIsland(int[][] grid) {
n=grid.length;
int index=2;
HashMap<Integer,Integer> indexMap=new HashMap<>();
//第一次遍历
int maxArea=0;
for(int i=0;i<n;i++){
for(int j=0;j<n;j++){
if(grid[i][j]==1){
int area=dfs(grid,i,j,index);
indexMap.put(index++,area);
maxArea=Math.max(maxArea,area);
}
}
}
//将ans初始化为上面最大的陆地面积,可以避免无海洋不能填海造陆的情况
int ans=maxArea;
//第二次遍历
for(int i=0;i<n;i++){
for(int j=0;j<n;j++){
if(grid[i][j]==0){
int size=1;
HashSet<Integer> set=new HashSet<>();
if(inArea(i-1,j)&&grid[i-1][j]!=0)set.add(grid[i-1][j]);
if(inArea(i+1,j)&&grid[i+1][j]!=0)set.add(grid[i+1][j]);
if(inArea(i,j-1)&&grid[i][j-1]!=0)set.add(grid[i][j-1]);
if(inArea(i,j+1)&&grid[i][j+1]!=0)set.add(grid[i][j+1]);
if(!set.isEmpty()){
for(int k:set){
size+=indexMap.get(k);
}
}
ans=Math.max(size,ans);
}
}
}
return ans;
}
public int dfs(int[][]grid,int i,int j,int index){
if(!(inArea(i,j))) return 0;
if(grid[i][j]!=1) return 0;
grid[i][j]=index;
return 1+dfs(grid,i+1,j,index)+dfs(grid,i-1,j,index)+dfs(grid,i,j+1,index)+dfs(grid,i,j-1,index);
}
public boolean inArea(int i,int j){
if(i<0||i>=n||j<0||j>=n) return false;
return true;
}
}
从边界开始,将与边界相连的陆地1都标记出来,最后剩下的就是无法离开的陆地。
class Solution {
int m,n;
public int numEnclaves(int[][] grid) {
m=grid.length;
n=grid[0].length;
for(int i=0;i<m;i++){
dfs(grid,i,0);
dfs(grid,i,n-1);
}
for(int i=0;i<n;i++){
dfs(grid,0,i);
dfs(grid,m-1,i);
}
int res=0;
for(int i=0;i<m;i++){
for(int j=0;j<n;j++){
if(grid[i][j]==1) res++;
}
}
return res;
}
public void dfs(int[][]grid,int i,int j){
if(i<0||i>=m||j<0||j>=n||grid[i][j]==0||grid[i][j]==2) return;
grid[i][j]=2;
dfs(grid,i+1,j);
dfs(grid,i-1,j);
dfs(grid,i,j-1);
dfs(grid,i,j+1);
}
}
与岛屿问题-200.岛屿数量的区别是,封闭岛屿的充要条件是四周都是水域,而本题没有假设网格四条边均被水包围,因此需要考虑未封闭的岛屿。函数返回结果为四个方向都满足封闭。两种方法解决:
class Solution {
int m,n;
public int closedIsland(int[][] grid) {
m=grid.length;
n=grid[0].length;
int ans=0;
for(int i=0;i<m;i++){
for(int j=0;j<n;j++){
if(grid[i][j]==0&&dfs(grid,i,j))ans++;
}
}
return ans;
}
public boolean dfs(int[][]grid,int i,int j){
if(i<0||i>=m||j<0||j>=n) return false;
//本来就是水域或者已经被访问标记
if(grid[i][j]==1) return true;
//grid[i][j]==0
grid[i][j]=1;
//&和&&结果一致,但是&判断了四个方向的封闭性,不存在短路规则
return dfs(grid,i-1,j)&dfs(grid,i+1,j)&dfs(grid,i,j-1)&dfs(grid,i,j+1);
}
}
class Solution {
int m,n;
public int closedIsland(int[][] grid) {
m=grid.length;
n=grid[0].length;
for(int i=0;i<m;i++){
preDfs(grid,i,0);
preDfs(grid,i,n-1);
}
for(int i=0;i<n;i++){
preDfs(grid,0,i);
preDfs(grid,m-1,i);
}
int ans=0;
for(int i=0;i<m;i++){
for(int j=0;j<n;j++){
if(grid[i][j]==0){
dfs(grid,i,j);
ans++;
}
}
}
return ans;
}
public void dfs(int[][]grid,int i,int j){
if(i<0||i>=m||j<0||j>=n||grid[i][j]==1) return;
grid[i][j]=1;
preDfs(grid,i+1,j);
preDfs(grid,i-1,j);
preDfs(grid,i,j-1);
preDfs(grid,i,j+1);
}
public void preDfs(int[][]grid,int i,int j){
//本来就是水域,或者已经被访问标记了
if(i<0||i>=m||j<0||j>=n||grid[i][j]==1) return;
grid[i][j]=1;
preDfs(grid,i+1,j);
preDfs(grid,i-1,j);
preDfs(grid,i,j-1);
preDfs(grid,i,j+1);
}
}
类似于 岛屿问题-1254.统计封闭岛屿的数目中的两种岛屿,但是这里不是统计数量,而需要改变原数组。
先将边界延伸的O替换成N,然后循环深搜替换掉所有被围绕的O,最终将N还原。
class Solution {
int m,n;
public void solve(char[][] board) {
m=board.length;
n=board[0].length;
for(int i=0;i<m;i++){
Ndfs(board,i,0);
Ndfs(board,i,n-1);
}
for(int i=0;i<n;i++){
Ndfs(board,0,i);
Ndfs(board,m-1,i);
}
for(int i=0;i<m;i++){
for(int j=0;j<n;j++){
if(board[i][j]=='O') dfs(board,i,j);
}
}
for(int i=0;i<m;i++){
for(int j=0;j<n;j++){
if(board[i][j]=='N') board[i][j]='O';
}
}
}
public void Ndfs(char[][]board,int i,int j){
if(i<0||i>=m||j<0||j>=n||board[i][j]=='X'||board[i][j]=='N') return;
board[i][j]='N';
Ndfs(board,i+1,j);
Ndfs(board,i-1,j);
Ndfs(board,i,j-1);
Ndfs(board,i,j+1);
}
public void dfs(char[][]board,int i,int j){
if(i<0||i>=m||j<0||j>=n||board[i][j]=='X'||board[i][j]=='N') return;
board[i][j]='X';
dfs(board,i+1,j);
dfs(board,i-1,j);
dfs(board,i,j-1);
dfs(board,i,j+1);
}
}
这题主要是边界的判断条件:
此外,由于题目中color标记较多,在原数组上修改会造成混淆,因此采用vis数组的方式标记访问。
class Solution {
int m,n;
public int[][] colorBorder(int[][] grid, int row, int col, int color) {
m=grid.length;
n=grid[0].length;
boolean[][] vis=new boolean[m][n];
dfs(grid,vis,row,col,color,grid[row][col]);
return grid;
}
public void dfs(int[][]grid,boolean[][]vis,int i,int j,int color,int curColor){
//越界或者不是当前着色或者已访问,不作为当前考虑的点
if(i<0||i>=m||j<0||j>=n||grid[i][j]!=curColor||vis[i][j]) return;
//在矩阵边界上为边界
if(i==0||i==m-1||j==0||j==n-1) grid[i][j]=color;
//相邻存在不同的连通分量为边界
if(diff(grid,vis,i-1,j,curColor)||diff(grid,vis,i+1,j,curColor)||diff(grid,vis,i,j-1,curColor)||diff(grid,vis,i,j+1,curColor)) grid[i][j]=color;
vis[i][j]=true;
dfs(grid,vis,i-1,j,color,curColor);
dfs(grid,vis,i+1,j,color,curColor);
dfs(grid,vis,i,j-1,color,curColor);
dfs(grid,vis,i,j+1,color,curColor);
}
//需要标记访问,不然已经修改了颜色条件判断会出错,出现在此修改
public boolean diff(int[][]grid,boolean[][]vis,int i,int j,int curColor){
if(i<0||i>=m||j<0||j>=n||vis[i][j]) return false;
if(grid[i][j]!=curColor) return true;
return false;
}
}
上来先判断是否点到雷,点到可以直接返回。
否则八个方向进行搜索,终止条件为越界或者非E(B或者有雷)
,遍历E的过程中计算八个方向雷的数量,有雷则显示数量停止搜索,没有雷则置B继续搜索。
class Solution {
int n,m;
public char[][] updateBoard(char[][] board, int[] click) {
m=board.length;
n=board[0].length;
if(board[click[0]][click[1]]=='M'){
board[click[0]][click[1]]='X';
return board;
}else{
dfs(board,click[0],click[1]);
}
return board;
}
public void dfs(char[][] board,int i,int j){
if(i<0||i>=m||j<0||j>=n) return;
if(board[i][j]!='E') return;
int count=calMine(board,i,j);
if(count!=0){
board[i][j]=(char)('0'+count);
return;
}else{
board[i][j]='B';
int[] dx={-1,-1,0,1,1,1,0,-1};
int[] dy={0,1,1,1,0,-1,-1,-1};
//注意是8个方向
for(int k=0;k<8;k++){
dfs(board,i+dx[k],j+dy[k]);
}
}
}
public int calMine(char[][] board,int i,int j){
int[] dx={-1,-1,0,1,1,1,0,-1};
int[] dy={0,1,1,1,0,-1,-1,-1};
int count=0;
for(int k=0;k<8;k++){
if(i+dx[k]>=0&&i+dx[k]<m&&j+dy[k]>=0&&j+dy[k]<n&&board[i+dx[k]][j+dy[k]]=='M')count++;
}
return count;
}
}
剑指Offer13.机器人的运动范围
简简单单DFS,注意vis数组,数位之和,函数定义如何返回格子数量
class Solution {
public int movingCount(int m, int n, int k) {
boolean[][] vis=new boolean[m][n];
return dfs(vis,0,0,m,n,k);
}
public int dfs(boolean[][]vis,int i,int j,int m,int n,int k){
if(i<0||i>=m||j<0||j>=n||vis[i][j]||cal(i)+cal(j)>k) return 0;
vis[i][j]=true;
return 1+dfs(vis,i-1,j,m,n,k)+dfs(vis,i+1,j,m,n,k)+dfs(vis,i,j-1,m,n,k)+dfs(vis,i,j+1,m,n,k);
}
public int cal(int k){
int res=0;
while(k>0){
res+=k%10;
k/=10;
}
return res;
}
}
参考题解DFS克隆图,深刻理解java对于引用类型的值拷贝,需要建立原图的克隆图,理论上要遍历团图的同时创建和其所有节点的克隆,值一样,节点已经不是原图的引用了,要重新创建。DFS遍历原图所有节点,其中子问题是DFS遍历其所有儿子节点,在这个过程中克隆。通过
为什么要用Map标记访问?
可以试着考虑只有两个点的情况。第一个点克隆之后如果不被添加到visited就直接开始克隆第二个点,则第二个点的克隆方法中会再次调用方法(创建克隆点,等待第一个点的引用加入)克隆第一个点,始终没有点被加入到visited中return,出现死循环。
/*
// Definition for a Node.
class Node {
public int val;
public List neighbors;
public Node() {
val = 0;
neighbors = new ArrayList();
}
public Node(int _val) {
val = _val;
neighbors = new ArrayList();
}
public Node(int _val, ArrayList _neighbors) {
val = _val;
neighbors = _neighbors;
}
}
*/
class Solution {
//整道题就是用DFS复制已经存在的图
//邻接表输出根据Node指针自动创建
public Node cloneGraph(Node node) {
HashMap<Node,Node> lookup=new HashMap<>();
return dfs(node,lookup);
}
public Node dfs(Node node, HashMap<Node,Node>lookup){
//如果当前节点为空,则创建空节点,
if(node==null) return null;
//如果已经被访问过克隆过,直接返回它的克隆
if(lookup.containsKey(node)) return lookup.get(node);
//没有被访问过,创建克隆节点
Node clone =new Node(node.val,new ArrayList<Node>());
//clone目前为空,后面通过引用指针往其中放入节点
lookup.put(node,clone);
for(Node n:node.neighbors) clone.neighbors.add(dfs(n,lookup));
return clone;
}
}
DFS统计连通域数量,从每个未访问过的节点进行搜索,通过“污染”的方式对和它相连且未被访问过的点进行标记,最后执行DFS的次数就是连通域个数。相比于二维平面上的搜索题目往上下左右进行搜索标记,如果已被访问则不搜索,这里是对相邻且未被访问的节点进行搜索标记,需要理解DFS的搜索路线。
class Solution:
def findCircleNum(self, isConnected: List[List[int]]) -> int:
vis=set()
res=0
# 从某个节点开始将与其连接的所有点都进行访问
def dfs(i):
for j in range(len(isConnected[i])):
if j not in vis and isConnected[i][j]==1:
vis.add(j)
dfs(j)
# 从每个未被访问过的节点开始进行搜索
for i in range(len(isConnected)):
if i not in vis:
vis.add(i)
dfs(i)
res +=1
return res
DFS或者BFS找连通分量,一组单词就是相似的所有单词组成的一个连通分量。所以本质上就是上面的省份数量,只不过这里的isConnected是相似字符串的判断,相似则connect.
DFS查找连通分量个数,时间复杂度 O ( N 2 ) O(N^2) O(N2),循环dfs N次,每次都尝试标记所有的N个节点。
class Solution {
HashSet<Integer> vis=new HashSet<>();
public int numSimilarGroups(String[] strs) {
int ans=0;
for(int i=0;i<strs.length;i++){
if(!vis.contains(i)){
vis.add(i);
dfs(i,strs);
ans++;
}
}
return ans;
}
public void dfs(int k,String[] strs){
for(int i=0;i<strs.length;i++){
// 从前往后检查后面未被访问过的单词i
if(!vis.contains(i)&&isConnect(strs[k],strs[i])){
vis.add(i);
dfs(i,strs);
}
}
}
public boolean isConnect(String a, String b){
int count=0;
for(int i=0;i<a.length();i++){
if(a.charAt(i)!=b.charAt(i)) count++;
}
return count<=2; // 必定成立,必然有0,2,4...个位置不同
}
}
BFS也可以解决连通数量问题,时间复杂度 O ( N 2 ) O(N^2) O(N2),循环bfs N次,每次在一次一次出队入队的过程中都尝试标记所有的N个节点。
class Solution {
HashSet<Integer> vis=new HashSet<>();
public int numSimilarGroups(String[] strs) {
int ans=0;
for(int i=0;i<strs.length;i++){
if(!vis.contains(i)){
bfs(i,strs);
ans++;
}
}
return ans;
}
public void bfs(int k,String[] strs){
Deque<Integer> que=new ArrayDeque<>();
vis.add(k);
que.offer(k);
while(!que.isEmpty()){
int top=que.poll();
// 从前往后检查后面未被访问过的单词i
for(int i=0;i<strs.length;i++){
if(!vis.contains(i)&&isConnect(strs[top],strs[i])){
vis.add(i);
que.offer(i);
}
}
}
}
public boolean isConnect(String a, String b){
int count=0;
for(int i=0;i<a.length();i++){
if(a.charAt(i)!=b.charAt(i)) count++;
}
return count<=2; // 必定成立,必然有0,2,4...个位置不同
}
}
用DFS遍历+回溯,将所有从根节点到叶子节点的路径收集起来,从根节点到叶子节点的序列长度就是树的高度,分支就是看当前元素是否有大小写两种可能,如果不能则继续往下搜索,如果能则有另一条搜索选择。
class Solution:
def letterCasePermutation(self, s: str) -> List[str]:
l=list(s)
ans=[]
def dfs(i):
if i== len(s):
ans.append(''.join(l))
return
# 保持原样
dfs(i+1)
# 可以转换大小写
if l[i].isalpha():
l[i]=chr(ord(l[i])^32)
dfs(i+1)
# 转换回来
l[i]=chr(ord(l[i])^32)
dfs(0)
return ans
深度优先搜索(回溯),采用全局path记录路径,因此需要回溯来撤销影响,所有节点的相邻节点都已经给出,所以不需要建图。题目保证有向无环图(DAG),因此也不需要考虑节点重复访问出现死循环的问题,不用标记访问。
class Solution {
LinkedList<Integer>path;
List<List<Integer>> res;
int n;
int[][] graph;
public List<List<Integer>> allPathsSourceTarget(int[][] g) {
n=g.length-1;
path=new LinkedList<>();
res=new ArrayList<>();
graph=g;
path.add(0);
backTracing(0);
return res;
}
public void backTracing(int cur){
if(cur==n){
res.add(new ArrayList<>(path));
return;
}
for(int i:graph[cur]){
path.add(i);
backTracing(i);
path.removeLast();
}
}
}
深度优先遍历,“一条路径可以多次包含同一条道路”相当于遍历一个连通分量的所有边,找到其中的最小值,不同于要找到从起点到终点的最短路径,比如test case2。图中需要标记访问防止重复访问死循环。
class Solution {
boolean[] vis;
int ans=0x3f3f3f3f;
ArrayList<int[]>[] map;
public int minScore(int n, int[][] roads) {
vis=new boolean[n];
// 存图
map=new ArrayList[n];
for(int i=0;i<n;i++){
map[i]=new ArrayList<int[]>();
}
for(int[]r:roads){
map[r[0]-1].add(new int[]{r[1]-1,r[2]});
map[r[1]-1].add(new int[]{r[0]-1,r[2]});
}
dfs(0);
return ans;
}
// 直接在这层不让vis[n]=true的点进入下层
public void dfs(int n){
vis[n]=true;
for(int[] i:map[n]){
// 更新最小边要在dfs之前
ans=Math.min(ans,i[1]);
// 不能重复遍历
if(!vis[i[0]]) dfs(i[0]);
}
}
// // 将return情况放到下层去处理
// public void dfs(int n){
// if(vis[n]) return;
// vis[n]=true;
// for(int[] i:map[n]){
// // 更新最小边要在
// ans=Math.min(ans,i[1]);
// dfs(i[0]);
// }
// }
}
根据relations数组
的定义,这是一个边权相等的图,对于边权相等的图,统计有限步数的到达某个节点的方案数,最常见的方式是使用 BFS
或 DFS
。对于只需要存储邻点信息,存图方式可以采用HashMap实现,参考算法学习-最短路算法与链式前向星,配图加深理解。
在 DFS 过程中限制深度最多为 k
,然后检查所达节点为 n-1 的次数即可。递归函数void dfs(int id, int sum)
定义为:搜索到点id,走的步数为sum。
class Solution {
HashMap<Integer,HashSet<Integer>> adj=new HashMap<>();
int n,k,ans;
public int numWays(int _n, int[][] relation, int _k) {
n=_n;
k=_k;
ans=0;
for(int[]r:relation){
HashSet<Integer> set=adj.getOrDefault(r[0],new HashSet<>());
set.add(r[1]);
adj.put(r[0],set);
}
dfs(0,0);
return ans;
}
public void dfs(int id,int sum){
if(sum==k){
if(id==n-1) ans++;
return;
}
HashSet<Integer> set=adj.get(id);
if(set==null) return;
for(int i:set){
dfs(i,sum+1);
}
}
}
采用容器存图,DFS递归遍历图并标记,这里都是碰到未访问过的节点继续递归,所以开头直接无脑ans++
,就是从0开始的连通分量中的节点数。相关存图方式可以看我的算法学习-最短路算法与各种存图方式,链式前向星,配图加深理解,讲的很详细。
class Solution {
int ans=0;
public int reachableNodes(int n, int[][] edges, int[] restricted) {
HashSet<Integer>[] adj=new HashSet[n];
for(int i=0;i<n;i++){
adj[i]=new HashSet<>();
}
for(int[]e:edges){
adj[e[0]].add(e[1]);
adj[e[1]].add(e[0]);
}
boolean[]vis=new boolean[n];
//受限点可以简单处理成已访问的点
for(int r:restricted){
vis[r]=true;
}
dfs(0,vis,adj);
return ans;
}
public void dfs(int node,boolean[]vis, HashSet<Integer>[] adj){
vis[node]=true;
ans++;
for(int i:adj[node]){
if(!vis[i]){
dfs(i,vis,adj);
}
}
}
}
邻接矩阵存图+染色法+DFS遍历(没有回溯,一直往下标记完)。通过一个数组染色来决定每个点的分组情况,我们从前往后一个个染色就可以了,因为前面的标记可以一并对后面的决策产生影响力。将不喜欢的关系用邻接矩阵存图,每个点在用邻接矩阵访问自己的相邻节点时,需要确保有dislike关系(graph[i][j]==1
)的人不是自己同一组的,这其中又有两种情况,已经被标记在自己组内没办法了,返回false,如果还未被标记,则递归看反向标记是否可以。
class Solution:
def possibleBipartition(self, n: int, dislikes: List[List[int]]) -> bool:
graph=[[0]*n for _ in range(n)]
# 标记数组,0表示未分组,1为第一组,-1为第二组,不断往下标记
colors=[0]*n
for a,b in dislikes:
graph[a-1][b-1]=1
graph[b-1][a-1]=1
for i in range(n):
# 对数组中未标记的点进行标记检查,看能否标记成1,一种情况不满足就return
# 只需要检查当前点是否能标记成1,因为在前面dfs过程中,已经给其他可以标记的点分好组了
if colors[i]==0 and not self.dfs(graph,colors,1,i,n):
return False
return True
# 判断某个点i是否能被标记成color
def dfs(self,graph,colors,color,i,n):
colors[i]=color
for j in range(n):
# 对于存在dislike的组
if graph[i][j]==1:
# 已经分成同组的了
if colors[j]==color:
return False
# 不能被分到别的组去
if colors[j]==0 and not self.dfs(graph,colors,-1*color,j,n):
return False
return True
DFS+邻接表的数组形式存图,每个节点可以相邻的点都已经列出来了,其他思路同上。
class Solution:
def isBipartite(self, graph: List[List[int]]) -> bool:
n = len(graph)
colors= [0]*n
for i in range(n):
# 一种情况不满足就return
if colors[i]==0 and not self.dfs(graph,colors,1,i):
return False
return True
def dfs(self,graph,colors,color,i):
colors[i]=color
# 对于相邻的点
for j in graph[i]:
if colors[j]==color:
return False
if colors[j]==0 and not self.dfs(graph,colors,-1*color,j):
return False
return True
DFS标记图+层数相关的多源BFS,不同于1162.地图分析要找到所有最短路的最大值,这里只需要存在一条最近的桥就可以了,可以在找到和当前值不同的值直接return层数。刚开始多源BFS将一座岛上的层数都看为0,在向外扩张的时候相当于是一层一层往外移动,找到最近的另一座岛上的节点就返回。
class Solution:
def shortestBridge(self, grid: List[List[int]]) -> int:
que=deque()
def dfs(grid,i,j,mark):
if i<0 or i>=len(grid) or j<0 or j>=len(grid[0]):
return
if grid[i][j] != 1:
return
grid[i][j]=mark
que.append((i,j))
dfs(grid,i+1,j,mark)
dfs(grid,i-1,j,mark)
dfs(grid,i,j-1,mark)
dfs(grid,i,j+1,mark)
find=0
for i in range (len(grid)):
for j in range(len(grid[0])):
if grid[i][j]==1 and find==0:
dfs(grid,i,j,2)
find +=1
# BFS 找最短路径
distance =-1
while len(que)!=0:
distance +=1
size = len(que)
for _ in range(size):
top=que.popleft()
dx=[0,1,0,-1]
dy=[1,0,-1,0]
for k in range(4):
i,j=top[0]+dx[k],top[1]+dy[k]
if i<0 or i>=len(grid) or j<0 or j>=len(grid[0]):
continue
if grid[i][j] == 2:
continue
# 直接return 结果,已走路径的两个端点都不算
if grid[i][j] == 1:
return distance
grid[i][j]=2
que.append((i,j))
return distance
dfs返回值表示以root节点为根的子树的路径总和(累积了前面的数位),叶子节点直接返回
num表示不包含root.val值的根节点开始到root节点结尾的子路径值
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
public int sumNumbers(TreeNode root) {
return dfs(root,0);
}
public int dfs(TreeNode root,int num){
//如果二叉树单侧无节点,直接返回0
if(root==null) return 0;
num=num*10+root.val;
//如果为叶子节点,直接返回结果
if(root.left==null&&root.right==null) return num;
return dfs(root.left,num)+dfs(root.right,num);
}
}