“还记得八皇后的解法吗?”数组
“上个世纪的事情,不记得了。”app
“…… 如今回忆一下?”函数
“开会,回头说。”性能
“ fuck u ”spa
“ u shit ”3d
我有一个C++基友,这么称呼是由于他入行时用的是C++。虽然在游走于腾讯、金山以后,现在已经十八般武艺略懂了,但提及来仍是C++的标签最刺眼。code
当你有一个C++基友时,QQ里的平常,不免就会碰到上面那种聊天记录了。orm
八皇后是一个古老的经典问题:如何在一张国际象棋的棋盘上,摆放8个皇后,使其任意两个皇后互相不受攻击。blog
该问题由一位德国国际象棋排局家 Max Bezzel 于 1848年提出。严格来讲,那个年代,尚未“德国”这个国家,彼时称做“普鲁士”。递归
Max Bezzel
1850年,Franz Nauck 给出了第一个解,并将其扩展成了“ n皇后 ”问题,即在一张 n x n 的棋盘上,如何摆放 n 个皇后,使其两两互不攻击。
历史上,八皇后问题曾惊动过“数学王子”高斯(Gauss),并且正是上面这个 Franz Nauck 写信找高斯请教的。
高斯和八皇后问题
在那天被基友问到时,我并不是真的不记得了,而是我压根就没有作过。但我第一次碰见这个问题时,确实是在上个世纪,那是在小学微机教室里,参加市级计算机奥林匹克小学组竞赛的培训课上。
我还记得初次看到这个问题的第一反应——怎么可能摆8个!要知道我初学国际象棋时,常常为了简化局面,早早地就乘机兑掉皇后,由于皇后的威力实在是溢出了我童年的脑壳。一个皇后感受就能够 hold 住全场了,怎么还能够摆8个互不干扰的呢?这确定无解。
因此说,童言无忌这个说法,是有必要的。
一晃好多年。现在基友问过来了,我琢磨着是该补上这份跨世纪的做业了。
给老爸拨了个电话——
“喂,爸,家里的国际象棋放哪了?”
“…… 压箱底了吧,得找找。怎么忽然问这,你要研究西西里防护?”
“我如今开局都不走 e4 了,要研究也是后翼弃兵。”
“别特么瞎扯,给你根杆子你就爬啊,快说,有什么屁事?”
“我要研究八皇后问题。”
“讲中文!”
“我有个问题想研究一下,要在国际象棋棋盘上摆放八个皇后,而且互相不受攻击,求摆法。”
“哦,这样啊…… 那你要国际象棋干吗?”
“我想在国际象棋上试着摆摆啊。”
“国际象棋没有八个皇后,你要国际象棋干吗?”
“呃…… 那我能够拿八个兵当皇后作试验。”
“那你直接画个棋盘摆八个硬币不是一回事?非要用国际象棋?脱裤子放屁,画蛇添足!”
“…… ……”
“老子懒得翻箱子跟你找了,你干脆去买四副国际象棋,而后就有八个皇后了。还有事吗?”
“没…… 没了,爸。”
“早点休息,多喝水,别熬夜。天气冷了,注意加衣服……”
“好,好好。”
—— 对方挂断,通话结束。
我默默地打开了淘宝,搜索“国际象棋”,准备买 4 副……
转念一想,仍是算了,本身画吧。
转念二想,懒得画了,就在脑子里摆摆看吧。
首先,作点分析工做。
虽然还不知道最终的答案长什么样,有多少个,但利用国际象棋的规则,能够知道的是,最终8个皇后的分布一定是:
每行有且只有1个,每列有且只有1个。
由于若是有某一行(或列)空置的话,则必然致使另有一行(或列)存在2个皇后。这显而易见的结果背后,有一个数学概念叫作“抽屉原则”。
借助这个“抽屉”,接下来要作的就是一行一行地找出8个位置。
固然,按一列一列来作也能够,但在处理图形图像等信息时,优先水平方向彷佛更符合人的思惟惯性,听说这是由于人的两只眼睛是水平的。
(跑题了……)
心算8皇后感受有点累,我打算简化问题。
从2皇后开始,不过2皇后无解得太昭然若揭了。
换成3皇后,无解得也是一目了然。
进而思考4皇后的状况(在4X4的棋盘上放4个皇后)。因而,有点思考的空间了。
一开始,棋盘是空的,第1个皇后能够任意放置,但为了思考方便,最好是按照秩序来尝试,因而先将第1个皇后放置在棋盘第1行第1列格子里。
BTW,若是你以为图中的皇后图标长得很像 ROLEX 的 Logo,是由于我用的就是 ROLEX 的 Logo 。
毕竟,他们长得实在是太像了。
第1行已经有了皇后,下一步是寻找第2行皇后的位置。在这以前,须要计算出第2行此时未被第1个皇后攻击的棋格分布。
上图中呈现的是整个棋盘的状态,但此时关注的重点在第2行。接下来,将第2个皇后放置于第2行第3列棋格中。
如今,第1行和第2行都有皇后了,从新计算棋盘状态,以寻找第3行的皇后位置。
通过计算,第3行的全部棋格已经所有处于第1个和第2个皇后的联合攻击范围内了,虽然第4行还有空位,但已经可有可无,当前尝试能够宣告 Game Over 了。
换句话说,刚才的第2个皇后位置不对。调整一下,将第2个皇后从第3列挪到第4列再试试。
调整以后,继续更新棋盘状态。
此时,第3行有一个可用的空位,因而将第3个皇后放在这个棋格中。
而后再次更新棋盘状态。
Oops,又遇到了相似的状况,第4行已经没有棋格能够用了,因此,刚才的第3个皇后位置不对。
但第3行只有一个空位能够用,而这惟一的一个空位又是错误的,这说明,问题仍是出在第2个皇后的位置上。
再进一步回溯分析,能够发现,第2行可用的棋格已经都尝试过了,然而都不对。
因此,问题其实出在第1个皇后的位置上。也就是说,第一步将皇后放置于第1行第1列的摆法就错了。
知错就改,善莫大焉。将第1个皇后挪到第1行第2列,重头再来。
继续,更新棋盘状态。
根据上图,将第2个皇后置于第2行第4列。
继续,更新棋盘状态。
看上去不错,接着,将第3个皇后置于第3行第1列。
继续,更新棋盘状态。
咦,彷佛成了。
BINGO!4皇后的第一个解,找到了。
如今,回顾上面的整个过程,作点抽象,引入一点计算机的思惟,就能够得出解题流程了。
步骤清楚了,如今须要思考的就是过程当中很关键的一步——根据已放置的皇后计算下一行棋格状态的逻辑实现。
这里须要回到国际象棋的规则自己了。
一个皇后在棋盘上的攻击范围以下图所示:
对这个图作点数学上的抽象分析:棋盘自己是一个标准的坐标平面,每一个棋格都有着很明显的坐标位置。
因此,上图能够转换成下面的模型:
受皇后攻击的点,按照和皇后(Q点)的相对位置,能够分红4类:
横向攻击其实不用考虑,由于解题的思路自己就是按行来推动的,先天就过滤掉横向攻击点了。
纵向攻击很容易判断,Q点 和 A2点 的 x坐标 相等,就处于攻击范围内。
不那么直观的是两条斜线的状况,须要算一下。
将正斜线攻击(A3类点)和反斜线攻击(A4类点)的坐标转换一下,表示成基于Q点的偏移——
Q:( x0, y0 )
正斜线 A3:( x0 + m, y0 + m )
反斜线 A4:( x0 - m, y0 + m )
经过观察不可贵出规律——
正斜线上的点: (x0 + m) – x0 = (y0 + m) – y0
即:A3点的横坐标值 - Q点的横坐标值 = A3点的纵坐标值 – Q点的纵坐标值
反斜线上的点: x0 + y0 = (x0 – m) + (y0 + m)
即:Q点横坐标值 + Q点纵坐标值 = A4点横坐标值 + A4点纵坐标值
自此,经过皇后所在的棋格判断棋盘上另外一处方格是否处于被攻击状态的逻辑就所有搞清楚了。
流程和方法都有了,是时候写代码实现具体程序了。
用什么语言来作这事呢?
QBasic,C,C#,Java,Python,Lua,JavaScript,PHP, ……
我在脑壳里慢慢遍历着我所精通的20门语言,俗话说艺不压身,但俗话却没说选择困难症,哎……
(以上这段纯属虚构)
最终,我决定用最近的新欢—— Go 语言来写这个程序。
延续以前的思路,依然将重心放到4皇后的状况,直译上面的分析过程,而后代码差很少长这样:
// 4皇后 package main import ( "fmt" ) func main() { // 定义4个皇后,初始化坐标为[-1,-1],即未放置于棋格中。 var ( queen1 = [2]int{-1, -1} queen2 = [2]int{-1, -1} queen3 = [2]int{-1, -1} queen4 = [2]int{-1, -1} ) // 放置第1个皇后 for i := 0; i < 4; i++ { // 遍历棋盘上的第一行方格(rank1) queen1[0] = i queen1[1] = 0 // 更新第2行棋格状态(此时已放置1个皇后) rank2 := render(queen1) // 放置第2个皇后 for i := 0; i < 4; i++ { if !rank2[i] { queen2[0] = i queen2[1] = 1 // 更新第3行棋格状态(此时已放置2个皇后) rank3 := render(queen1, queen2) // 放置第3个皇后 for i := 0; i < 4; i++ { if !rank3[i] { queen3[0] = i queen3[1] = 2 // 更新第4行棋格状态(此时已放置3个皇后) rank4 := render(queen1, queen2, queen3) // 放置第4个皇后 for i := 0; i < 4; i++ { if !rank4[i] { queen4[0] = i queen4[1] = 3 // 到此,4个皇后均成功置于棋盘中 fmt.Println("solution:", queen1, queen2, queen3, queen4) } } } } } } } } // 根据已放置的皇后,更新下一行棋格的状态 // 返回一个含4个bool类型元素的数组,true表示受攻击的,false表示未受攻击。 func render(queens ...[2]int) [4]bool { // 国际象棋棋盘中的一行,在英文中叫作:rank var rank [4]bool // 获取已放置的皇后的数量,能够获得下一行的索引 y := len(queens) // 遍历下一行的棋格 for x := 0; x < 4; x++ { for _, queen := range queens { // 经过已放置的皇后的棋格坐标来判断攻击范围 if x-queen[0] == y-queen[1] || // 正斜攻击 x == queen[0] || // 纵向攻击 x+y == queen[0]+queen[1] { // 反斜攻击 rank[x] = true // 一旦判断出该棋格受到攻击,则不用再计算后面的皇后对其影响 break } } } return rank }
运行后结果以下:
共2种解,并分别给出了每种解法的4皇后的坐标分布。
说明一下:这里我用到一个包含两整型元素的数组来表示皇后的坐标,每个中括号里面的第1个数字表示 x轴 坐标(对应棋盘上的列),第2个数字表示 y轴 坐标(对应棋盘上的行)。
如今,4皇后已经解决了,那8皇后呢?
很简单,我只须要将 main 函数里面的 for 循环再写4套,就搞定了,复制粘贴但是基本功啊。
(开个玩笑~)
虽然照着上面的代码,写8套循环也确实能够获得正确的结果,但应该没有人有勇气公开地这么干吧。
因此,上面的代码充其量只能算是个草稿,接下来须要把它改为像样的程序。
经过前面的分析以及上面的代码,能够很明显地看出4层循环体里的代码逻辑是同样的。
当循环赶上重复时…… 递归,就要来了。
但在递归以前,先作点小调整。
增长一个const n,用于定义棋盘的规格,避免直接使用字面量“4”;
将用于存储皇后坐标的4个 array 合成1个 slice,这样就不用作固定次数的初始化了,并且对 slice 的操做也使得代码看上去更讨巧一点。
而后,将以前代码中,main 函数里的多重循环部分,精简成一个递归的形式函数调用:
// 放置下一个皇后 // 函数的参数为已放置的皇后的坐标集 func place(queens [][2]int) { // 获取已放置的皇后数量 y := len(queens) // 当已放置的皇后数量未达到n个以前,继续求解动做 if y < n { // 计算下一行的棋格状态 nextRank := render(queens) for x := 0; x < n; x++ { // 当遍历到下一行的可用棋格时 if !nextRank[x] { // 放置一个皇后 queens = append(queens, [2]int{x, y}) // 而后继续尝试下一个皇后的放置 place(queens) // 当上一句的递归调用结束时,表示本次求解过程的结束 // 此时,不管求解是否成功,均须要还本来次的状态(即拿起皇后,准备尝试下一次放置) queens = queens[:y] } } } else { // 当n个皇后均已放置时,表示一次求解的完成 // TODO } }
以前代码中,用于“根据已放置的皇后计算下一行棋格状态”的 render 函数,无须调整。
最后,我以为应该增长一点可视化的工做,将结果直观的打印出来,虽然这不是解题的必要,但数据可视化绝对是一种人文关怀。
加个打印结果的函数:
// 打印结果 // 参数说明 - index:当前解法的序号;solution:皇后分布的坐标 func visualize(index int, solution [][2]int) { fmt.Println("Solution ", index) fmt.Println(strings.Repeat("-", 2*n-1)) for y := 0; y < n; y++ { for x := 0; x < n; x++ { if x == solution[y][0] && y == solution[y][1] { fmt.Print("Q ") } else { fmt.Print("* ") } } println() } fmt.Println(strings.Repeat("-", 2*n-1)) }
函数 visualize 的调用,天然应该发生在 place 函数体的 else 部分,而且顺便记录一下解法的数量(加一个统计变量 total,统计解法总数)
else { // 当n个皇后均已放置时,表示一次求解的完成 total++ visualize(total, queens) }
最终的代码长这样:
// 8 QUEENS PUZZLE package main import ( "fmt" "strings" ) // 棋盘规格 const n int = 4 // 统计解法总数 var total int func main() { // 用于记录已放置的皇后 var queens [][2]int // 递归求解 place(queens) } // 放置下一个皇后 // 函数的参数为已放置的皇后的坐标集 func place(queens [][2]int) { // 获取已放置的皇后数量 y := len(queens) // 当已放置的皇后数量未达到n个以前,继续求解动做 if y < n { // 计算下一行的棋格状态 nextRank := render(queens) for x := 0; x < n; x++ { // 当遍历到下一行的可用棋格时 if !nextRank[x] { // 放置一个皇后 queens = append(queens, [2]int{x, y}) // 而后继续尝试下一个皇后的放置 place(queens) // 当上一句的递归调用结束时,表示本次求解过程的结束 // 此时,不管求解是否成功,均须要还本来次的状态(即拿起皇后,准备尝试下一次放置) queens = queens[:y] } } } else { // 当n个皇后均已放置时,表示一次求解的完成 total++ visualize(total, queens) } } // 根据已放置的皇后,更新下一行棋格的状态 // 返回一个含4个bool类型元素的数组,true表示受攻击的,false表示未受攻击。 func render(queens [][2]int) [n]bool { var rank [n]bool y := len(queens) for x := 0; x < n; x++ { for _, queen := range queens { if x-queen[0] == y-queen[1] || x == queen[0] || x+y == queen[0]+queen[1] { rank[x] = true break } } } return rank } // 打印结果 // 参数说明 - index:当前解法的序号;solution:皇后分布的坐标 func visualize(index int, solution [][2]int) { fmt.Println("Solution ", index) fmt.Println(strings.Repeat("-", 2*n-1)) for y := 0; y < n; y++ { for x := 0; x < n; x++ { if x == solution[y][0] && y == solution[y][1] { fmt.Print("Q ") } else { fmt.Print("* ") } } println() } fmt.Println(strings.Repeat("-", 2*n-1)) }
运行结果以下:
将 const n int = 4 改为 const n int = 8 .
终于,获得八皇后的答案了。
共92种互不相同的解。
拿到结果了,能够再研究研究过程了。去掉可视化工做,只计算解法数量,而后看看程序的性能。
注释掉 visualize 函数的调用,并将 main 函数改造一下,统计程序运行的时间:
func main() { start := time.Now() // 用于记录已放置的皇后 var queens [][2]int // 递归求解 place(queens) end := time.Now() elapsed := end.Sub(start) fmt.Println("Total:", total) fmt.Println("Elapsed:", elapsed) }
在我老旧的一款ThinkPad E系列笔记本上
运行结果以下:
998微秒,不到1毫秒,看上去不错。
至此,八皇后的问题完全完结。
事实上,n 皇后的问题也顺便完结了。
将常量 n 改为9,试试看:
共352种互不相同的解,耗时1.99毫秒。
n = 10 时:
8.99毫秒算出724种互不相同的解。
就这样吧……
后 记
原本觉得这个问题就算研究完了,直到有一天和老爸的另外一次通话——
“你上次找老子要国际象棋的那个问题,后来想出来没有?”
“爸,那小儿科我当天挂完电话,分分钟就解出来了。”
“滚远点,怕不是买了4副象棋吧?”
“怎么可能,我能够心算8盘棋。”
“滚远点,你那个问题我后来也想了的,很简单的问题啊。”
“啊?”(What??? 老爸也解八皇后?)
“你题目只说了摆八个皇后,没说不让摆其它的棋子,对吧?你用其它的兵啊、马啊等棋子把八个皇后隔开,就能够作到互不攻击了。”
“@#¥%&*……”
“还有事吗?”
“没没,没了,爸。”(持续晕眩中)
“早点休息,多喝水,天气冷了注意加衣服,少熬夜。就这。”
——对方挂断通话
附:老爸的解法
本文已独家受权给脚本之家(ID:jb51net)公众号发布