我所知道的十大常用算法之马踏棋盘算法(深度搜索、贪心思想优化 )

前言需求


今天我们学习的是马踏棋盘算法,我们还是从一个场景里引入看看

image.png

马踏棋盘算法也被称为骑士周游问题

将马随机放在国际象棋的6×6棋盘Board0~5的某个方格中

提示:马按走棋规则(马走日字)进行移动

要求:每个方格只进入一次,走遍棋盘上全部64个方格

小游戏体验网址:4399:马踏棋盘小游戏

一、马踏棋盘问题

马踏棋盘问题(骑士周游问题)实际上是:图的深度优先搜索(DFS)的应用

还记得图的深度优先搜索(DFS)吗?

有些模糊或者不记得小伙伴可以看往期文章:图(广度优先与深度优先)

image.png

那么按照我们的简单思路,是不是要一个位置一个位置去踩坑看看?

image.png
image.png

那么按照我们的深度优先搜索,就要一步步走下去,直至达成任务

图片.png

当我们的所选第三步的位置,无法达成完成任务

那么我们需要回溯,将原第三步更换到下一个位置里去

image.png

在以新第三步开始,进行搜索,也要一步步走下去,直至达成任务

图片.png

二、通过示例来认识算法

根据我们之前简单的思路,首先我们需要创建一个棋盘的数组

当我们做出选择下一步的时候,我们需要将当前的位置标记为已访问,并根据当前位置计算出马儿能走那些位置,并放入到一个集合中里去

图片.png

当然我们可以根据棋盘的情况来判断是否可以进行计算

图片.png

注意::马儿不同的走法、会得到不同的结果,效率也会有影响(需优化)

规则判断是否可走

那么我怎么知道这些位置是否可走呢?我是怎么计算出来的呢?

图片.png

首先我们先分析当前位置的x、y坐标,按照规则进行计算:(马走日字)

图片.png

我们先分析一下象棋里的马走日是怎么样的吧

图片.png

马走日所说的是马从提棋位置到落棋位置是一个“日”子的对角线,在没有棋子踩住马脚时,马是可以随意走哪个方向的日字都是可以的

图片.png
图片.png

有其他棋子在马的如图相关位置时,马就不能走该方向的日字了,我们也熟称“踩马脚了”。注意无论踩马脚的棋子是己方的棋子还是敌方的棋子,被踩方向的日字都不能走了

图片.png

如果四只马脚都被踩了,那么这只马哪里都走不了了(如图)

图片.png

在我们这个问题中,还请你看图关联看懂马儿怎么走的,即称马走日

图片.png

当我们知道规则怎么玩了,就可以从图上看出来,每个点与当前点的关系

图片.png

那么我们的马儿剩下的点与当前是什么关系呢?怎么走呢?

图片.png

骑士周游算法思路

我们创建一个类存放棋盘行、列,并记录棋盘上的是否被访问过

public class HorseChessboard {
    private static int x;//棋盘的列数
    private static int y;//棋盘的行数
    //创建一个数组,标记棋盘的各个位置是否被访问过
    private static boolean visited[];
    //使用一个属性,标记是否棋盘的所有位置都被访问
    private static boolean finished; // 如果为true,表示成功
}

我们使用Point 类来表示 (x, y) 坐标空间中的位置的点

public class Point extends Point2D implements java.io.Serializable {
    
    public int x;

    public int y;

    private static final long serialVersionUID = -5276940640259749850L;

    public Point() {
        this(0, 0);
    }

    public Point(Point p) {
        this(p.x, p.y);
    }

    public Point(int x, int y) {
        this.x = x;
        this.y = y;
    }

    //以双精度型返回点的 X 坐标。
    public double getX() {
        return x;
    }

    //以双精度型返回点的 Y 坐标。
    public double getY() {
        return y;
    }

    //返回此点的位置。
    @Transient
    public Point getLocation() {
        return new Point(x, y);
    }
    //将点的位置设为指定位置
    public void setLocation(Point p) {
        setLocation(p.x, p.y);
    }
    //将此点更改为具有指定位置
    public void setLocation(int x, int y) {
        move(x, y);
    }

