dfs算法

506 阅读4分钟

深度优先搜索(dfs)的题目分为两种类型:

  1. 搜索完所有节点后结束

  2. 搜索到可行解后就结束

两类问题的区别就在于第二类问题dfs函数用一个布尔类型的返回值来标记是否找到答案,一旦找到一个解就不再继续。至于第一类问题的返回值,根据问题要求解的问题定义。

其实第二类问题很像回溯,比如回溯算法中经典题目「数独」,在某一个未填入数字的方格中只要有一种方案可行,就不用再考虑别的数字了。

因此,第二类问题框架中,一个节点搜索完毕后,应该将该节点重置为「未搜索」,类似于回溯框架中的「撤销选择」

两类问题的框架中值得注意的地方有:

  • 用数组mark(或其他形式)标记搜索过的点,搜索到某一个点,标记当前点为「已搜索」
  • 当多次调用dfs时,考虑每次调用结束后是否需要重置mark。在连通性问题中不需要重置,在查找是否存在路径的问题中需要重置,下文详细介绍。

接下来,就这两种类型分别介绍相关的题目

类型一:搜索完所有节点后结束

第一类问题可以概括为「求连通区域大小问题」

找一个点所在的连通区域,思想就是从这个点出发,寻找四周联通的点,再从这些点出发递归地找,直到不存在符合条件的点。

这类问题,题目大致意思都是要你「一直找...直到没有符合要求的点」

框架:

mark=[false,false...]//用来标记在一轮搜素中每个节点是否已被搜索过
def 主函数:
    for 起点列表:
        dfs(起点)
        
def dfs(当前节点):
    if 当前节点不合法:
        return;
    if 当前节点搜索过:
        return;
    标记当前节点为已搜索
    for 选择列表:
        dfs(子节点)

水域大小

水域大小

这个问题里没有另开mark数组来标记搜索过的点,而是通过修改当前节点值来标记。

两种形式目的是相同的,试想如果不标记搜索过的点,那么会出现StackOverFlow的错误,用树表示就是:

dfs-1.jpg

这里需要主要的是多次调用dfs,每次调用结束不需要恢复-1为原来的值。因为当两个点属于同一个连通区域时,其中一个点进行dfs即可得到结果,另一个点再计算结果就会重复。如下图所示:

dfs-2.jpg

class Solution {
	int r;
	int c;
	int[][] land;
	int[][] dirs= {{1,0},{0,1},{-1,0},{0,-1},{-1,-1},{1,1},{1,-1},{-1,1}};
    public int[] pondSizes(int[][] land) {
    	r=land.length;
    	c=land[0].length;
    	this.land=land.clone();
    	List<Integer> res=new ArrayList<> ();
    	for (int i=0;i<r;i++) {
    		for (int j=0;j<c;j++) {
    			int pondSize=dfs(i,j);
    			if (pondSize>0) {
    				res.add(pondSize);
    			}
    		}
    	}
    	Integer[] ans=res.toArray(new Integer[res.size()]);
    	Arrays.sort(ans);
    	int[] fi=new int[res.size()];
    	for (int i=0;i<ans.length;i++) {
    		fi[i]=ans[i];
    	}
    	return fi;
    	

    }
    private int dfs(int x,int y) {
    	if (!inArea(x, y) || land[x][y]!=0) {
    		//包含不合法的点(不在区域内的点、不是陆地的点)以及已经搜索过的点(-1的点)
    		return 0;
    	}
    	land[x][y]=-1;//用来标记当前节点已被搜索过,不用另外开数组来标记了
    	int sum=1;
    	for (int[] dir:dirs) {
    		int newX=x+dir[0];
    		int newY=y+dir[1];
    		sum+=dfs(newX,newY);
    	}
    	return sum;
    }
    private boolean inArea(int x,int y) {
    	return x>=0 && x<r && y>=0 && y<c;
    }
}

扫雷游戏

扫雷游戏

