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