    //将此点的位置设为指定的双精度坐标
    public void setLocation(double x, double y) {
        this.x = (int) Math.floor(x+0.5);
        this.y = (int) Math.floor(y+0.5);
    }

    //将此点移动到 (x,y) 坐标平面中的指定位置。
    public void move(int x, int y) {
        this.x = x;
        this.y = y;
    }

    //平移 (x,y) 位置的点,沿 x 轴平移 dx,沿 y 轴平移 dy,移动后得到点 (x+dx, y+dy)
    public void translate(int dx, int dy) {
        this.x += dx;
        this.y += dy;
    }
    
    //确定两个点是否相等。
    public boolean equals(Object obj) {
        if (obj instanceof Point) {
            Point pt = (Point)obj;
            return (x == pt.x) && (y == pt.y);
        }
        return super.equals(obj);
    }
    // 返回此点的字符串表示形式及其在 (x,y) 坐标空间中的位置
    public String toString() {
        return getClass().getName() + "[x=" + x + ",y=" + y + "]";
    }
}

图片.png

根据思路,需要根据当前位置判断马儿能走那些位置,并将结果放入ArrayList集合中

图片.png

public class HorseChessboard {
    
    //省略其他关键性代码....
    
    /**
     * 功能:根据当前位置(Point对象),计算马儿还能走哪些位置(Point),并放入到一个集合中(ArrayList),最多有8个位置
     * @param curPoint
     * @return
     */
    public static  ArrayList<Point> next(Point curPoint){
        ArrayList<Point> ps = new ArrayList<>();
        //创建一个点
        Point p1 = new Point();
        //判断马儿是否能走5的位置
        if((p1.x = curPoint.x - 2) >=0 && (p1.y = curPoint.y+1) >=0 ){
            ps.add(new Point(p1));
        }
        return  ps;
    }
}

而其他点的位置与当前位置关系,我们之前也使用图解的方式分析,现在代码实现

图片.png

public class HorseChessboard {
    
    //省略其他关键性代码....
    
     /**
     * 功能:根据当前位置(Point对象),计算马儿还能走哪些位置(Point),并放入到一个集合中(ArrayList),最多有8个位置
     * @param curPoint
     * @return
     */
    public static  ArrayList<Point> next(Point curPoint){

        ArrayList<Point> ps = new ArrayList<>();

        //创建一个点
        Point p1 = new Point();

        //表示马儿可以走5这个位置
        if((p1.x = curPoint.x - 2) >= 0 && (p1.y = curPoint.y -1) >= 0) {
            ps.add(new Point(p1));
        }
        //判断马儿可以走6这个位置
        if((p1.x = curPoint.x - 1) >=0 && (p1.y=curPoint.y-2)>=0) {
            ps.add(new Point(p1));
        }
        //判断马儿可以走7这个位置
        if ((p1.x = curPoint.x + 1) < x && (p1.y = curPoint.y - 2) >= 0) {
            ps.add(new Point(p1));
        }
        //判断马儿可以走0这个位置
        if ((p1.x = curPoint.x + 2) < x && (p1.y = curPoint.y - 1) >= 0) {
            ps.add(new Point(p1));
        }
        //判断马儿可以走1这个位置
        if ((p1.x = curPoint.x + 2) < x && (p1.y = curPoint.y + 1) < y) {
            ps.add(new Point(p1));
        }
        //判断马儿可以走2这个位置
        if ((p1.x = curPoint.x + 1) < x && (p1.y = curPoint.y + 2) < y) {
            ps.add(new Point(p1));
        }
        //判断马儿可以走3这个位置
        if ((p1.x = curPoint.x - 1) >= 0 && (p1.y = curPoint.y + 2) < y) {
            ps.add(new Point(p1));
        }
        //判断马儿可以走4这个位置
        if ((p1.x = curPoint.x - 2) >= 0 && (p1.y = curPoint.y + 1) < y) {
            ps.add(new Point(p1));
        }
        return  ps;
    }
}