class Solution {
	int r;
	int c;
	char[][] board;
	int[][] dirs= {{0,1},{1,0},{-1,0},{0,-1},{1,1},{-1,-1},{1,-1},{-1,1}};
    public char[][] updateBoard(char[][] board, int[] click) {
    	r=board.length;
    	c=board[0].length;
    	this.board=board;
    	dfs(click[0],click[1]);
    	return board;
    }
    private void dfs(int x,int y) {
    	if (!inArea(x, y)) {//不合法的点
    		return;
    	}
    	if (board[x][y]=='M') {//不合法的点
    		board[x][y]='X';
    		return;
    	}
    	if (board[x][y]=='B' || (board[x][y]>=49 && board[x][y]<=56)) {//已搜索过的点
    		return;
    	}
    	int count=0;//E周围的地雷数量
    	for (int[] dir:dirs) {
    		int newX=x+dir[0];
    		int newY=y+dir[1];
    		if (inArea(newX, newY) && board[x+dir[0]][y+dir[1]]=='M') {
    			count++;
    		}
    	}
    	if (count!=0) {//标记当前节点为已搜索过的点
    		board[x][y]=(char) (count+48);
    	}else {
    		board[x][y]='B';//标记当前节点为已搜索过的点
    		for (int[] dir:dirs) {
    			int newX=x+dir[0];
    			int newY=y+dir[1];
    			dfs(newX,newY);
    		}
    	}
    }
    private boolean inArea(int x,int y) {
    	return x>=0 && x<r && y>=0 && y<c;
    }
}

岛屿数量

岛屿数量

class Solution {
	int r;
	int c;
	char[][] grid;
	int[][] dirs= {{1,0},{0,1},{-1,0},{0,-1}};
    public int numIslands(char[][] grid) {
    	r=grid.length;
        if (r==0)
            return 0;
    	c=grid[0].length;
    	this.grid=grid;
    	int count=0;
    	for (int i=0;i<r;i++) {
    		for (int j=0;j<c;j++) {
    			int size=dfs(i,j);
    			if (size>0) {
    				count++;
    			}
    		}
    	}
    	return count;
    }
    private int dfs(int x,int y) {
    	if (!inArea(x, y) || grid[x][y]=='0' || grid[x][y]=='2') {//不合法的点和已经查找过的点
    		return 0;
    	}
    	grid[x][y]='2';//标记当前节点为已搜索的点
    	int size=1;//岛屿大小
    	for (int[] dir:dirs) {
    		int newX=x+dir[0];
    		int newY=y+dir[1];
    		size+=dfs(newX,newY);
    	}
    	return size;
    	
    }
    private boolean inArea(int x,int y) {
    	return x>=0 && x<r && y>=0 && y<c;
    }
}

飞地的数量

飞地的数量

两次深度搜索:

第一次先从边界点的点出发,把与边界连通的岛屿变成海。这样,剩余的岛屿都和边界不连通了。

第二次从所有点出发,计算每个岛屿的面积。

class Solution {
	int r;
	int c;
	int[][] A;
	int[][] dirs= {{1,0},{0,1},{-1,0},{0,-1}};
    public int numEnclaves(int[][] A) {
    	r=A.length;
    	c=A[0].length;
    	this.A=A.clone();
    	int res=0;
    	for (int j=0;j<c;j++) {//处理第一行
    		dfs1(0,j);
    	}
    	for (int j=0;j<c;j++) {//处理最后一行
    		dfs1(r-1, j);
    	}
    	for (int i=1;i<r-1;i++) {//处理第一列
    		dfs1(i, 0);
    	}
    	for (int i=1;i<r-1;i++) {//处理最后一列
    		dfs1(i,c-1);
    	}
    	for (int i=0;i<r;i++) {
    		for (int j=0;j<c;j++) {
    			res+=dfs2(i,j);
    		}
    	}
    	return res;

    }
    private void dfs1(int x,int y) {//把与边界连通的飞地变成海
    	if (!inArea(x, y) || A[x][y]==0) {//不合法的点以及搜索过的点(已经变成0的点)
    		return;
    	}
    	A[x][y]=0;//标记当前点为已搜索的点
    	for (int[] dir:dirs) {
    		int newX=x+dir[0];
    		int newY=y+dir[1];
    		dfs1(newX,newY);
    	}	
    }
    private int dfs2(int x,int y) {//求每个飞地的大小
    	if (!inArea(x, y) || A[x][y]==0 || A[x][y]==-1) {//不合法的点以及搜索过的点(-1)
    		return 0;
    	}
    	A[x][y]=-1;//标记当前点为搜素过的点
    	int size=1;
    	for (int[] dir:dirs) {
    		int newX=x+dir[0];
    		int newY=y+dir[1];
    		size+=dfs2(newX,newY);
    	}
    	return size;
    
    }
    private boolean inArea(int x,int y) {
    	return x>=0 && x<r && y>=0 && y<c;
    }
}

岛屿的最大面积

岛屿的最大面积

