算法之美-二维平面的深度优先与回溯法
给定一个二维网格和一个单词,找出该单词是否存在于网格中。
单词必须按照字母顺序,通过相邻的单元格内的字母构成,其中“相邻”单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母不允许被重复使用。
示例:
board =
[
['A','B','C','E'],
['S','F','C','S'],
['A','D','E','E']
]
给定 word = "ABCCED", 返回 true.
给定 word = "SEE", 返回 true.
给定 word = "ABCB", 返回 false.
public class SearchWord {
static int[][] d = {{-1,0},{0,1},{1,0},{0,-1}};
static int m;
static int n;
static boolean[][] visited;
static char [][]b1 = { {'A','B','C','E'},
{'S','F','C','S'},
{'A','D','E','E'}};
static String words[] = {"ABCCED", "SEE", "ABCB" };
public static void main(String[] args) {
for(int i=0;i<words.length;i++) {
if(exist(b1,words[i])) System.out.println("found " + words[i]);
else
System.out.println("can not found " + words[i]);
}
}
private static boolean exist(char[][] board, String word) {
// TODO Auto-generated method stub
if(board==null||word==null)
throw new IllegalArgumentException("board or word can not be null!");
m = board.length;
n = board[0].length;
visited = new boolean[m][n];
for(int i=0;i<m;i++) {
for(int j=0;j<n;j++) {
if(FindWord(board,word,0,i,j))
return true;
}
}
return false;
}
private static boolean FindWord(char[][] board, String word, int index, int startx, int starty) {
// TODO Auto-generated method stub
if(index == word.length()) {
return board[startx][starty] == word.charAt(index);
}
if(board[startx][starty]==word.charAt(index)) {
visited[startx][starty] =true;
for(int i =0;i<4;i++) {
int newx = startx+d[i][0];
int newy = startx+d[i][1];
if(inArea(newx,newy)&&!visited[newx][newy]&&FindWord(board,word,index+1,newx,newy));
}
visited[startx][starty] =false;
}
return false;
}
private static boolean inArea(int newx, int newy) {
// TODO Auto-generated method stub
return newx>=0&&newx<m&&newy>=0&&newy<n;
}
}
输出:
经典案例
给定一个由 '1'
(陆地)和 '0'
(水)组成的的二维网格,计算岛屿的数量。一个岛被水包围,并且它是通过水平方向或垂直方向上相邻的陆地连接而成的。你可以假设网格的四个边均被水包围。
示例 1:
输入:
11110
11010
11000
00000
输出: 1
示例 2:
输入:
11000
11000
00100
00011
输出: 3
public class IsandsFind {
static int d[][] = {{0, 1}, {1, 0}, {0, -1}, {-1, 0}};
static int m;
static int n;
static boolean[][] visited;
static char[][] grid = {
{'1','1','1','1','0'},
{'1','1','0','1','0'},
{'1','1','0','0','0'},
{'0','0','0','0','0'}
};
public static void main(String[] args) {
if(grid==null||grid.length==0||grid[0].length==0) {
System.out.println(0);
return;
}
m = grid.length;
n = grid[0].length;
visited = new boolean[m][n];
int rest =0;
for(int i=0;i<m;i++) {
for(int j=0;j<m;j++) {
if(grid[i][j]=='1'&&!visited[i][j]) {
DFS(grid,i,j);
rest++;
}
}
}
System.out.println(rest);
}
private static void DFS(char[][] grid, int x, int y) {
// TODO Auto-generated method stub
visited[x][y] =true;
for(int i=0;i<4;i++) {
int newx = x + d[i][0];
int newy = y + d[i][1];
if(inArea(newx,newy)&&!visited[newx][newy]&&grid[newx][newy]=='1')
DFS(grid,newx,newy);
}
}
private static boolean inArea(int x, int y){
return x >= 0 && x < m && y >= 0 && y < n;
}
}
经典案例
给定一个二维的矩阵,包含 'X'
和 'O'
(字母 O)。
找到所有被 'X'
围绕的区域,并将这些区域里所有的 'O'
用 'X'
填充。
示例:
X X X X
X O O X
X X O X
X O X X
运行你的函数后,矩阵变为:
X X X X
X X X X
X X X X
X O X X
解释:
被围绕的区间不会存在于边界上,换句话说,任何边界上的 'O'
都不会被填充为 'X'
。 任何不在边界上,或不与边界上的 'O'
相连的 'O'
最终都会被填充为 'X'
。如果两个元素在水平或垂直方向相邻,则称它们是“相连”的。
这道题我们拿到基本就可以确定是图的dfs、bfs遍历的题目了。题目中解释说被包围的区间不会存在于边界上,所以我们会想到边界上的o要特殊处理,只要把边界上的o特殊处理了,那么剩下的o替换成x就可以了。问题转化为,如何寻找和边界联通的o,我们需要考虑如下情况。
X X X X
X O O X
X X O X
X O O X
这时候的o是不做替换的。因为和边界是连通的。为了记录这种状态,我们把这种情况下的o换成#作为占位符,待搜索结束之后,遇到o替换为x(和边界不连通的o);遇到#,替换回o(和边界连通的o)。
如何寻找和边界联通的o? 从边界出发,对图进行dfs和bfs即可。这里简单总结下dfs和dfs。
- bfs递归。可以想想二叉树中如何递归的进行层序遍历。
- bfs非递归。一般用队列存储。
- dfs递归。最常用,如二叉树的先序遍历。
- dfs非递归。一般用stack。
那么基于上面这种想法,我们有四种方式实现。
public class Reverse {
static int d[][] = {{0, 1}, {1, 0}, {0, -1}, {-1, 0}};
static int m;
static int n;
static boolean[][] visited;
static char[][] grid = {
{'x','x','x','x'},
{'x','0','0','x'},
{'x','x','0','x'},
{'x','0','x','x'}
};
public static void main(String[] args) {
if(grid==null||grid.length==0||grid[0].length==0) {
System.out.println(0);
return;
}
m = grid.length;
n = grid[0].length;
visited = new boolean[m][n];
for(int i=0;i<m;i++) {
for(int j=0;j<m;j++) {
boolean isEdge = i==0||j==0||i==m-1||j==n-1;
if(isEdge&&grid[i][j]=='0') {
DFS(grid,i,j);
}
}
}
for(int i = 0; i < m; i++) {
for(int j = 0; j < n; j++) {
if(grid[i][j] == '0') {
grid[i][j] = 'x';
}
if(grid[i][j] == '#') {
grid[i][j] = '0';
}
}
}
System.out.println(grid);
}
private static void DFS(char[][] grid, int i, int j) {
// TODO Auto-generated method stub
if(i<0||j<0||i>=grid.length||j>=grid[0].length||grid[i][j]=='x'||grid[i][j]=='#') {
return;
}
grid[i][j]='#';
DFS(grid, i-1, j);//上
DFS(grid, i+1, j);//下
DFS(grid, i, j-1);//左
DFS(grid, i, j+1);//右
}
}
输出:
经典案例
在N*N的方格中,随机添加横线完成方阵。走线的方式由上到下,下滑的过程碰到一条横线的顶点,移动到横线的另一个顶点。最下面一行某一个格子放着炸弹,最下面一行不能放横线,每行可以由多个横线,按顺序输出碰到炸弹人的序号
输入三个部分,第一部分人数,第二部分炸弹那最后一行的位置,第三部分横线总数,第四部分横线两个顶点的位置
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Scanner;
public class Huati {
static int[] d = {1,0};//向下
static int[][] Map;
static int N;
static int E;
static int startIndex;
static List<Integer> totallist;
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
while(scanner.hasNext()) {
N = scanner.nextInt();
E = scanner.nextInt();
int M = scanner.nextInt();
Map = new int[N][N];
for(int j=0;j<M;j++) {
int s1 = scanner.nextInt();
int e1 = scanner.nextInt();
int s2 = scanner.nextInt();
int e2 = scanner.nextInt();
Map[s1-1][e1-1] = j+1;
Map[s2-1][e2-1] = j+1;
}
totallist = new ArrayList<Integer>();
for(int i=0;i<N;i++) {
startIndex = i;
DFS(0,i);
}
Collections.sort(totallist);
StringBuilder str = new StringBuilder();
for(int i = 0;i<totallist.size();i++) {
str.append((totallist.get(i)+1));
if(i!=totallist.size()-1) {
str.append(" ");
}
}
System.out.println(str);
}
}
private static void DFS(int x, int y) {
// TODO Auto-generated method stub
if(x==N-1&&y==(E-1)) {
totallist.add(startIndex);
return;
}
if(inArea(x+1,y)&&Map[x][y]==0) {
DFS(x+1, y);
}
if(inArea(x, y)&&Map[x][y]!=0) {
for(int i=0;i<Map[x].length;i++) {
if(Map[x][i] == Map[x][y]&&i!=y) {
if(inArea(x+1, y)) {
DFS(x+1,i);
}
}
}
}
}
private static boolean inArea(int x, int y) {
// TODO Auto-generated method stub
return x>=0&&y>=0&&x<N&&y<N;
}
}
经典案例
给定一个 m x n
的非负整数矩阵来表示一片大陆上各个单元格的高度。“太平洋”处于大陆的左边界和上边界,而“大西洋”处于大陆的右边界和下边界。
规定水流只能按照上、下、左、右四个方向流动,且只能从高到低或者在同等高度上流动。
请找出那些水流既可以流动到“太平洋”,又能流动到“大西洋”的陆地单元的坐标。
提示:
- 输出坐标的顺序不重要
- m 和 n 都小于150
示例:
给定下面的 5x5 矩阵:
太平洋 ~ ~ ~ ~ ~
~ 1 2 2 3 (5) *
~ 3 2 3 (4) (4) *
~ 2 4 (5) 3 1 *
~ (6) (7) 1 4 5 *
~ (5) 1 1 2 4 *
* * * * * 大西洋
返回:
[[0, 4], [1, 3], [1, 4], [2, 2], [3, 0], [3, 1], [4, 0]] (上图中带括号的单元).
从海上找陆地,不然深搜的点太多了,还有就是深搜过的点不要重复找就行了
首先拿到这道题很明显能够判断出是一个二维平面回溯算法的题目,所以首先我们要准备一个移动坐标:
# 分别表示上右下左
self.directs = [(-1, 0), (0, 1), (1, 0), (0, -1)]
一个判定是否在范围内的函数:
def in_area(self, x, y):
return 0 <= x < self.m and 0 <= y < self.n
然后继续分析,这道题是要寻找一个坐标既能够到达太平洋也能到达大西洋,但是这个过程一般不是一次深度搜索就能够完成的,所以我们从各边界开始逆流进行搜索。然后用两个二维数组进行记录,相当于进行了4次深度搜索
import java.util.ArrayList;
import java.util.List;
public class Ocean {
public static void main(String[] args) {
int[][] matrix= {{1,2,2,3,5},{3,2,3,4,4},{2,4,5,3,1},{6,7,1,4,5},{5,1,1,2,4}};
boolean[][] can1 = new boolean[matrix.length][matrix[0].length];//记录太平洋
boolean[][] can2 = new boolean[matrix.length][matrix[0].length];//记录大西洋
List<int[]> res = new ArrayList<>();
for(int i=0;i<matrix.length;i++) {//y固定x在变化
DFS(can1,i,0,matrix); //y=0
DFS(can2,i,matrix[0].length-1,matrix); //y=最大值
}
for(int i=0;i<matrix[0].length;i++) {//x固定y在变化
DFS(can1,0,i,matrix); //x=0
DFS(can2,matrix.length-1,i,matrix); //x=最大值
}
for(int i=0;i<matrix.length;i++) {
for(int j=0;j<matrix[0].length;j++) {
if(can1[i][j]&&can2[i][j]) {
res.add(new int[] {i,j});
}
}
}
System.out.println(res);
}
//(-1,0) 上 (0,1) 右 (1,0) 下 (0,-1)左
private static int[] r= {-1,0,0,1};//上左右下
private static int[] c= {0,-1,1,0};//上左右下
private static void DFS(boolean[][] can, int i, int j, int[][] matrix) {
// TODO Auto-generated method stub
can[i][j] = true;
for(int k=0;k<4;k++) {
int ii = i+r[k];
int jj = j+c[k];
if(ii<0||jj<0||ii>=matrix.length||jj>=matrix[0].length||can[ii][jj]) {
continue;
}
if(matrix[ii][jj]>=matrix[i][j]) {
DFS(can,ii,jj,matrix);
}
}
}
}