那么会不会有小伙伴有疑惑??

为什么走五那个位置就要>=0呢,走七的位置就要<x呢?<y又是什么一样?

我们先分析走五的位置的时候,为什么要>=0

图片.png

同理,小于x,小于y代表我们只选择在棋盘内的点,超出的则不能走

骑士周游算法实践

往期我们使用的是二维数组代表这个点是否被访问过

但这里是36步都需要走一遍,那么我们其实可以使用一维数组进行表示

图片.png

这样我们可以是用公式:马儿所在行 * 棋盘行 +马儿所在列 = 马儿下标 + 1

public class HorseChessboard {
    
    //省略其他关键性代码....
    
    
    /**
     * 完成骑士周游问题的算法
     * @param chessboard 棋盘
     * @param row 马儿当前的位置的行 从0开始
     * @param column 马儿当前的位置的列  从0开始
     * @param step 是第几步 ,初始位置就是第1步
     */
    public static void traversalChessboard(int[][] chessboard, int row, int column, int step) {

        //标记当前棋盘执行的是第几步
        chessboard[row][column] = step;

        //row = 3 X = 6 column = 3 = 3 * 6 + 3 = 21 -1 = 20
        visited[row * x + column] = true; //标记该位置已经访问

        //获取当前位置可以走的下一个位置的集合
        ArrayList<Point> ps = next(new Point(column, row));
    }
}

当我们获取到当前位置可以走的下一个位置的集合,就进行遍历递归

public class HorseChessboard {
    
    //省略其他关键性代码....
    
    
    /**
     * 完成骑士周游问题的算法
     * @param chessboard 棋盘
     * @param row 马儿当前的位置的行 从0开始
     * @param column 马儿当前的位置的列  从0开始
     * @param step 是第几步 ,初始位置就是第1步
     */
    public static void traversalChessboard(int[][] chessboard, int row, int column, int step) {

        //标记当前棋盘执行的是第几步
        chessboard[row][column] = step;

        //row = 3 X = 6 column = 3 = 3 * 6 + 3 = 21 -1 = 20
        visited[row * x + column] = true; //标记该位置已经访问

        //获取当前位置可以走的下一个位置的集合
        ArrayList<Point> ps = next(new Point(column, row));
        
        //遍历 ps
        while(!ps.isEmpty()) {
            Point p = ps.remove(0);//取出下一个可以走的位置
            //判断该点是否已经访问过
            if(!visited[p.y * X + p.x]) {//说明还没有访问过
                traversalChessboard(chessboard, p.y, p.x, step + 1);
            }
        }
    }
}

我们怎么区分当前节点的可以走的下一个位置的集合,是否一路就成功了呢?

使用step 和 应该走的步数比较:step = X * Y

假如当前节点的可以走的下一个位置的集合,没有一路就成功,怎么办?

取消该位置已访问,并将棋盘置为0,说明该节点处于回溯状态

public class HorseChessboard {
    
    //省略其他关键性代码....
    
    
    /**
     * 完成骑士周游问题的算法
     * @param chessboard 棋盘
     * @param row 马儿当前的位置的行 从0开始 
     * @param column 马儿当前的位置的列  从0开始
     * @param step 是第几步 ,初始位置就是第1步 
     */
    public static void traversalChessboard(int[][] chessboard, int row, int column, int step) {
        chessboard[row][column] = step;
        //row = 4 X = 8 column = 4 = 4 * 8 + 4 = 36
        visited[row * x + column] = true; //标记该位置已经访问
        //获取当前位置可以走的下一个位置的集合 
        ArrayList<Point> ps = next(new Point(column, row));
        //遍历 ps
        while(!ps.isEmpty()) {
            Point p = ps.remove(0);//取出下一个可以走的位置
            //判断该点是否已经访问过
            if(!visited[p.y * x + p.x]) {//说明还没有访问过
                traversalChessboard(chessboard, p.y, p.x, step + 1);
            }
        }
        //判断马儿是否完成了任务,使用   step 和应该走的步数比较 , 
        //如果没有达到数量,则表示没有完成任务,将整个棋盘置0
        //说明: step < X * Y  成立的情况有两种
        //1. 棋盘到目前位置,仍然没有走完
        //2. 棋盘处于一个回溯过程
        if(step < x * y && !finished ) {
            chessboard[row][column] = 0;
            visited[row * x + column] = false;
        } else {
            finished = true;
        }
    }
}

