局部搜索解n皇后,并测试n的极限
回溯法难以求解更大规模的n皇后问题,但是基于概率的局部搜索算法可以解决一定规模上的 n 皇后问题
根据课件上的局部搜索算法思路,n皇后具体流程为:
对于上面的思路,现提供 QS2 算法思路,主要优化点在于交换策略不再是任意交换,而是选择陷入冲突的棋子进行交换
package main
import (
"fmt"
"math/rand"
"time"
)
const (
// 加速系数
speedUpFactor = 0.25
// 循环系数
cycleFactor = 32
)
// n 后问题:局部搜索法
func NQueenLocalSearch(n int) {
c := n
downSlash, upSlash := make([]int, 2*n), make([]int, 2*n)
var s []int
// 从这里开始计时
start := time.Now()
outer:
for c > 0 {
// 1. 随机生成棋盘
s = RandomCreateNQByArray(n)
// 2. 获取棋盘中的冲突
c := GetConflictsByArray(s, downSlash, upSlash)
// 3. 如果冲突为0,转 7
if c == 0 {
break outer
}
// 4. 获取棋盘上陷入冲突的所有棋子
attacks := GetAttack(s, downSlash, upSlash)
limit := int(speedUpFactor * float64(c))
loopSteps := 0
for loopSteps < cycleFactor*n {
for k := 0; k < len(attacks); k++ {
i := attacks[k]
j := randInt(i, n)
// 5. 对冲突棋子进行模拟交换的冲突计算,若冲突减少,则更新棋盘,否则继续取其他冲突棋子
if c1 := calConflictBeforeSwap(s, downSlash, upSlash, i, j); c1 < 0 {
c += c1
// 更新交换后的棋盘
updateQueen(s, downSlash, upSlash, i, j)
// 如果冲突为0,转 7
if c == 0 {
break outer
}
// 加速项:若冲突小于某个阈值,则更新阈值并重新计算冲突棋子
if c < limit {
limit = int(speedUpFactor * float64(c))
attacks = GetAttack(s, downSlash, upSlash)
}
}
}
if c == 0 {
break outer
}
// 6. 若陷入局部最优,即一直存在冲突不可解且交换了一定轮次,则转 1
loopSteps += len(attacks)
}
}
// 7. 输出结果并结束
// 略去打印的时间
d := time.Since(start)
printResult(s, n)
fmt.Printf("%d皇后问题,耗时:%s", n, d)
}
// 更新交换后的棋盘
func updateQueen(s, downSlash, upSlash []int, a, b int) {
n := len(s)
aj, bj := s[a], s[b]
// 把两个棋子拿出,斜线上棋子-1
upSlash[a+aj]--
upSlash[b+bj]--
downSlash[aj-a+n]--
downSlash[bj-b+n]--
// 交换
s[a], s[b] = s[b], s[a]
// 交换后放入,斜线上棋子+1
upSlash[a+bj]++
upSlash[b+aj]++
downSlash[bj-a+n]++
downSlash[aj-b+n]++
}
// 交换前计算冲突数,若冲突数减少,则可交换
func calConflictBeforeSwap(s, downSlash, upSlash []int, a, b int) int {
var c int
n := len(s)
aj := s[a]
bj := s[b]
// 计算拿起棋子后的斜线冲突
// 冲突数 - (n-1)
c -= upSlash[aj+a] - 1
c -= upSlash[bj+b] - 1
if a+aj == b+bj {
c++
}
c -= downSlash[aj-a+n] - 1
c -= downSlash[bj-b+n] - 1
if aj-a == bj-b {
c++
}
// 计算放下棋子的斜线冲突
c += upSlash[bj+a]
c += upSlash[aj+b]
if a+bj == b+aj {
c++
}
c += downSlash[bj-a+n]
c += downSlash[aj-b+n]
if bj-a == aj-b {
c++
}
return c
}
// 除 i 外的随机数
func randInt(i, n int) int {
r := getRT()
j := r.Intn(n)
for i == j {
j = r.Intn(n)
}
return j
}
// 输出结果
func printResult(s []int, n int) {
fmt.Println("结果为:")
for i := 0; i < n; i++ {
fmt.Println(i, s[i])
}
}
// 获取随机种子
func getRT() *rand.Rand {
return rand.New(rand.NewSource(time.Now().UnixNano()))
}
// 通过减少数组下标,进行没有重复数据的生成
func RandomCreateNQByArray(n int) []int {
result := make([]int, n)
a := make([]int, n)
for i := 0; i < n; i++ {
a[i] = i
}
var index int
r := getRT()
N := n
for N > 0 {
randNum := r.Intn(N)
result[index] = a[randNum]
index++
N--
a[randNum] = a[N]
}
return result
}
// 使用空间换时间计算冲突
func GetConflictsByArray(s, downSlash, upSlash []int) int {
var c int
n := len(s)
// 列-行+n 列+行
// 只可能是 斜线 上冲突
for i := 0; i < n; i++ {
downSlash[s[i]-i+n]++
upSlash[s[i]+i]++
}
// 计算冲突,这里计算冲突的方式是用 两两组合 的方式:C_n^2
for i := 0; i < 2*n; i++ {
c += downSlash[i] * (downSlash[i] - 1) / 2
c += upSlash[i] * (upSlash[i] - 1) / 2
}
return c
}
// 计算出冲突的棋子个数和冲突的棋子数组
func GetAttack(s, downSlash, upSlash []int) []int {
var result []int
n := len(s)
for i := 0; i < n; i++ {
j := s[i]
if upSlash[i+j] > 1 || downSlash[j-i+n] > 1 {
result = append(result, i)
}
}
return result
}
func main() {
n := 10000
NQueenLocalSearch(n)
}
在自己的笔记本下,性能结果如下
皇后个数 | 100000 | 200000 | 400000 |
---|---|---|---|
时间 | 18s | 32s | 1m3s |
该程序使用 Go 语言编写,在性能上略逊色于 C 语言稍稍,但对结果没有太大影响
该程序在课件的描述上,借鉴了网上的 QS2 算法的实现,将PPT的递归描述转化为递推描述,并且使用两个upSlash
和downSlash
斜线数组进行冲突数的存储和提取,达到加速的效果;
参考的 QS2 算法:https://blog.csdn.net/yongnuzhibu/article/details/7178112
大致流程和加速细节如下:
C_n^2
在随机生产棋盘上,采用一个数组进行缓存加速;这个可以用在一个通用的算法上,即n个数的乱序排列
// 通过减少数组下标,进行没有重复数据的生成
func RandomCreateNQByArray(n int) []int {
result := make([]int, n)
a := make([]int, n)
for i := 0; i < n; i++ {
a[i] = i
}
var index int
r := getRT()
N := n
for N > 0 {
randNum := r.Intn(N)
result[index] = a[randNum]
index++
N--
a[randNum] = a[N]
}
return result
}
使用两个数组upSlash
和downSlash
进行两种斜线上的冲突的缓存;在后面的模拟冲突和更新棋盘直接对两个数组进行操作即可
棋子的冲突计算方式为两两组合:C_n^2
,而不是棋子数+1
这里的交换策略不再是交换所有棋子,而是将冲突棋子和另一个随机棋子进行交换,被选中的冲突棋子又称为被攻击棋子;当被攻击棋子使用完,即相当于没有冲突,得解。
若陷入局部最优,则会一直存在可被攻击棋子,这里通过引入 循环系数 和 加速系数 对被攻击棋子 的筛选和遍历进行加速,当小于某个阈值时,会进行 棋盘的重新生成 或 被攻击棋子的重新生成