深度优先搜索(dfs)的题目分为两种类型:
-
搜索完所有节点后结束
-
搜索到可行解后就结束
两类问题的区别就在于第二类问题dfs函数用一个布尔类型的返回值来标记是否找到答案,一旦找到一个解就不再继续。至于第一类问题的返回值,根据问题要求解的问题定义。
其实第二类问题很像回溯,比如回溯算法中经典题目「数独」,在某一个未填入数字的方格中只要有一种方案可行,就不用再考虑别的数字了。
因此,第二类问题框架中,一个节点搜索完毕后,应该将该节点重置为「未搜索」,类似于回溯框架中的「撤销选择」
两类问题的框架中值得注意的地方有:
- 用数组mark(或其他形式)标记搜索过的点,搜索到某一个点,标记当前点为「已搜索」
- 当多次调用dfs时,考虑每次调用结束后是否需要重置mark。在连通性问题中不需要重置,在查找是否存在路径的问题中需要重置,下文详细介绍。
接下来,就这两种类型分别介绍相关的题目
类型一:搜索完所有节点后结束
第一类问题可以概括为「求连通区域大小问题」
找一个点所在的连通区域,思想就是从这个点出发,寻找四周联通的点,再从这些点出发递归地找,直到不存在符合条件的点。
这类问题,题目大致意思都是要你「一直找...直到没有符合要求的点」
框架:
mark=[false,false...]//用来标记在一轮搜素中每个节点是否已被搜索过
def 主函数:
for 起点列表:
dfs(起点)
def dfs(当前节点):
if 当前节点不合法:
return;
if 当前节点搜索过:
return;
标记当前节点为已搜索
for 选择列表:
dfs(子节点)
水域大小
这个问题里没有另开mark数组来标记搜索过的点,而是通过修改当前节点值来标记。
两种形式目的是相同的,试想如果不标记搜索过的点,那么会出现StackOverFlow的错误,用树表示就是:
这里需要主要的是多次调用dfs,每次调用结束不需要恢复-1为原来的值。因为当两个点属于同一个连通区域时,其中一个点进行dfs即可得到结果,另一个点再计算结果就会重复。如下图所示:
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;
}
}