递归回溯法解决八皇后问题

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/Kwansy/article/details/78624007

一、八皇后问题

皇后是国际象棋中威力最大的棋子。她可以攻击同一行、同一列以及与她处在斜线上的棋子。八皇后问题在1848年由国际西洋棋棋手马克斯·贝瑟尔提出:如何在8x8格的棋盘上摆放八个皇后,使她们不能互相攻击?

这里写图片描述

上图是92种摆法中的其中一种。

一个有用的经验是:在正式敲代码之前,我们应该对程序的流程有清晰的理解。这意味着我需要先做出流程图或者伪算法。
另一个经验是,使用函数将大问题分解成若干个小问题,可以极大地降低编程的难度和提高程序的可读性。为了获得更好的可读性,牺牲一点效率是值得的。
下面,我会介绍我的思路,由简入深地解决这个问题。


二、准备工作

首先,我定义了一个8*8的整型二维数组,用来描述棋盘。我将它所有元素默认初始化为0,0代表某个位置没有放皇后。1代表有皇后。一开始棋盘上什么都没有,所以全是0。

int chess[8][8] = { 0 };

接下来,我根据将来可能会用到的操作编写了几个函数。分别是:

1、在指定的位置放置一枚皇后

void place_chess(int (*m)[8], int x, int y) {
    m[x][y] = 1;
}

2、拿走某个位置的皇后

void remove_chess(int (*m)[8], int x, int y) {
    m[x][y] = 0;
}

3、打印棋盘

void print_chess(int(*m)[8]) {
    printf("****************\n");
    for (int i = 0; i < 8; i++) {
        for (int j = 0; j < 8; j++) {
            printf("%d ", m[i][j]);
        }
        printf("\n");
    }
    printf("****************\n\n");
}

4、判断能否在某个位置放置皇后,如果可以就返回1,否则返回0

int judge(int(*m)[8], int x, int y) {
    int judgement = 1; // 默认可行
    int k1 = 1; // 斜率1
    int k2 = -1; // 斜率2
    int b1 = y - k1 * x; // 点斜式的常数项1
    int b2 = y - k2 * x; // 点斜式的常数项2

    for (int i = 0; i < 8; i++) {
        for (int j = 0; j < 8; j++) {
            // 行、列、斜线
            if (i == x && m[i][j] == 1) {
                judgement = 0;
                return judgement;
            }
            if (j == y && m[i][j] == 1) {
                judgement = 0;
                return judgement;
            }
            if (j == k1 * i + b1 && m[i][j] == 1) {
                judgement = 0;
                return judgement;
            }
            if (j == k2 * i + b2 && m[i][j] == 1) {
                judgement = 0;
                return judgement;
            }
        }
    }

    return judgement;
}

有必要解释最后一个函数judge。这个函数的功能是这样的:传进代表棋盘的二维数组,还有代表一个格子的 x,y 坐标。然后判断能否在这个坐标放置皇后。显然,只要同行、同列、以及过这个点的两条斜率为1和-1的直线上没有其他皇后就行了。点斜式的公式是y = kx + b。斜率只能是1或-1,对应的常数项b分别解出来。就构成了两条斜线的公式,应该很好理解。


三、伪算法

因为我们要在每一行放一个皇后,而每行的操作实际上是差不多的,这种重复性是递归的第一个特征。定义一个函数,它只关注当前的行。

void putQueenInRow(int(*m)[8], int row);

这个函数和它的名字一样,尝试在某行放置皇后。下面给出这个函数的伪算法:

循环8次:尝试在当前行(row)的每一列放置皇后
{
    如果可以放置在这个位置
    {
        在这个位置放置皇后

        如果当前位置不是最后一行
        {
            递归调用自己,进入下一行
            递归返回后,把当前位置的皇后拿走,然后尝试下一列
        }

        如果当前处在最后一行
        {
            将当前的棋盘打印出来,这样就得到了一种摆法
            拿走当前位置的皇后
        }
    }   
}   

