相关文章:
数据结构与算法 -- 使用DFS算法处理组合类和排列类问题(一维)
在上一篇文章中,我们介绍了通过DFS算法解决组合类以及排列类的问题,主要用于解决一维数组的问题,例如字符串数组、int数组的排列组合问题,那么如果数据集是二维数组,例如矩阵,通常会用来解决路径问题,那么如何通过DFS算法解决此类问题,接下来会详细介绍。
1 矩阵基础知识
1.1 矩阵的遍历
矩阵通常为一个n*m的数组结构,我们可以理解为是多维的数组,首先我们先需要知道,如何遍历矩阵。
java
public static int[][] matrix = {{2, 3, 5}, {4, 7, 9}};
public static void matrixForeach() {
for (int i = 0; i < matrix.length; i++) {
for (int j = 0; j < matrix[i].length; j++) {
Log.d(TAG, "matrixForeach: i = " + i + "j = " + j + " " + matrix[i][j]);
}
}
}
如果学习过线性代数,我们知道行列式和矩阵的区别在于,行列式是n行n列,但是矩阵没有这个限制,所以在遍历的时候,我们可以先拿到有多少行,即matrix的长度;拿到某一行的数组之后,这一行的数组元素个数就是列数。
java
matrixForeach: i = 0 j = 0 2
matrixForeach: i = 0 j = 1 3
matrixForeach: i = 0 j = 2 5
matrixForeach: i = 1 j = 0 4
matrixForeach: i = 1 j = 1 7
matrixForeach: i = 1 j = 2 9
1.2 矩阵内部的移动
回到问题的本身,如果我们想要做二维数组内部的DFS路径问题,如果必须得知道如何在矩阵内部做移动,如下图所示:
元素2可以上下左右移动(理论上),但图中2仅可以向右或者向下移动,因此2会有两条路线,如果选择向右到达下一个点之后,依然采取同样的策略,最终会拿到一条线路;此时回溯到2可以选择向下继续寻找路径。
从上图中我们可以看到,元素2的位置为(0,0),其左侧的元素(0,-1),右侧的元素为(0,1),上方的元素为(-1,0),下方的元素为(1,0),所以左右方向的查找是列数的改动,上下方向的查找是行数的改动。
java
//0 上 1 下 2 左 3 右
private static int dx(int orientation) {
if (orientation == 0) {
return -1;
} else if (orientation == 1) {
return 1;
}
return 0;
}
private static int dy(int orientation) {
if (orientation == 2) {
return -1;
} else if (orientation == 3) {
return 1;
}
return 0;
}
所以二维数组的查询跟一维数组的不同之处在于,二维的需要上下左右的找,而一维数组只需要从左向右找即可,也就是说控制startIndex的方式不一样,但递归回溯的思想不变。
因为矩阵中每一个元素都可以作为起点,所以需要遍历整个数组元素,对每一个元素执行上下左右的深度优先搜索。
java
public static void findPath() {
boolean[][] visited = new boolean[matrix.length][matrix[0].length];
List<String> result = new ArrayList<>();
for (int i = 0; i < matrix.length; i++) {
for (int j = 0; j < matrix[i].length; j++) {
visited[i][j] = true;
move(matrix, i, j, visited,matrix[i][j]+"", result);
visited[i][j] = false;
}
}
Log.d(TAG, "matrixForeach: result" + result);
}
/**
* 二维数组路径查找
*
* @param nums 二维数组
* @param x 行数
* @param y 列数
* @param visited 当前节点是否被访问过了
*/
public static void move(int[][] nums,
int x,
int y,
boolean[][] visited,
String subset,
List<String> result) {
//出口
Log.d(TAG, "move: " + subset);
//什么样的标准可以找到全部路径呢
//上下左右找
for (int i = 0; i < 4; i++) {
int dx = x + dx(i);
int dy = y + dy(i);
//如果没有在界内 或者已经被访问过了,就不管
if (!isInside(nums, dx, dy) || visited[dx][dy]) {
continue;
}
visited[dx][dy] = true;
move(nums, dx, dy, visited, subset + nums[dx][dy], result);
visited[dx][dy] = false;
}
}
当然在上下左右遍历元素的时候,如果出现数组越界或者已经访问了这个元素,那么就不需要走这个方向了,换其他的方向继续查找即可。
java
//判断是否越界
private static boolean isInside(int[][] nums, int x, int y) {
return (x >= 0 && x < nums.length) && (y >= 0 && y < nums[x].length);
}
2 路径问题 - 无固定起点
那么在了解了矩阵的基础知识之后,我们可以使用深度优先搜素路径这个模板,解决一系列的路径问题。
2.1 单词搜索
给定一个 m x n
二维字符网格 board
和一个字符串单词 word
。如果 word
存在于网格中,返回 true
;否则,返回 false
。
单词必须按照字母顺序,通过相邻的单元格内的字母构成,其中"相邻"单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母不允许被重复使用。
示例 1:
arduino
输入: board = [["A","B","C","E"],["S","F","C","S"],["A","D","E","E"]], word = "ABCCED"
输出: true
示例 2:
arduino
输入: board = [["A","B","C","E"],["S","F","C","S"],["A","D","E","E"]], word = "SEE"
输出: true
这道题是要我们查找二维矩阵中是否通过某个路径形成一个单词,这道题其实就是经典的路径问题,我们可以通过搜索全部的路径,来匹配要查询的单词,模板依然是1.2小节中。
java
public boolean exist(char[][] board, String word) {
if(board == null || board.length == 0){
return false;
}
boolean[][] visited = new boolean[board.length][board[0].length];
//构建前缀集合
List<String> prefix = new ArrayList();
for(int i = 1;i<=word.length();i++){
prefix.add(word.substring(0,i));
}
List<String> results = new ArrayList();
//遍历矩阵
for(int i = 0;i < board.length;i++){
for(int j = 0;j < board[i].length;j++){
visited[i][j] = true;
dfs(board,i,j,visited,board[i][j]+"",results,word,prefix);
visited[i][j] = false;
}
}
return results.size() > 0;
}
private void dfs(char[][] board,
int x,
int y,
boolean[][] visited,
String subset,
List<String> results,
String word,
List<String> prefix){
//如果前缀不一致,直接return
if(!prefix.contains(subset)){
return;
}
if(subset.equals(word)){
results.add(subset);
}
for(int i = 0;i<4;i++){
int dx = x + dx(i);
int dy = y + dy(i);
//如果越界 或者 访问过元素,跳过即可
if(!inside(board,dx,dy) || visited[dx][dy]){
continue;
}
visited[dx][dy] = true;
dfs(board,dx,dy,visited,subset+board[dx][dy],results,word,prefix);
visited[dx][dy] = false;
}
}
//基础方法
// 0 上 1 下 移动
private int dx(int orientation){
if(orientation == 0){
return -1;
}else if(orientation == 1){
return 1;
}
return 0;
}
// 2 左 3 右 移动
private int dy(int orientation){
if(orientation == 2){
return -1;
}else if(orientation == 3){
return 1;
}
return 0;
}
//判断是否越界
public boolean inside(char[][] arr,int x,int y){
return (x >= 0 && x < arr.length) && (y >= 0 && y < nums[x].length);
}
这里我们先构建了搜索单词的前缀数组,如果路径与前缀不匹配,那么就停止搜索;如果匹配到了单词,那么就存到一个数组中,最终判断数组是否为空。
当然这个算法存在优化的空间,因为我们找到单词之后后续的路径可以停止寻找。
3 路径问题 - 有固定起点
3.1 LeetCode64 - 最小路径和问题
给定一个包含非负整数的 m x n
网格 grid
,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。
说明: 每次只能向下或者向右移动一步。
示例 1:
lua
输入: grid = [[1,3,1],[1,5,1],[4,2,1]]
输出: 7
解释: 因为路径 1→3→1→1→1 的总和最小。
像这道题,因为题目要求是要从左上角出发,终点是左下角;而且只能向下或者向右移动,只有2个方向,那么就可以归结为经典的二叉树问题,采用二叉树的深度优先算法,如下图。
这里需要注意一点,就是当一直往下的时候,例如(2,0)的位置,此时在递归的出口处无法拦截,此时会继续进入到dfs中,此时需要做边界的判断,如果超出行、或者列数时,和不变即可,此后进入到出口时会被return,回溯转向右侧分支,此时右侧分支的行数一定不会越界,只需要关注列数的边界即可。
java
public int minPathSum(int[][] grid) {
if(grid == null || grid.length == 0){
return 0;
}
int[] min = {Integer.MAX_VALUE};
dfs(grid,0,0,grid[0][0],min);
return min[0];
}
private void dfs(int[][] grid,
int x,
int y,
int sum,
int[] min){
if (x < 0 || y < 0 || x >= grid.length || y >= grid[x].length) {
return;
}
if (x == grid.length - 1 && y == grid[x].length - 1) {
//到了右下角了
min[0] = Math.min(min[0], sum);
return;
}
//只能移动两个方向
for (int i = 0; i < 2; i++) {
int dx = x + dx3(i);
int dy = y + dy3(i);
dfs(grid, dx, dy, sum + ((dx == grid.length || dy == grid[dx].length) ? 0 : grid[dx][dy]), min);
}
}
// 0 下 1 右
// 当向下走时,x + 1,y 不变
// 当向右走时,x 不变, y + 1
private static int dx3(int orientation) {
if (orientation == 0) {
// 下
return 1;
} else {
// 右
return 0;
}
}
private static int dy3(int orientation) {
if (orientation == 0) {
return 0;
} else {
return 1;
}
}
3.2 LeetCode120 - 三角形最小路径和
给定一个三角形 triangle
,找出自顶向下的最小路径和。
每一步只能移动到下一行中相邻的结点上。相邻的结点 在这里指的是 下标 与 上一层结点下标 相同或者等于 上一层结点下标 + 1 的两个结点。也就是说,如果正位于当前行的下标 i
,那么下一步可以移动到下一行的下标 i
或 i + 1
。
示例 1:
lua
输入: triangle = [[2],[3,4],[6,5,7],[4,1,8,3]]
输出: 11
解释: 如下面简图所示:
2
3 4
6 5 7
4 1 8 3
自顶向下的最小路径和为 11(即,2 + 3 + 5 + 1 = 11)。
同样,这道题我们也要求我们从原点出发,而且只能走到相邻节点上,如下图所示:
像这种标准的平衡二叉树,对于边界问题的处理,只需要判断当前x,也就是行数是否超过二维数组的行数作为出口即可。
java
public int minimumTotal(List<List<Integer>> triangle) {
if(triangle == null || triangle.size() == 0){
return 0;
}
int[] min = {Integer.MAX_VALUE};
dfs(triangle,0,0,triangle.get(0).get(0),min);
return min[0];
}
private void dfs(List<List<Integer>> triangle,
int x,
int y,
int sum,
int[] min){
if (x == triangle.size() - 1) {
min[0] = Math.min(min[0], sum);
return;
}
if (!inside(triangle,x,y)){
return;
}
//只有两步路可以走
dfs(triangle,x+1,y,sum+triangle.get(x+1).get(y),min);
dfs(triangle,x+1,y+1,sum+triangle.get(x+1).get(y+1),min);
}