class Solution {
	int r;
	int c;
	int[][] grid;
	int[][] dirs= {{1,0},{0,1},{-1,0},{0,-1}};
    public int maxAreaOfIsland(int[][] grid) {
    	int maxSize=0;
    	r=grid.length;
    	c=grid[0].length;
    	this.grid=grid;   
    	for (int i=0;i<r;i++) {
    		for (int j=0;j<c;j++) {
    			maxSize=Math.max(maxSize, dfs(i,j));
    		}
    	}	
    	return maxSize;
    }
    private int dfs(int x,int y) {
    	if (!inArea(x, y) || grid[x][y]!=1) {//不合法的点以及搜过的点
    		return 0;
    	}
    	grid[x][y]=-1;//标记当前节点为已搜索过的点
    	int size=1;
    	for (int[] dir:dirs) {
    		int newX=x+dir[0];
    		int newY=y+dir[1];
    		size+=dfs(newX,newY);
    	}
    	return size;
    	
    }
    private boolean inArea(int x,int y) {
    	return x>=0 && x<r && y>=0 && y<c;
    }
}

边框着色

边框着色

先dfs找到(r0,c0)所在的连通区域,再对其边界着色

class Solution {
	int r;
	int c;
	int[][] grid;
	int r0;
	int c0;
	int[][] dirs= {{1,0},{0,1},{-1,0},{0,-1}};
	int old;//(r0,c0)原来的颜色
    public int[][] colorBorder(int[][] grid, int r0, int c0, int color) {
    	this.grid=grid;
    	r=grid.length;
    	c=grid[0].length;
    	this.r0=r0;
    	this.c0=c0;
    	this.old=grid[r0][c0];
    	dfs(r0,c0);
    	Queue<int[]> queue=new LinkedList<> ();
    	for (int i=0;i<r;i++) {
    		for (int j=0;j<c;j++) {
    			if (grid[i][j]==-1) {//属于连通区域
    				for (int[] dir:dirs) {
    					int newi=i+dir[0];
    					int newj=j+dir[1];
    					if (!inArea(newi,newj) || grid[newi][newj]!=-1) {
    						queue.offer(new int[] {i,j});
    						break;
    					}
    				}
    			}
    		}
    	}
    	while (!queue.isEmpty()) {//边界着色
    		int[] pos=queue.poll();
    		grid[pos[0]][pos[1]]=color;
    	}
    	for (int i=0;i<r;i++) {//把连通区域的非边界的点恢复
    		for (int j=0;j<c;j++) {
    			if (grid[i][j]==-1) {
    				grid[i][j]=old;
    			}
    		}
    	}
    	return grid;

    }
    private void dfs(int x,int y) {//找(r0,c0)所在的联通区域并标记成-1
    	if (!inArea(x, y) || grid[x][y]!=old) {//不合法的点
    		return;
    	}
    	if (grid[x][y]==-1) {//搜索过的点
    		return;
    	}
    	grid[x][y]=-1;//标记当前节点为已搜索过的点
    	for (int[] dir:dirs) {
    		int newX=x+dir[0];
    		int newY=y+dir[1];
    		dfs(newX,newY);
    	}
    }
    private boolean inArea(int x,int y) {
    	return x>=0 && x<r && y>=0 && y<c;
    }
    
}

统计封闭岛屿的数目

统计封闭岛屿的数目

class Solution {
	//求不和网格的四条边连通的岛屿的个数
	int[][] grid;
	int r;
	int c;
	int[][] dirs= {{0,1},{1,0},{-1,0},{0,-1}};
	boolean flag=true;//是否封闭
    public int closedIsland(int[][] grid) {
    	r=grid.length;
    	c=grid[0].length;
    	//二维数组的深复制,不能用clone
    	this.grid=new int[r][c];
    	for (int i=0;i<r;i++) {
    		for (int j=0;j<c;j++) {
    			this.grid[i][j]=grid[i][j];
    		}
    	}
    	int res=0;
    	for (int i=0;i<r;i++) {
    		for (int j=0;j<c;j++) {
    			int size=dfs(i,j);
    			if (size>0 && flag) {
    				res++;
    			}
    			flag=true;
    		}
    	}
    	return res;
    }
    private int dfs(int x,int y) {
    	if (!inArea(x, y)) {//不合法的点,表明不封闭
    		flag=false;
    		return 0;
    	}
    	if (grid[x][y]!=0) {//-1搜索过的点,1水域
    		return 0;
    	}
    	grid[x][y]=-1;//标记当前点为已经搜索过的点
    	int size=1;
    	for (int[] dir:dirs) {
    		int newX=x+dir[0];
    		int newY=y+dir[1];
    		size+=dfs(newX,newY);
    	}
    	if (flag)
    		return size;
    	return 0;
    	
    }
    private boolean inArea(int x,int y) {
    	return x>=0 && x<r && y>=0 && y<c;
    }
}

朋友圈

朋友圈

这里的连通区域是x和y两人所在的朋友圈