接下来,让我们使用demo 测试一把这些思路与代码

图片.png

我们采用上图的马儿作为起始位置,来测试看看

public class HorseChessboard {
    
    //省略其他关键性代码....
    
    
    public static void main(String[] args) {

        System.out.println("骑士周游算法,开始运行~~");
        //测试骑士周游算法是否正确
        x = 6;
        y = 6;
        int row = 4; //马儿初始位置的行,从1开始编号
        int column = 3; //马儿初始位置的列,从1开始编号

        //创建棋盘
        int[][] chessboard = new int[x][y];
        visited = new boolean[x * y];//初始值都是false

        //测试一下耗时
        long start = System.currentTimeMillis();
        traversalChessboard(chessboard, row - 1, column - 1, 1);
        long end = System.currentTimeMillis();
        System.out.println("共耗时: " + (end - start) + " 毫秒");
        //输出棋盘的最后情况
        for(int[] rows : chessboard) {
            for(int step: rows) {
                System.out.print(step + "\t");
            }
            System.out.println();
        }
    }
}

运行结果如下:
骑士周游算法,开始运行~~
共耗时: 40 毫秒
08    03    10    29    32    05    
17    28    07    04    11    30    
02    09    18    31    06    33    
27    16    01    20    23    12    
36    19    14    25    34    21    
15    26    35    22    13    24

三、使用贪心思想进行优化

利用贪心算法的思想,对下一步的所有集合的数目, 进行非递减排序

什么是非递减?

递增的情况是:1、2、3、4、5、6、7、8、9

递减的情况是:9、8、7、6、5、4、3、2、1

非递增的情况是:9、8、7、6、5、5、4、3、2、1

非递减的情况是:1、2、2、3、3、4、4、5、6、7

目的:使马儿走的下一步是下一步集合中可选性最少的,减少回溯可能性

public class HorseChessboard {
    
    //省略其他关键性代码....
    
    //根据当前这个一步的所有的下一步的选择位置,进行非递减排序, 减少回溯的次数
    public static void sort(ArrayList<Point> ps) {
        ps.sort(new Comparator<Point>() {

            @Override
            public int compare(Point o1, Point o2) {
                // TODO Auto-generated method stub
                //获取到o1的下一步的所有位置个数
                int count1 = next(o1).size();
                //获取到o2的下一步的所有位置个数
                int count2 = next(o2).size();
                if(count1 < count2) {
                    return -1;
                } else if (count1 == count2) {
                    return 0;
                } else {
                    return 1;
                }
            }
            
        });
    }
}

那么怎么使用呢,我们在算法里进行排序优化

public class HorseChessboard {
    
    //省略其他关键性代码....
    
    
    /**
     * 完成骑士周游问题的算法
     * @param chessboard 棋盘
     * @param row 马儿当前的位置的行 从0开始
     * @param column 马儿当前的位置的列  从0开始
     * @param step 是第几步 ,初始位置就是第1步
     */
    public static void traversalChessboard(int[][] chessboard, int row, int column, int step) {

        //标记当前棋盘执行的是第几步
        chessboard[row][column] = step;

        //row = 3 X = 6 column = 3 = 3 * 6 + 3 = 21 -1 = 20
        visited[row * x + column] = true; //标记该位置已经访问

        //获取当前位置可以走的下一个位置的集合
        ArrayList<Point> ps = next(new Point(column, row));
        
        //对ps进行排序,排序的规则就是对ps的所有的Point对象的下一步的位置的数目,进行非递减排序
        sort(ps);

        //遍历 ps
        while(!ps.isEmpty()) {
            Point p = ps.remove(0);//取出下一个可以走的位置
            //判断该点是否已经访问过
            if(!visited[p.y * X + p.x]) {//说明还没有访问过
                traversalChessboard(chessboard, p.y, p.x, step + 1);
            }
        }
    }
}
public class HorseChessboard {
    
