1. 程式人生 > >遞歸回溯法解決八皇后問題

遞歸回溯法解決八皇后問題

一、八皇后問題

皇后是國際象棋中威力最大的棋子。她可以攻擊同一行、同一列以及與她處在斜線上的棋子。八皇后問題在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