class Solution {
	int[][] M;
	int r;
	int c;
    public int findCircleNum(int[][] M) {
    	this.M=M;
    	r=M.length;
    	c=M[0].length;
    	int res=0;
    	for (int i=0;i<r;i++) {
    		for (int j=0;j<c;j++) {
    			int count=dfs(i,j);
    			if (count>0) {
    				res++;
    			}
    		}
    	}
    	return res;

    }
    private int dfs(int x,int y) {//x和y所在朋友圈的人数
    	//不合法的
    	//搜索过的
    	if (M[x][y]!=1) {
    		return 0;
    	}
    	//标记已搜索
    	M[x][y]=-1;
    	M[y][x]=-1;
    	int count=1;
    	for (int j=0;j<c;j++) {//x和y是朋友,y的朋友也是x的朋友,找y的朋友
    		count+=dfs(y,j);
    	}
    	return count;
    	
    }
}

I'm stuck

对于这道题目的第一个子问题:找可以从初始位置S到达的方格个数,就属于dfs里的类型一。

思路:从S开始搜索,设置一个R*C的数组表示能否从S到达当前节点。

AC代码:

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.Deque;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.PriorityQueue;
import java.util.Queue;
import java.util.Scanner;
import java.util.Set;


public class Main {
	static int r;
	static int c;
	static char[][] board;
	static int[][] lr= {{0,1},{0,-1}};
	static int[][] ud= {{1,0},{-1,0}};
	static int[][] dirs= {{0,1},{0,-1},{1,0},{-1,0}};
	static boolean[][] mvTo;
	static boolean[][] toEnd;
	static boolean[][] mark;//dfs2中一轮搜索中是否已经搜索过这个点
	public static void main(String[] args) throws IOException {
			Scanner sc=new Scanner(System.in);
			r=sc.nextInt();
			c=sc.nextInt();
			sc.nextLine();
			board=new char[r][c];
			mvTo=new boolean[r][c];
			toEnd=new boolean[r][c];
			mark=new boolean[r][c];
			for (int i=0;i<r;i++) {
				board[i]=sc.nextLine().toCharArray();

			}

			int[] start=new int[2];
			int[] end=new int[2];
			for (int i=0;i<r;i++) {//寻找S和T的坐标
				for (int j=0;j<c;j++) {
					if (board[i][j]=='S') {
						start[0]=i;
						start[1]=j;
					}
					if (board[i][j]=='T') {
						end[0]=i;
						end[1]=j;
					}
					
				}
			}
			dfs1(start[0],start[1]);
			for (int i=0;i<r;i++) {
				for (int j=0;j<c;j++) {
					toEnd[i][j]=dfs2(i,j,new boolean[r][c]);
					mark=new boolean[r][c];
				}
			}
			int count=0;
			for (int i=0;i<r;i++) {
				for (int j=0;j<c;j++) {
					if (mvTo[i][j]==true && toEnd[i][j]==false)
						count++;
				}
			}
			System.out.println(count);

					
			
	}
	private static void dfs1(int x,int y) {//能否从S到达当前点
		if (!inArea(x, y) || board[x][y]=='#' || mvTo[x][y]==true)
			return;
		mvTo[x][y]=true;
		if (board[x][y]=='S' || board[x][y]=='+' || board[x][y]=='T') {
			for (int[] dir:dirs) {
				int newX=x+dir[0];
				int newY=y+dir[1];
				dfs1(newX,newY);
			}
		}
		if (board[x][y]=='-') {
			for (int[] dir:lr) {
				int newX=x+dir[0];
				int newY=y+dir[1];
				dfs1(newX,newY);
			}
		}
		if (board[x][y]=='|') {
			for (int[] dir:ud) {
				int newX=x+dir[0];
				int newY=y+dir[1];
				dfs1(newX,newY);
			}
		}
		if (board[x][y]=='.') {
			dfs1(x+1,y);
		}
}
	private static boolean dfs2(int x,int y,boolean[][] toEnd) {//能否从当前点到达T
		if (!inArea(x, y) || board[x][y]=='#')//不合法的点
			return false;
		if (mark[x][y]) {//已经搜索过
			return toEnd[x][y];
		}
		char temp=board[x][y];
		if (board[x][y]=='+'  || board[x][y]=='S') {
			for (int[] dir:dirs) {
				int newX=x+dir[0];
				int newY=y+dir[1];
				board[x][y]='#';
				if (dfs2(newX,newY,toEnd)) {//有一个方向走通即可
					mark[x][y]=true;
					toEnd[x][y]=true;
					board[x][y]=temp;
					return true;					
				}
					
			}
			board[x][y]=temp;
			mark[x][y]=true;
			toEnd[x][y]=false;
			return false;
		}
		if (board[x][y]=='-') {
			for (int[] dir:lr) {
				int newX=x+dir[0];
				int newY=y+dir[1];
				board[x][y]='#';
				if (dfs2(newX,newY,toEnd)) {
					mark[x][y]=true;
					toEnd[x][y]=true;
					board[x][y]=temp;
					return true;
				}
					
			}
			board[x][y]=temp;
			mark[x][y]=true;
			toEnd[x][y]=false;
			return false;
		}
		if (board[x][y]=='|') {
			for (int[] dir:ud) {
				int newX=x+dir[0];
				int newY=y+dir[1];
				board[x][y]='#';
				if (dfs2(newX,newY,toEnd)) {
					mark[x][y]=true;
					toEnd[x][y]=true;
					board[x][y]=temp;
					return true;
				}
					
			}
			board[x][y]=temp;
			mark[x][y]=true;
			toEnd[x][y]=false;
			return false;
		}
		if (board[x][y]=='.') {
			board[x][y]='#';
			boolean res=dfs2(x+1,y,toEnd);
			mark[x][y]=true;
			toEnd[x][y]=res;
			board[x][y]=temp;
			return res;
		}
		mark[x][y]=true;
		toEnd[x][y]=true;
		return true;//这个点是T
			
}
	private static boolean inArea(int x,int y) {
		return x>=0 && x<r && y>=0 && y<c;
	}

}