    //省略其他关键性代码....
    
    
    public static void main(String[] args) {

        System.out.println("骑士周游算法,开始运行~~");
        //测试骑士周游算法是否正确
        x = 6;
        y = 6;
        int row = 4; //马儿初始位置的行,从1开始编号
        int column = 3; //马儿初始位置的列,从1开始编号

        //创建棋盘
        int[][] chessboard = new int[x][y];
        visited = new boolean[x * y];//初始值都是false

        //测试一下耗时
        long start = System.currentTimeMillis();
        traversalChessboard(chessboard, row - 1, column - 1, 1);
        long end = System.currentTimeMillis();
        System.out.println("共耗时: " + (end - start) + " 毫秒");
        //输出棋盘的最后情况
        for(int[] rows : chessboard) {
            for(int step: rows) {
                System.out.print(step + "t");
            }
            System.out.println();
        }
    }
}

运行结果如下:
骑士周游算法,开始运行~~
共耗时: 9 毫秒
08    03    10    29    32    05    
17    28    07    04    11    30    
02    09    18    31    06    33    
27    16    01    20    23    12    
36    19    14    25    34    21    
15    26    35    22    13    24

从40毫秒 到9毫秒 这个速度还是很客观的,相比之前的算法更优一些

  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
以下是一个使用贪心算法解决亚马逊棋问题的 C 语言程序。程序实现了一个简单的人机对战,可以让用户先手或者电脑先手。 ```c #include <stdio.h> #include <stdlib.h> #include <string.h> #define BOARD_SIZE 8 #define EMPTY 0 #define BLACK 1 #define WHITE 2 #define ARROW 3 typedef struct Position { int row; int col; } Position; int board[BOARD_SIZE][BOARD_SIZE]; int turn; Position last_move; void init_board() { // 初始化棋盘 memset(board, 0, sizeof(board)); board[0][3] = board[3][0] = board[4][0] = board[7][3] = WHITE; board[0][4] = board[7][4] = BLACK; board[3][7] = board[4][7] = board[7][4] = board[7][7] = WHITE; } int is_valid_move(Position pos) { // 判断是否是合法的移动或放箭操作 if (board[pos.row][pos.col] != turn) { return 0; } if (pos.row == last_move.row && pos.col == last_move.col) { return 0; } if (board[pos.row][pos.col] == ARROW) { return 0; } int d_row, d_col; for (d_row = -1; d_row <= 1; d_row++) { for (d_col = -1; d_col <= 1; d_col++) { if (d_row != 0 || d_col != 0) { int i, row = pos.row, col = pos.col; for (i = 0; i < BOARD_SIZE; i++) { row += d_row; col += d_col; if (row < 0 || row >= BOARD_SIZE || col < 0 || col >= BOARD_SIZE) { break; } if (board[row][col] == EMPTY) { break; } } if (i < BOARD_SIZE && board[row][col] != ARROW) { return 1; } } } } return 0; } void get_valid_moves(Position moves[], int *count) { // 获取所有合法的移动和放箭操作 *count = 0; int row, col; for (row = 0; row < BOARD_SIZE; row++) { for (col = 0; col < BOARD_SIZE; col++) { Position pos = {row, col}; if (is_valid_move(pos)) { moves[*count] = pos; (*count)++; } } } } int evaluate() { // 评估当前局面的得分 int score = 0; int row, col, d_row, d_col; for (row = 0; row < BOARD_SIZE; row++) { for (col = 0; col < BOARD_SIZE; col++) { if (board[row][col] == BLACK) { for (d_row = -1; d_row <= 1; d_row++) { for (d_col = -1; d_col <= 1; d_col++) { if (d_row != 0 || d_col != 0) { int i, row2 = row, col2 = col; for (i = 0; i < BOARD_SIZE; i++) { row2 += d_row; col2 += d_col; if (row2 < 0 || row2 >= BOARD_SIZE || col2 < 0 || col2 >= BOARD_SIZE) { break; } if (board[row2][col2] == WHITE) { score++; } else if (board[row2][col2] == ARROW || board[row2][col2] == BLACK) { break; } } } } } } else if (board[row][col] == WHITE) { for (d_row = -1; d_row <= 1; d_row++) { for (d_col = -1; d_col <= 1; d_col++) { if (d_row != 0 || d_col != 0) { int i, row2 = row, col2 = col; for (i = 0; i < BOARD_SIZE; i++) { row2 += d_row; col2 += d_col; if (row2 < 0 || row2 >= BOARD_SIZE || col2 < 0 || col2 >= BOARD_SIZE) { break; } if (board[row2][col2] == BLACK) { score--; } else if (board[row2][col2] == ARROW || board[row2][col2] == WHITE) { break; } } } } } } } } return score; } int minimax(int depth, int alpha, int beta) { // 使用 Minimax 算法搜索最优解 if (depth == 0) { return evaluate(); } Position moves[BOARD_SIZE * BOARD_SIZE]; int count, i; get_valid_moves(moves, &count); if (count == 0) { return evaluate(); } if (turn == BLACK) { int max_score = -10000; for (i = 0; i < count; i++) { int score; Position move = moves[i]; int tmp = board[move.row][move.col]; board[move.row][move.col] = BLACK; turn = WHITE; score = minimax(depth - 1, alpha, beta); board[move.row][move.col] = tmp; turn = BLACK; if (score > max_score) { max_score = score; if (depth == 3) { last_move = move; } } if (score > alpha) { alpha = score; } if (alpha >= beta) { break; } } return max_score; } else { int min_score = 10000; for (i = 0; i < count; i++) { int score; Position move = moves[i]; int tmp = board[move.row][move.col]; board[move.row][move.col] = WHITE; turn = BLACK; score = minimax(depth - 1, alpha, beta); board[move.row][move.col] = tmp; turn = WHITE; if (score < min_score) { min_score = score; if (depth == 3) { last_move = move; } } if (score < beta) { beta = score; } if (alpha >= beta) { break; } } return min_score; } } void print_board() { // 打印当前棋盘 int row, col; printf(" "); for (col = 0; col < BOARD_SIZE; col++) { printf("%c ", 'a' + col); } printf("\n"); for (row = 0; row < BOARD_SIZE; row++) { printf("%d ", row + 1); for (col = 0; col < BOARD_SIZE; col++) { if (board[row][col] == EMPTY) { printf(". "); } else if (board[row][col] == BLACK) { printf("B "); } else if (board[row][col] == WHITE) { printf("W "); } else if (board[row][col] == ARROW) { printf("* "); } } printf("%d\n", row + 1); } printf(" "); for (col = 0; col < BOARD_SIZE; col++) { printf("%c ", 'a' + col); } printf("\n"); } int main() { int human_first = 0; printf("Do you want to go first? (1 for yes, 0 for no): "); scanf("%d", &human_first); init_board(); turn = BLACK; while (1) { print_board(); if (turn == BLACK) { printf("Your turn (enter move as row col): "); int row, col; scanf("%d %d", &row, &col); Position move = {row - 1, col - 1}; if (!is_valid_move(move)) { printf("Invalid move!\n"); continue; } board[move.row][move.col] = BLACK; last_move = move; turn = WHITE; } else { printf("Computer's turn...\n"); minimax(3, -10000, 10000); board[last_move.row][last_move.col] = WHITE; turn = BLACK; } if (evaluate() > 0) { printf("You win!\n"); break; } else if (evaluate() < 0) { printf("Computer wins!\n"); break; } } return 0; } ```

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值