C# 用回溯遞歸解決“八皇后”問題

C# 用回溯遞歸解決“八皇后”問題

  在很早以前,我曾經用C++寫過一篇使用回溯法來生成隨機數獨的博客。這一次,考慮到這是一系列關於C#的博客,所以利用C#的一些特點,來解決著名的“八皇后”問題。

  一、問題概述

  問題概述搬自百度百科。八皇后問題,是一個古老而著名的問題,是回溯算法的典型案例。該問題是國際西洋棋棋手馬克斯·貝瑟爾於1848年提出:在8X8格的國際象棋棋盤上擺放八個皇后,使其不能互相攻擊,即任意兩個皇后都不能處於同一行、同一列或同一斜線上,問有多少種擺法。

  二、算法分析

  在迭代過程中,不停進行嘗試,如果不行則退回來,換一條路走,這就是“回溯法”的基本思想。

  在本例中,基本算法如下:先遍歷棋盤的每一行,在每一行的第一個位置放一個皇后,接下來遍歷每一列,尋找下一個皇后的位置;一旦找到合適的位置,則把下一個皇后放上去;然後再尋找其下一列放皇后的位置。如果某一列不存在可以放皇后的位置(也就是“無解”的情況),則將前一步的皇后拿掉,回到前一步尋找下一個放皇后的位置。

  例如,對於以下的情況,我們無法在F列再放入一個皇后,問題“無解”,此時我們就要將E4的皇后拿走,從E5、E6、E7、E8來遍歷可以放皇后的位置。拿走E4皇后的這個行爲就叫做“回溯”——我們試過了不行,所以就返回換一個條件重新試。

  遞歸是不斷調用自身的過程。其流程圖大致如下所示:

  三、面向對象的思維設計

  既然是用C#來實現算法,那麼就要用到面向對象的思維,而不是C語言中面向過程的思維。在本例中,我們可以看成,我們在“棋盤”上放“皇后”,可以將棋盤和皇后設計爲類。棋盤的作用是,用回溯法往自身上面放“皇后”對象,皇后的作用是,記錄自身所在的棋盤位置,以及判斷是否與其它皇后相沖突。

  以下是Queen類(Queen.cs)的源代碼:

using System;

namespace EightQueen
{
    public class Queen
    {
        public int X { get; set; }
        public int Y { get; set; }
        public Queen(int _X, int _Y)
        {
            X = _X;
            Y = _Y;
        }

        public bool HasCollision(Queen otherQueen)
        {
            return (
                X == otherQueen.X ||
                Y == otherQueen.Y ||
                Math.Abs(X - otherQueen.X) == Math.Abs(Y - otherQueen.Y)
                );
        }

        public static bool HasCollision(Queen queen1, Queen queen2)
        {
            return (
               queen1.X == queen2.X ||
               queen1.Y == queen2.Y ||
               Math.Abs(queen1.X - queen2.X) == Math.Abs(queen1.Y - queen2.Y)
               );
        }

        public override string ToString()
        {
            return String.Format("X: {0}, Y: {1}", X, Y);
        }
    }
}
  我們可以用Queen.HasCollision來判斷兩個Queen類是否相沖突(是否在相同橫、豎或斜線上),判斷方法十分簡單,如果兩個皇后的X座標相同,或者Y座標相同,或者兩個皇后的X座標之差的絕對值與Y座標之差的絕對值相同,則說明它們是相沖突的。

  接下來是棋盤類,它用於存放皇后。以下是棋盤類Checkerboard.cs的源代碼:

using System.Collections.Generic;
using System.Text;

namespace EightQueen
{
    public class Checkerboard
    {
        public List<Queen> Queens;
        public List<int[,]> Results;
        private int Size;
        
        public Checkerboard(int size)
        {
            Size = size;
            Results = new List<int[,]>();
        }

        public int[][,] SettleQueen()
        {
            Results = new List<int[,]>();
            SettleNext(new List<Queen>(Size), 0);
            return Results.ToArray ();
        }

