`
jgsj
  • 浏览: 965582 次
文章分类
社区版块
存档分类
最新评论

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



  


  
分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics