回溯算法
回溯算法已经在前面详细的分析过了,详见猛击此处。
简单的讲:
回溯算法是一种局部暴力的枚举算法
循环中,若条件满足,进入递归,开启下一次流程,若条件不满足,就不进行递归,转而进行上一次流程。这个过程称之为回溯
结构往往是在一个循环中,嵌套一个条件判断,条件判断中,嵌套一个自身递归。三层结构,相互嵌套,缺一不可
【例子】在一个7*7的棋盘中,指定某个起始位置,如何才能使遵循马走日规则的棋子,将36个棋盘位置全部走一遍,其走过的位置不能再走
算法分析:
马走日是规则,极限情况下,一个棋子有如下图8种可选方案,每种方案都对应着一个if语句,8种方案组成我们马走日的行走策略
采用回溯算法需要先找到当前棋子所有的可选方案(代码中由next方法实现),然后遍历每个方案,每个方案采取上述相同的流程,直到找到可以符合题目要求的行走路线,或者是被判定无法再行走而终止递归(代码中由 traversalChessboard 方法实现)
通过上述回溯算法就能完成马踏棋盘问题求解
代码实现:
package cn.dataStructureAndAlgorithm.demo.tenAlgorithm.backtrace;
import java.awt.*;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
public class 回溯算法_backtrace_马踏棋盘 {
private static int X;//棋盘宽度
private static int Y;//棋盘高度
private static boolean[] visited;//用于记录棋牌被访问情况
private static boolean finish=false;//用于记录是否完成马踏棋盘
public static void main(String[] args) {
//初始化数据
X=7;
Y=7;
int row=3;//起始点为3行3列
int col=3;
visited=new boolean[X*Y];
int[][] chessboard=new int[X][Y];
long start=System.currentTimeMillis();//计时开始
//调用马踏棋盘解决方法
traversalChessboard(chessboard,row-1,col-1,1);//为了人机交互方便,需要减-1操作
long end=System.currentTimeMillis();//计时结束
System.out.println("运算总耗时:"+(end-start));;
//输出结果
for (int[] temp:chessboard){
System.out.println(Arrays.toString(temp));
}
}
/**
* 马踏棋盘解决方案主体(回溯算法)
* @param chessboard 棋盘
* @param row 行数
* @param col 列数
* @param step 步数
*/
public static void traversalChessboard(int[][] chessboard,int row,int col,int step){
chessboard[row][col]=step;//将步数赋值到对应棋盘位置上
visited[row*X+col]=true;//将该处设置为已访问(row被-1)
//调用next来获得可选方案,注意next方法处理的时候先处理列,再处理行,所以传入的point对象为point(y,x)
ArrayList allow = next(new Point(col,row));//用于存储目前可以进入的棋盘(存储可选方案)
//回溯算法核心:回溯式的找到所有位置
while (!allow.isEmpty()){//可选方案非空时
Point p=allow.remove(0);//拿取可选方案中的头一个位置
if (!visited[p.y*X+p.x]){//当该方案位置没有被访问时
traversalChessboard(chessboard,p.y,p.x,step+1);//以方案位置再进行上述查找
}
}
//终止回溯条件
if (step next(Point curPoint){
ArrayList allowPoint=new ArrayList<>();//存储当前位置可以进入的位置
Point allow=new Point();
//下面的策略是先计算列,再计算行
//走位置5
if ((allow.x=curPoint.x-2)>=0 && (allow.y=curPoint.y-1)>=0){
allowPoint.add(new Point(allow));
}
//走位置6
if ((allow.x=curPoint.x-1)>=0 && (allow.y=curPoint.y-2)>=0){
allowPoint.add(new Point(allow));
}
//走位置7
if ((allow.x=curPoint.x+1)=0){
allowPoint.add(new Point(allow));
}
//走位置0
if ((allow.x=curPoint.x+2)=0){
allowPoint.add(new Point(allow));
}
//走位置1
if ((allow.x=curPoint.x+2)=0 && (allow.y=curPoint.y+2)=0 && (allow.y=curPoint.y+1)
运算总耗时:13322
[11, 8, 3, 20, 23, 16, 5]
[2, 21, 10, 7, 4, 19, 24]
[9, 12, 1, 22, 15, 6, 17]
[40, 33, 44, 13, 18, 25, 36]
[43, 30, 41, 34, 37, 14, 47]
[32, 39, 28, 45, 48, 35, 26]
[29, 42, 31, 38, 27, 46, 49]
上述代码使用了回溯算法,虽然回溯算法是小规模的暴力,本身已经相较于枚举算法有了提升。但上述实例中,在7*7的棋盘的3行3列上出发的棋子需要运算13322ms(结果不唯一)。这表明这种算法在处理大量数据时是低效率的(回溯了大量无用的步骤)。
为了提高算法效率,就要减少无用步骤的回溯次数
由于一个棋子可能存在多个可进入的位置,而进入这些位置中又可能会存在多个可进入位置。那么我们按照这些位置的可能进入位置数升序排列,先运算可能性小的,再运算可能性大的。即更改了回溯的策略。这样的优化一定程度上(与运气有关)可以大大优化算法效率。
优化代码:
package cn.dataStructureAndAlgorithm.demo.tenAlgorithm.backtrace;
import java.awt.*;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
public class 回溯算法_backtrace_马踏棋盘 {
private static int X;//棋盘宽度
private static int Y;//棋盘高度
private static boolean[] visited;//用于记录棋牌被访问情况
private static boolean finish=false;//用于记录是否完成马踏棋盘
public static void main(String[] args) {
//初始化数据
X=7;
Y=7;
int row=3;//起始点为3行3列
int col=3;
visited=new boolean[X*Y];
int[][] chessboard=new int[X][Y];
long start=System.currentTimeMillis();//计时开始
//调用马踏棋盘解决方法
traversalChessboard(chessboard,row-1,col-1,1);//为了人机交互方便,需要减-1操作
long end=System.currentTimeMillis();//计时结束
System.out.println("运算总耗时:"+(end-start));;
//输出结果
for (int[] temp:chessboard){
System.out.println(Arrays.toString(temp));
}
}
/**
* 马踏棋盘解决方案主体(回溯算法)
* @param chessboard 棋盘
* @param row 行数
* @param col 列数
* @param step 步数
*/
public static void traversalChessboard(int[][] chessboard,int row,int col,int step){
chessboard[row][col]=step;//将步数赋值到对应棋盘位置上
visited[row*X+col]=true;//将该处设置为已访问(row被-1)
//调用next来获得可选方案,注意next方法处理的时候先处理列,再处理行,所以传入的point对象为point(y,x)
ArrayList allow = next(new Point(col,row));//用于存储目前可以进入的棋盘(存储可选方案)
sort(allow);//!!!对结果进行排序优化!!!
//回溯算法核心:回溯式的找到所有位置
while (!allow.isEmpty()){//可选方案非空时
Point p=allow.remove(0);//拿取可选方案中的头一个位置
if (!visited[p.y*X+p.x]){//当该方案位置没有被访问时
traversalChessboard(chessboard,p.y,p.x,step+1);//以方案位置再进行上述查找
}
}
//终止回溯条件
if (step next(Point curPoint){
ArrayList allowPoint=new ArrayList<>();//存储当前位置可以进入的位置
Point allow=new Point();
//下面的策略是先计算列,再计算行
//走位置5
if ((allow.x=curPoint.x-2)>=0 && (allow.y=curPoint.y-1)>=0){
allowPoint.add(new Point(allow));
}
//走位置6
if ((allow.x=curPoint.x-1)>=0 && (allow.y=curPoint.y-2)>=0){
allowPoint.add(new Point(allow));
}
//走位置7
if ((allow.x=curPoint.x+1)=0){
allowPoint.add(new Point(allow));
}
//走位置0
if ((allow.x=curPoint.x+2)=0){
allowPoint.add(new Point(allow));
}
//走位置1
if ((allow.x=curPoint.x+2)=0 && (allow.y=curPoint.y+2)=0 && (allow.y=curPoint.y+1) allow){
allow.sort(new Comparator(){//覆写排序方法,使用Lambda表达式完成
@Override
public int compare(Point p1,Point p2) {
return next(p1).size()-next(p2).size();
}
});
}
}
运算总耗时:637
[47, 16, 3, 30, 7, 18, 5]
[2, 29, 46, 17, 4, 31, 8]
[15, 48, 1, 40, 45, 6, 19]
[36, 41, 28, 49, 34, 9, 32]
[27, 14, 35, 44, 39, 20, 23]
[42, 37, 12, 25, 22, 33, 10]
[13, 26, 43, 38, 11, 24, 21]
你看运算时间从13秒优化到不到1秒,这就是编程的魅力(doge)
还有一个值得注意的地方是:两次结果不同。这是由于回溯算法的不确定性组成的(我们更改了回溯的策略)
其他常用算法,见下各链接
【常用十大算法_二分查找算法】
【常用十大算法_分治算法】
【常用十大算法_贪心算法】
【常用十大算法_动态规划算法(DP)】
【常用十大算法_KMP算法】
【常用十大算法_普里姆(prim)算法,克鲁斯卡尔(Kruskal)算法】
【常用十大算法_迪杰斯特拉(Dijkstra)算法,弗洛伊德(Floyd)算法】
【数据结构与算法整理总结目录 :>】<-- 宝藏在此(doge)