        private bool SettleNext(List<Queen> queens, int x)
        {
            //按照行來遍歷
            for (int _x = x; _x < Size; _x++)
            {
                bool hasSettled = false;
                //按照列來遍歷
                for (int _y = 0; _y < Size; _y++)
                {
                    Queen q = new Queen(_x, _y);
                    if (queens.Count == 0)
                    {
                        // 一定可以放一個皇后
                        hasSettled = true;
                        queens.Add(q);
                        hasSettled = SettleNext(queens, x + 1);
                    }
                    else
                    {
                        bool hasCollision = false;
                        foreach (var queen in queens)
                        {
                            if (queen.HasCollision (q))
                            {
                                //只要包含一個皇后衝突,則將hasCollision標記爲true
                                hasCollision = true;
                                break;
                            }
                        }
                        if (!hasCollision)
                        {
                            hasSettled = true;
                            queens.Add(q);
                            hasSettled = SettleNext(queens, x + 1);
                        }
                    }
                }
                if (!hasSettled)
                {
                    //遍歷完一列後,如果沒有合適的擺放位置,則回溯
                    //此時有兩種情況:如果queens集合成語數量大於0,說明它可以進行回溯;如果等於0,說明它已經將所有的情況遍歷完成,此時應該終止遞歸。
                    if (queens.Count > 0)
                    {
                        queens.RemoveAt(queens.Count - 1);
                        return false;
                    }
                    return true;
                }
            }

            if (queens.Count == Size)
            {
                Results.Add(ToResultMap(queens));
                //得到一種解後,回溯,尋求下一個解
                //遍歷完一列後,如果沒有合適的擺放位置,則回溯
                queens.RemoveAt(queens.Count - 1);
                return false;
            }
            return true;
        }

        private int[,] ToResultMap(IEnumerable<Queen> queens)
        {
            int[,] result = new int[Size, Size];
            //把所有數組中的數字賦予初值0
            for (int i = 0; i < Size; i++)
            {
                for (int j = 0; j < Size; j++)
                {
                    result[i, j] = 0;
                }
            }
            //將皇后的位置標記爲1
            foreach (var queen in queens)
            {
                result[queen.Y, queen.X] = 1;
            }

            return result;
        }

        public static string ResultMapToString(int[,] resultMap)
        {
            StringBuilder sb = new StringBuilder((int)(resultMap.GetLongLength(0) * resultMap.GetLongLength(1)));
            for (int i = 0; i < resultMap.GetLongLength (0); i++)
            {
                for (int j = 0; j < resultMap.GetLongLength(1); j++)
                {
                    sb.Append(resultMap[i, j] == 0 ? "□" : "■");
                }
                sb.AppendLine();
            }

            return sb.ToString();
        }
    }
}
  棋盤類在實例化的時候可以接入一個Size參數,表明是棋盤的大小。在設計時,爲了解決這一類問題,不應當把棋盤的大小限定爲8x8。Queens保存的是某一個解中的Queen對象組,通過ToResultMap方法,傳入Queens,可以得到一個矩形數組int[,],元素總數是Size * Size。數組表示的是一個棋盤,如果某位置沒有皇后,則爲0,某位置放了皇后,則爲1。如果你覺得這樣不夠直觀,可以用ResultMapToString將這個矩形數組以文本的形式返回。

  下面是程序的入口Program.cs:

using System;

namespace EightQueen
{
    class Program
    {
        static void Main(string[] args)
        {
            Checkerboard checkerboard = new Checkerboard(8);
            int[][,] results = checkerboard.SettleQueen();
            foreach (int[,] result in results)
            {
                Console.WriteLine(Checkerboard.ResultMapToString(result));
            }
            Console.WriteLine("共{0}組解。", results.Length);
            Console.ReadKey(true);
        }
    }
}

  首先實例化一個棋盤Checkerboard,大小爲8*8。通過調用Checkerboard.SettleQueen來得到所有解,通過遍歷所有解,調用Checkerboard.ResultMapToString方法將所有解輸出,最後輸出解的總數。

  在此我們已經看出面向對象編程的好處了。整個程序一目瞭然,非常簡單、清晰。

  事實上,只要棋盤的大小≥4,就存在解,大家可以去試一試。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章