您的位置:首页 > 编程语言

基础算法 | 回溯和递归--矩阵中的路径(编程之美)

2018-03-08 19:20 387 查看
因为在日常经常碰到回溯和递归一块使用,所以干脆一块写了。

首先我们先了解一下定义,然后分析具体事例。

回溯(backhacking)算法将搜索空间看作一定的结构,通常为树形结构,一个解对应于树种的一片树叶,算法从树根出发,尝试所有可达的节点。回溯是一种遵照某种规则(避免遗漏)、跳跃式(带裁剪)地搜索解空间的技术—— 引自《算法设计与分析》屈婉玲

回溯算法设计的主要步骤如下:

定义搜索问题的解向量和每个分量的取值范围

确定子结点的排列规则

判断是否满足多米诺性质

确定搜索策略:深度优先、宽度优先、宽深结合等

确定每个结点能够分支的约束条件

确定存储搜索路径的数据结构

这只是课本上的教条的步骤,实际设计并非严格按照这样的顺序。这里需要提一下的是多米诺效应,这个就是为了裁剪用的,它指的是搜索的某结点就知道后面不用走了,比如要狗(1只哈士奇,1只泰迪,1只八哥,1只藏獒)放到笼子里能放多少狗,放了1只哈士奇和1只泰迪笼子满满的了,就没必要尝试放其他狗了。

程序调用自身的编程技巧称为递归( recursion)。——引自百度百科“递归”

递归最大的好处就是减少代码量,在算法设计中如果不考虑内存占用,递归是个不错的选择。如果考虑内存,因为递归一个函数调用自身一个函数,新函数开辟新空间,老函数开辟的空间也不释放,导致内存使用的增加,如果深度过大,导致对内存的不合理占用。但是,在OJ的过程中,我们暂且可以先不考虑这一点。

请设计一个函数,用来判断在一个矩阵中是否存在一条包含某字符串所有字符的路径。路径可以从矩阵中的任意一个格子开始,每一步可以在矩阵中向左,向右,向上,向下移动一个格子。如果一条路径经过了矩阵中的某一个格子,则该路径不能再进入该格子。 例如 a b c e s f c s a d e e 矩阵中包含一条字符串”bcced”的路径,但是矩阵中不包含”abcb”路径,因为字符串的第一个字符b占据了矩阵中的第一行第二个格子之后,路径不能再次进入该格子。

下面是大神的代码,借花献佛了:

链接:https://www.nowcoder.com/questionTerminal/c61c6999eecb4b8f88a98f66b273a3cc
来源:牛客网

public class Solution {
public boolean hasPath(char[] matrix, int rows, int cols, char[] str) {
int flag[] = new int[matrix.length];
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
if (helper(matrix, rows, cols, i, j, str, 0, flag))
return true;
}
}
return false;
}

private boolean helper(char[] matrix, int rows, int cols, int i, int j, char[] str, int k, int[] flag) {
int index = i * cols + j;
if (i < 0 || i >= rows || j < 0 || j >= cols || matrix[index] != str[k] || flag[index] == 1)
return false;
if(k == str.length - 1) return true;
flag[index] = 1;
if (helper(matrix, rows, cols, i - 1, j, str, k + 1, flag)
|| helper(matrix, rows, cols, i + 1, j, str, k + 1, flag)
|| helper(matrix, rows, cols, i, j - 1, str, k + 1, flag)
|| helper(matrix, rows, cols, i, j + 1, str, k + 1, flag)) {
return true;
}
flag[index] = 0;
return false;
}
}




设计回溯的时候,首先想到的是中间过程中如何遍历,然后才是哪是出口,怎么截断,怎么对已访问结点打标记。

设计遍历,最简单就是我们设计一个遍历策略,可以遍历所有点,就比如所示代码,下图的方向选择顺序和代码有出入。



截断策略,走到边界需要不能走,已经访问的不能走,不符合匹配要求的不能走

添加出口,整条字符都匹配完了,就可以出去了

对已访问的结点加锁,这部分在组装的时候再考虑

然后就是把这些组件按照一定逻辑组装起来。

首先把约束和出口函数组上,然后是遍历逻辑,我们考虑两种情况倒推回去:

一种走到出口,return true,回到上层递归遍历代码块中,此时遍历代码块可以继续向上层return true,直接返回第一层的true

另一种中间截断,return false ,四个方向都false,那直接可以直接返回上层false了,上层开始尝试另一个方向,然后它需要对之前方向结点的访问进行失忆处理,这个失忆事件发生时间是和return false一起的,所以写到一块去。

然后尝试四个方向都不行,上层需要放回上上层。这就形成了递归,函数闭合。

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: