在很早以前,我曾经用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); } } }
在此我们已经看出面向对象编程的好处了。整个程序一目了然,非常简单、清晰。
事实上,只要棋盘的大小≥4,就存在解,大家可以去试一试。