设计窗口程序演示八皇后问题
本文节选自《.NET程序员面试指南》一书
这是一道考查应聘者综合能力的问题,其中包含了算法的设计、UI的设计、接口的设计等问题,当然在具体面试中没有时间让应聘者完成整个设计和编码,但是面试官往往通过了解应聘者的设计思路和工作步骤来考查应聘者的能力。本节将具体分析这道程序设计题。
所涉及到的知识点
• 回溯算法
• .NET桌面应用程序的UI设计
分析问题
1.整体设计
八皇后问题是一个非常著名的问题,最初是由著名数学家高斯提出的。问题的描述是这样的:在一个88的棋盘上,摆放8个皇后,任意两个皇后不能处在同一行、同一列和同一斜线上。该问题也可以被扩展为在一个nn的棋盘上摆放n个皇后的问题。而在本节的面试题中,不仅要求寻找解决八皇后问题,并且需要在窗口程序中演示。
应聘者最先要做的不是思考八皇后问题的算法,而是整体的系统架构,为了简单起见,这样的系统可以分成两个模块,第一个模块是一个library类型的项目QueensDll,负责模拟问题、提供算法;而另外一个模块则致力于窗口演示,是一个窗体应用程序QueensUI。这两个模块之间包括下列接口:
• QueensDll接受一个参数,该参数表示棋盘的维度和皇后的数量,这样做是为了模拟N皇后的问题。
• QueensDll提供一个公共方法,返回一个整数说明解的数量。
• QueensDll提供一个公共方法,返回一个整型数组来代表一个解,该方法接受一个下标参数。
• QueensDll提供一个公共只读属性,返回维度N。
2.使用回溯算法解决八皇后问题
下面是设计皇后问题的算法,这里使用的是较为经典的回溯算法,该算法的思路如下:
• 依次在棋盘的每一行上摆放一个皇后。
• 每次摆放都要检查当前摆放是否可行。如果当前的摆放引发冲突,则把当前皇后摆放到当前行的下一列上,并重新检查冲突。
• 如果当前皇后在当前行的每一列上都不可摆放,则回溯到上一个皇后并且将其摆放到下一列上,并重新检查冲突。
• 如果所有皇后都被摆放成功,则表明成功找到一个解,记录下该解并且回溯到上一个皇后。
• 如果第一个皇后也被回溯,则表明已经完成所有可能性的计算。
在开始实际编写算法前,还需要考虑棋盘和棋子摆放的模拟方式,这里采取简单的数组方法,使用一个int[n]的数组来模拟一个棋盘,而该数组某个下标的值,则代表了该行摆放的列数。
int[] data=new int[3]{0,2,1};
(1)有了棋盘的表达方式,根据整个系统的设计,可以初步写下核心模块的结构,如代码13-20所示。
代码13-20 八皇后问题:Queens.cs
/// <summary>
/// 皇后问题内核模块
/// </summary>
public partial class Queens
{
//棋盘列表,每一项代表一个解的棋盘
private List<int[]> _chessBoard;
//维度
private int _dimension;
/// <summary>
/// 不允许默认的公共构造方法
/// </summary>
private Queens()
{
}
/// <summary>
/// 构造方法,需要提供维度来构造
/// </summary>
/// <param name="n">维度</param>
public Queens(int n)
{
//初始化解
_chessBoard = new List<int[]>(n);
//存储维度
_dimension = n;
//计算并得到所有解
Calculate();
}
/// <summary>
/// 只读属性,返回维度
/// </summary>
public int Dimension
{
get
{
return _dimension;
}
}
public int[] GetChessBoard(int index)
{
if (_chessBoard.Count <= index||
index<0)
return null;
int[] result = new Int32[_dimension];
Array.Copy(_chessBoard[index], result, result.LongLength);
return result;
}
/// <summary>
/// 得到解的数量
/// </summary>
public int GetCount()
{
return _chessBoard.Count;
}
}
(2)上述代码定义了所有对外公开的公共方法和属性,并且定义了内部成员和构造方法。在此基础上,就可以实现回溯算法了,如代码13-21所示。
代码13-21 八皇后问题:Queens.cs
/// <summary>
/// 回溯算法的实现
/// </summary>
public partial class Queens
{
/// <summary>
/// 计算入口
/// </summary>
private void Calculate()
{
//初始化一个棋盘
int[] board = new Int32[_dimension];
//循环摆放N个皇后,直至第一皇后也需要回溯
for (int j = 0; j < _dimension && j >= 0; )
{
//是否需要回溯的标志
bool found = false;
//从第一列摆放到第N列
for (int i = board[j]; i < _dimension; i++)
{
//检查是否有冲突
if (NoConflict(j, board, i))
{
//表示没有冲突,实际摆放当前皇后
board[j] = i;
//如何当前摆放的是最后一个皇后,则表明已经找到一个解
if (j == (_dimension - 1))
{
//复制当前棋盘并且存储这个解
int[] result = new Int32[_dimension];
board.CopyTo(result, 0);
_chessBoard.Add(result);
//一个解得到后,就需要回溯来寻找下一个解
found = false;
}
//不需要回溯
else
found = true;
break;
}
}
if (!found)
{
//这里回溯,复位当前皇后
board[j] = 0;
//回溯到上一个皇后
j = j - 1;
if (j >= 0)
board[j]++;
}
else
j++;
}
}
/// <summary>
/// 检查当前摆放是否有冲突
/// </summary>
/// <param name="index">现在摆放第几个皇后</param>
/// <param name="board">当前棋盘</param>
/// <param name="val">当前尝试摆放的列数</param>
/// <returns></returns>
private bool NoConflict(int index, int[] board, int val)
{
//循环检查当前摆放是否和已经摆放的皇后有冲突
//由于是逐行摆放的,所以不存在两个皇后在同一行的可能,只检查列和斜线
for (int i = 0; i < index; i++)
{
if (board[i] == val || //在同一列上
(index - i) == Math.Abs(board[i] - val))//在同一斜线上
return false;
}
//检查接触,无冲突
return true;
}
}
算法的注释已经详细解释了算法的思路,这里有几点读者在编写回溯算法时候需要特别注意:
• 回溯循环的结束在于第一个皇后被回溯。
• 当找到一个解时,需要复制整个棋盘,不然接下来的回溯将破坏已经找到的解。
• 找到一个解后,需要在当前皇后的基础上回溯。
• 回溯一个皇后时,要对当前的列数进行重置。
一般在编写完核心代码后,需要编写一定的测试代码进行单元测试,而不是马上在UI模块中应用算法。这里为了突出重点,笔者省略了测试代码。但笔者强烈建议读者在实际工作时为每个模块编写测试代码。
3.编写UI窗体来演示皇后问题
下面进入了窗体设计的阶段,这里先要考虑的是究竟提供怎样的图形界面给用户使用,需要用户输入什么,同时需要向用户输出什么。整理整个需求,可以得到如下的交互需求:
• 用户需要输入维度,即皇后问题中的N。
• 需要向用户输出N皇后问题的解的数量。
• 需要提供接口,允许用户遍历每个解。
• 对于每个解,需要使用图示的方式绘画整个棋盘。
(1)初步设计整个UI界面
(2)编写后台代码来具体实现UI的功能,首先是“计算”按钮的代码,这里需要读入用户输入的维度,并且调用QueensDll模块来生成具体的解,如代码13-22所示。
代码13-22 八皇后问题:QueensUI.cs
/// <summary>
/// UI后台代码,接受输入,计算所有的解
/// </summary>
public partial class QueensUI : Form
{
//存储皇后问题的解
private Queens _queens;
//棋盘每个格子的宽度
private int _width;
//当前显示的解的下标
private int _current = -1;
/// <summary>
/// 计算按钮的点击事件
/// </summary>
private void button1_Click(object sender, EventArgs e)
{
//得到用户输入的维度
String numstring = NUM.Text.Trim();
//输入为空
if (String.IsNullOrEmpty(numstring))
{
MessageBox.Show("请输入皇后的数目!");
return;
}
//转换失败,输入的不是一个整数
int num;
if (!Int32.TryParse(numstring, out num))
{
MessageBox.Show("输入不正确!");
return;
}
//输入的整数小于0
if (num <= 0)
{
MessageBox.Show("输入不正确!");
return;
}
//输出话所有解
_queens = new Queens(num);
//计算棋盘格子的宽度
_width = (PANEL.Width - 20) / _queens.Dimension;
//显示解的数量
TOTAL.Text = "一共有:" + _queens.GetCount().ToString() + "种方式";
//绘画棋盘和解
Display(0);
}
}
(3)用户单击计算按钮后窗体不仅会输出第一个解,UI还允许用户使用“上一个”和“下一个”按钮来遍历所有的解,这样就需要编写这两个按钮的单击事件,来绘画新的解,如代码13-23所示。
代码13-23 八皇后问题:QueensUI.cs
/// <summary>
/// 遍历所有的解
/// </summary>
public partial class QueensUI : Form
{
/// <summary>
/// 单击下一个按钮事件
/// </summary>
private void NEXT_Click(object sender, EventArgs e)
{
//调整当前解的下标
_current = (_current + 1) % _queens.GetCount();
//重新绘画当前解
Display(_current);
}
/// <summary>
/// 单击上一个按钮事件
/// </summary>
private void FORMER_Click(object sender, EventArgs e)
{
//调整当前解的下标
_current = ((_current - 1)+_queens.GetCount()) % _queens.GetCount();
//重新绘画当前解
Display(_current);
}
}
(4)结束这些按钮响应代码的编写后,就是UI模块最核心的部分:绘画某一个解。绘画的步骤包括绘画整个棋盘和每个皇后的位置。棋盘以交错的横线表示,而皇后的位置则用一个实心的圆来表示。
代码13-24 八皇后问题:QueensUI.cs
/// <summary>
/// 绘画一个解
/// </summary>
public partial class QueensUI : Form
{
//皇后位置和棋盘格子之间的空隙比例
private double _margin = 0.2;
/// <summary>
/// 绘画第index个解
/// </summary>
/// <param name="index">解的下标</param>
private void Display(int index)
{
//index不在解的数组范围内
if (index >= _queens.GetCount())
{
MessageBox.Show("没有合适的摆放方式!");
return;
}
//刷新画板,擦除上一次绘画
PANEL.Refresh();
//得到需要绘画的解
int[] board = _queens.GetChessBoard(index);
//生成画图组件
using (Graphics g = PANEL.CreateGraphics())
{
//生成画笔来绘画棋盘
using (Pen pen = new Pen(Color.Blue))
DrawBoard(g, pen);
//生成画刷来绘画皇后
using (SolidBrush brush = new SolidBrush(Color.Green))
{
DrawQueen(board, g, brush);
}
}
//设置当前解的下标
_current = index;
//输出给用户
CURRENT.Text="当前是第"+(_current+1).ToString()+"种";
}
/// <summary>
/// 绘画一个棋盘
/// </summary>
/// <param name="dc">绘画资源</param>
/// <param name="pen">画笔</param>
private void DrawBoard(Graphics dc, Pen pen)
{
//循环,每次绘画两个线,交叉形成整个棋盘
for (int i = 0; i <=_queens.Dimension; i++)
{
dc.DrawLine(pen, new Point(0, i * _width), new Point(_queens.Dimension * _width, i * _width));
dc.DrawLine(pen, new Point(i * _width, 0), new Point(i * _width, _queens. Dimension * _width));
}
}
/// <summary>
/// 绘画皇后的位置
/// </summary>
/// <param name="queens">存储位置的数组</param>
/// <param name="dc">绘画资源</param>
/// <param name="brush">画笔</param>
private void DrawQueen(int[] queens,Graphics dc,Brush brush)
{
for (int i = 0; i < queens.Length; i++)
{
dc.FillEllipse(brush, new Rectangle(i * _width + (int)(_width * _margin),
queens[i] * _width + (int)(_width * _margin),
(int)(_width*(1-2*_margin)),
(int)(_width*(1-2*_margin))));
}
}
}
(5)至此整个系统就完成了,尝试编译并运行该程序,输入8来演示八皇后问题,将得到如图13.6所示的输出效果。
图13.6 八皇后问题的执行结果
在实际面试和笔试中,一般不会让应聘者完成整个系统的编码,但应聘者需要对系统的设计过程和算法的思想有比较深刻的理解。
答案
八皇后乃至N皇后的问题,通常使用回溯算法来解决。而类似于本节问题的面试题,不仅考查了应聘者的算法能力,也考查了应聘者的系统设计能力。