回溯法(探索与回溯法)是一种选优搜索法,又称为试探法,按选优条件向前搜索,以达到目标。但当探索到某一步时,发现原先选择并不优或达不到目标,就退回一步重新选择,这种走不通就退回再走的技术为回溯法,而满足回溯条件的某个状态的点称为“回溯点”。
对于八皇后和矩阵与寻找字符串也是如此,我一个个分析,为了自己更加深刻地理解记忆,我将尝试非常清晰的描绘出整个思路,当然如何能帮到查看的你,我也十分欣慰。
第一题:
分析:
大思路:
看到本体,主要思路就是你既然要我找字符串,那么我就遍历整个矩阵,一个个元素节点开始出发,如果我能找到一个,本题就结束了,如果找到最后一个都没找到,那么over直接return false即可。
细节:
假设我们需要找“path“,那么我们需要首先找p如果满足条件再找他的周围有没有a,..直到h, 当然如果p都没找到,那么遍历下一个,如果找到p但是从该点出发找不到a那么也遍历下一个,只有我们找到h,才算满足条件。
题目也要求我们同一节点只能走一次,那么我需要一个和矩阵大小相同的另一个矩阵【isPass】来记录我走过的路,在我们判别该节点出发找下一个节点的时候,我们需要结合isPass来判断。
ok思路就是如此
接下来代码分析,另外提一点,我们输入的是string类型字符串,如果我们把它转化为m*n矩阵的话,在这里我给出给一个不包含异常判别的方法。
//这个方法我没有做防错,默认是我们输入是合法的。 private static char[,] getMatrix(string matrix, int rows, int cols) { char[,] strArr = new char[rows, cols]; int rowIndex = 0, colIndex = 0; for (int i = 0; i < matrix.Length; i++) { strArr[rowIndex, colIndex++] = matrix[i]; if (colIndex == cols) { colIndex = 0; rowIndex++; } } return strArr; }
//接下来我们暂时不使用矩阵转化,代码如下:
//一个个找 //首先从0索引开始 如果找到就找下一个索引 直到找到path个长度 说明全部找完 public bool hasPath(string matrix, int rows, int cols, string path) { // write code here bool[,] isPass = new bool[rows,cols]; for (int i = 0; i < rows; i++) { for (int j = 0; j < cols; j++) { if (isFind(matrix,rows,cols,i,j,path,0, isPass)) return true; } } return false; }
//初始k带入都是0 如何能到path.Length呢? 只有在isFind内部 满足条件的时候k才能继续增长 //当然要记住 如果不满足条件 不要忘记将该点标记置为false,因为数组为引用类型 private bool isFind(string matrix, int rows, int cols,int rowIndex,int colIndex,string path,int k, bool[,] isPass) { //这样使用深拷贝 ,然后下方可以不写 isPass[rowIndex, colIndex] = false; 但是这样 运行速度 会变慢不少 //因此能使用同一个数组就是用同一个数组 //对于是否有一个满足的我们就是用同一个 //对于找出所有我们使用 下方数组深拷贝,同时注意在本次赋值前,需要将本次的其他值清空 //bool[,] isPass2 = new bool[isPass.GetLength(0), isPass.GetLength(1)]; //for (int i = 0; i < isPass.GetLength(0); i++) //{ // for (int j = 0; j < isPass.GetLength(1); j++) // { // isPass2[i, j] = isPass[i,j]; // } //} if (k == path.Length) { return true; } if(rowIndex>=0&& rowIndex<rows&& colIndex>=0&& colIndex<cols&& !isPass[rowIndex,colIndex]&& matrix[rowIndex* cols+ colIndex]==path[k]) { isPass[rowIndex, colIndex] = true; if( isFind(matrix, rows, cols, rowIndex,colIndex-1, path, k+1, isPass) ||//左 isFind(matrix, rows, cols, rowIndex,colIndex+1, path, k+1, isPass) ||//右 isFind(matrix, rows, cols, rowIndex-1,colIndex, path, k+1, isPass) ||//上 isFind(matrix, rows, cols, rowIndex+1,colIndex, path, k+1, isPass) //下 ) return true; //没成功 isPass[rowIndex, colIndex] = false; } return false; }
上方hasPath就是遍历矩阵每个节点,进行判别
isFind是回溯的核心,就是当满足条件的时候,就是行、列在数组范围内,本节点可以走,该节点满足我们需要查找的字符,这样的话我们标记节点为已经走过,同时判断周围节点,如果周围节点有一个可以达到,那么我们就找到答案了,如果周围节点都达不到,我们一定要将刚刚标记的节点,取消标记,因为数组是引用类型,本次失败了,我们要回溯到之前的状态。
当k达到了字符的最后一位时,说明我们已经找到,可以返回true了。
对于代码中isFind下方的注释,我们等下看完八皇后之后,可以做个总结,就很容易理解了。
题目二:八皇后问题
大家应该都知道什么是八皇后问题,当然下方我写的代码是针对于八皇后的,对于扩展性也没考虑,如果需要扩展的话,稍微改改就好了,OK我们主要聊思路。
首先对于初始化,和打印,这种简单的方法,写法如下:
static void Print(int[,] arr) { for (int i = 0; i < arr.GetLength(0); i++) { for (int j = 0; j < arr.GetLength(1); j++) { Console.Write(arr[i, j] + " "); } Console.WriteLine(); } Console.WriteLine(""); } static int[,] initArr() { int[,] res = new int[8, 8]; for (int i = 0; i < 8; i++) { for (int j = 0; j < 8; j++) { res[i, j] = 0; } } return res; }
对于八皇后问题解很多,我们需要找各个解,因此呢,我们不能只是用一个棋盘
完整的思路:我们从第一行开始逐列遍历,我们判断皇后放在当前位置是否ok? 如果ok的话我们将本棋盘该点置为1,然后继续判别第二行,然后第三行...如果在最后一行也就是第7行的时候也能找到满足的位置,那么ok一个棋盘已经成型。
对于这种情况:当我们判别到第6行第一列满足条件,在第7行也找到了一个满足条件的,然后我们会继续寻找第6行从第一列后面开始找,我们一定找不到,为什么呢,因为我们在第1行的时候满足条件已经将该点标识为1了,因此按照要求该行已经不能放了,但是呢上个节点结果已经判别完了,我们需要将它清掉,ok那我们怎么清空呢,此时和那一点已经没关系了,所以我们需要在每次判断该行各个列之前,将该行的棋盘全部0。
代码:
static void EightQueen() { int[,] arr = initArr(); findRes(0, arr); }
static void findRes(int rowIndex, int[,] arr) { //每次执行方法前深拷贝一个数组,这样才不会污染原数组,本次失败和其他层级之间并不会产生影响 int[,] arr2 = new int[8, 8]; for (int i = 0; i < 8; i++) { for (int j = 0; j < 8; j++) { int a = arr[i, j]; arr2[i, j] =a; } } if (rowIndex == 8) { num++; Console.WriteLine("第" + num + "个为:"); Print(arr2); return; } for (int i = 0; i < 8; i++) { //加入i=1时候isok可以进去 当判断i=2也可以的时候,i=1并没有清除掉,因此这里需要在调整本行之前,先让本行全部清0 然后再对我们需要的进行设置 //必须先清掉 才能判别,否则可能判别不成功,因此这个需要在isOk判别之前写。 for (int j = 0; j < 8; j++) { arr2[rowIndex, j] = 0; } //如果放在本行的i该位置满足条件 这样才能继续找下一行 if (isOk(rowIndex, i, arr2)) { arr2[rowIndex, i] = 1; findRes(rowIndex + 1, arr2); } } }
对于其中的isOk其实很简单,判断上下 左右 左上 左下 右上 右下是否都满足条件,如果满足条件即可嘛。
static bool isOk(int rowIndex, int colIndex, int[,] arr) { bool res = false; //判断上下 左右 左上 左下 右上 右下是否都满足条件 bool topBottom = true, leftRight = true, leftTop = true, leftBottom = true, rightTop = true, rightBottom = true; //上下 for (int i = 0; i < arr.GetLength(0); i++) { if (arr[i,colIndex] != 0) { topBottom = false; } } //左右 for (int i = 0; i < arr.GetLength(1); i++) { if (arr[rowIndex, i] != 0) { leftRight = false; } } //左上 int tempR = rowIndex; int tempC = colIndex; while (--tempR >= 0&&--tempC>=0) { if (arr[tempR, tempC]!=0) { leftTop = false; } } //左下 tempR = rowIndex; tempC = colIndex; while (++tempR <= 7 && --tempC >= 0) { if (arr[tempR, tempC] != 0) { leftBottom = false; } } //右上 tempR = rowIndex; tempC = colIndex; while (--tempR >=0 && ++tempC <= 7) { if (arr[tempR, tempC] != 0) { rightTop = false; } } //右下 tempR = rowIndex; tempC = colIndex; while (++tempR <= 7 && ++tempC <= 7) { if (arr[tempR, tempC] != 0) { rightTop = false; } } if (topBottom&& leftRight&& leftTop&& leftBottom && rightTop && rightBottom) { return true; } return res; }
java求解,精简思路在手机留言
public class EightQueenOptimization { public static void main(String[] args) { findResolve(0); System.out.println(count); } static boolean[][] visited=new boolean[8][8]; static int count; public static void findResolve(int k){ if(k==8){ Print(); System.out.println(); count++; return; } for (int i = 0; i < 8; i++) { visited[k][i]=true; if(!isDange(k,i)){ findResolve(k+1); } //清除该行,另该节点为true //放在这是因为最后一次走的时候他直接返回到上一层栈中了,只有放在这才会都执行,如果放在上方,最后一次返回上一层栈的时候就 //不会清掉该行,就会有问题了! for (int j = 0; j < 8; j++) { visited[k][j]=false; } } } public static void Print(){ for (int i = 0; i < 8; i++) { for (int j = 0; j < 8; j++) { int n=visited[i][j]?1:0; System.out.print(n+" "); } System.out.println(""); } } public static boolean isDange(int rowIndex,int colIndex){ //整行 //整列 for (int i = 0; i < 8; i++) { if(visited[rowIndex][i]){ if(i==colIndex){ continue; } return true; } } for (int i = 0; i < 8; i++) { if (visited[i][colIndex]) { if(i==rowIndex){ continue; } return true; } } //左上、左下。右上。右下 int cl,cr,rt,rb; cl=colIndex-1;rt=rowIndex-1; while (cl>=0&&rt>=0){ if(visited[rt][cl]){ return true; } cl--; rt--; } //左下 cl= colIndex-1;rb=rowIndex+1; while (cl>=0&&rb<8){ if(visited[rb][cl]){ return true; } cl--;rb++; } //右上 cr=colIndex+1;rt=rowIndex-1; while (cr<8&&rt>=0){ if(visited[rt][cr]){ return true; } cr++;rt--; } //右下 cr=colIndex+1;rb=rowIndex+1; while (cr<8&&rb<8){ if(visited[rb][cr]){ return true; } cr++;rb++; } return false; } }
综上:很容易发现,回溯问题求解法和定义一样,我发现回溯问题的 都有一个共同点,就是使用k来记录当前可以到达的位置,只有满足条件的时候k才能继续往下走,当k到达最后的时候就是一个结果!