请花一些时间理解伪算法,这是整个程序的关键。
在伪算法的基础上,我添加了一个静态变量count,用于记录摆法的序号,每打印一种摆法,就将count增加1
下面是伪算法的实现:

// 在指定行的每一列尝试放皇后
void putQueenInRow(int(*m)[8], int row) {
    static count = 0; // 计数君,每打印一种摆法就加1 

    for (int col = 0; col < 8; col++) {
        if (judge(m, row, col) == 1) {
            // 如果可以,就在这里放一个皇后
            place_chess(m, row, col);

            if (row != 7) {
                // 如果不是最后一行,就进入下一行
                putQueenInRow(m, row + 1);

                // 将原来的皇后拿走,然后尝试下一列
                remove_chess(m, row, col);
                continue;
            }
            else {
                count++;
                printf("这是第%d种摆法\n", count);
                print_chess(m); // 否则就打印棋盘
                remove_chess(m, row, col); // 拿走当前位置的皇后
            }
        }       
    }
}

四、完整代码

#include <stdio.h>

void place_chess(int(*m)[8], int x, int y);
void remove_chess(int(*m)[8], int x, int y);
int judge(int(*m)[8], int x, int y);
void print_chess(int(*m)[8]);
void putQueenInRow(int(*m)[8], int row);

int main() {
    int chess[8][8] = { 0 };
    putQueenInRow(chess, 0); // 从第一行开始尝试

    return 0;
}

// 在指定位置放皇后
void place_chess(int(*m)[8], int x, int y) {
    m[x][y] = 1;
}

// 移走指定位置的皇后
void remove_chess(int(*m)[8], int x, int y) {
    m[x][y] = 0;
}

// 判断是否能在某个格子放皇后
int judge(int(*m)[8], int x, int y) {
    int judgement = 1; // 默认可行
    int k1 = 1; // 斜率1
    int k2 = -1; // 斜率2
    int b1 = y - k1 * x; // 点斜式的常数项1
    int b2 = y - k2 * x; // 点斜式的常数项2

    for (int i = 0; i < 8; i++) {
        for (int j = 0; j < 8; j++) {
            // 行、列、斜线
            if (i == x && m[i][j] == 1) {
                judgement = 0;
                return judgement;
            }
            if (j == y && m[i][j] == 1) {
                judgement = 0;
                return judgement;
            }
            if (j == k1 * i + b1 && m[i][j] == 1) {
                judgement = 0;
                return judgement;
            }
            if (j == k2 * i + b2 && m[i][j] == 1) {
                judgement = 0;
                return judgement;
            }
        }
    }

    return judgement;
}

// 打印棋盘
void print_chess(int(*m)[8]) {
    printf("****************\n");
    for (int i = 0; i < 8; i++) {
        for (int j = 0; j < 8; j++) {
            printf("%d ", m[i][j]);
        }
        printf("\n");
    }
    printf("****************\n\n");
}

// 在指定行的每一列尝试放皇后
void putQueenInRow(int(*m)[8], int row) {
    static count = 0; // 计数君,每打印一种摆法就加1 

    for (int col = 0; col < 8; col++) {
        if (judge(m, row, col) == 1) {
            // 如果可以,就在这里放一个皇后
            place_chess(m, row, col);

            if (row != 7) {
                // 如果不是最后一行,就进入下一行
                putQueenInRow(m, row + 1);

                // 将原来的皇后拿走,然后尝试下一列
                remove_chess(m, row, col);
                continue;
            }
            else {
                count++;
                printf("这是第%d种摆法\n", count);
                print_chess(m); // 否则就打印棋盘
                remove_chess(m, row, col); // 拿走当前位置的皇后
            }
        }       
    }
}

这里写图片描述

五、总结

折腾了两天,经历了无数次失败,最后成功运行的瞬间看着控制台往下滚了半秒,一看果然是92个解,这种感觉真是爽啊(^▽^)

btw,皇后太多有时不见得是好事呀o( ̄︶ ̄)o

猜你喜欢

转载自blog.csdn.net/Kwansy/article/details/78624007