类型二:搜索到可行解后就结束

mark=[false,false...]//用来标记在一轮搜素中每个节点是否已被搜索过
def 主函数:
    for 起点列表:
        dfs(起点)
        重置mark
def dfs(当前节点):
	if 当前节点不合法:
        return False;
    if 当前节点搜索过:
        return 当前节点的结果
    标记当前节点为已搜索
    for 选择列表:
        if dfs(子节点):
            记录当前节点结果为True
            return True
    恢复当前节点到未搜索
    return False;

矩阵中的路径

矩阵中的路径

class Solution {
	char[][] board;
	String word;
	int r;
	int c;
	int[][] dirs= {{1,0},{0,1},{-1,0},{0,-1}};
	boolean[][] mark;
    public boolean exist(char[][] board, String word) {
    	this.board=board;
    	this.word=word;
    	r=board.length;
    	c=board[0].length;
    	for (int i=0;i<r;i++) {
    		for (int j=0;j<c;j++) {
    			mark=new boolean[r][c];//这里多次dfs要重置mark
    			if (dfs(i,j,0)) {
    				return true;
    			}
    		}
    	}
    	return false;

    }
    private boolean dfs(int x,int y,int index) {
    	if (index==word.length()) {
    		return true;
    	}
    	if (!inArea(x, y) || board[x][y]!=word.charAt(index)) {//不合法的点
    		return false;
    	}
    	if (mark[x][y]) {//搜索过的点
    		return false;
    	}
    	mark[x][y]=true;//标记当前点为已搜索
    	for (int[] dir:dirs) {
    		int newX=x+dir[0];
    		int newY=y+dir[1];
    		if (dfs(newX,newY,index+1)) {
    			return true;
    		}
    	}
    	mark[x][y]=false;//恢复到未搜索
    	return false;
    	
    }
    private boolean inArea(int x,int y) {
    	return x>=0 && x<r && y>=0 && y<c;
    }
}

节点间通路

节点间通路

class Solution {
	Map<Integer,Set<Integer>> map=new HashMap<> ();//邻接表
	int target;
	boolean[] mark;
    public boolean findWhetherExistsPath(int n, int[][] graph, int start, int target) {
    	this.mark=new boolean[n+1];
    	this.target=target;
    	for (int i=0;i<n;i++) {
    		map.put(i, new HashSet<> ());
    	}
    	for (int i=0;i<graph.length;i++) {//构建邻接表
    		int[] edge=graph[i];
    		Set<Integer> oldSet=map.get(edge[0]);
    		oldSet.add(edge[1]);
    		map.put(edge[0],oldSet);
    	}
    	return dfs(start);

    }
    private boolean dfs(int x) {
    	if (mark[x]) {//搜索过的
    		return false;
    	}
    	if (x==target) {//满足结果条件
    		return true;
    	}
    	mark[x]=true;//标记当前节点为已搜索
    	Set<Integer> nexts=map.get(x);
    	for (int next:nexts) {
    		if (dfs(next)) {
    			return true;
    		}
    	}
    	mark[x]=false;//恢复当前节点为未搜索
    	return false;
    }
}