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,就存在解,大家可以去試一試。