算法:n后问题_局部搜索算法

n后问题_局部搜索算法

题目

局部搜索解n皇后,并测试n的极限

回溯法难以求解更大规模的n皇后问题,但是基于概率的局部搜索算法可以解决一定规模上的 n 皇后问题

思路

根据课件上的局部搜索算法思路,n皇后具体流程为:

  1. 随机将 N 皇后分布在棋盘上,并保证每行每列仅有一个皇后
  2. 计算皇后间的冲突数 conflicts,注意这里只需要计算斜线的冲突,行列不需要
  3. 如果冲突为0则转(6)
  4. 对于棋盘上的任意两个皇后,交换位置,如果交换后冲突减少,则接受交换,更新冲突数 conflicts
  5. 如果陷入了局部极小,即交换了所有皇后后,冲突数不能下降,则转(1)
  6. 输出结果,结束

优化思路

对于上面的思路,现提供 QS2 算法思路,主要优化点在于交换策略不再是任意交换,而是选择陷入冲突的棋子进行交换

  1. 随机地将N 个皇后分布在棋盘上,每行、每列只有一个皇后。
  2. 计算皇后间的冲突数Conflicts
  3. 如果冲突数等于0等转(9)
  4. 获取棋盘上陷入冲突的所有棋子
  5. 将冲突棋子和其他棋子进行交换,冲突降低则更新棋盘
  6. 重新计算冲突,冲突为0则转(9)
  7. 加速: 若冲突数小于某个阈值,则更新阈值并重新获取冲突 棋子
  8. 如果陷入了局部极小(达到循环次数上限),即交换了所有的皇后后,冲突数仍不能下降,则转(1)
  9. 输出结果并结束

源代码

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的递归描述转化为递推描述,并且使用两个upSlashdownSlash斜线数组进行冲突数的存储和提取,达到加速的效果;

参考的 QS2 算法:https://blog.csdn.net/yongnuzhibu/article/details/7178112

大致流程和加速细节如下:

流程

  1. outer repeat:冲突不为0
    1. 随机生产棋盘
    2. 获取棋盘中的冲突:这里的冲突计算是两两组合,即C_n^2
    3. 若冲突为0,break
    4. 获取棋盘上陷入冲突的所有棋子
    5. inner repeat:遍历冲突棋子
      1. 对冲突棋子进行模拟交换的冲突计算,若冲突减少,则更新棋盘,否则继续取其他冲突棋子
      2. 若冲突为0,break
      3. 若陷入局部最优,即冲突棋子拿完,仍没有解,则继续循环 outer repeat
  2. 结束并打印

加速

1. 生成棋盘

在随机生产棋盘上,采用一个数组进行缓存加速;这个可以用在一个通用的算法上,即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
}

2. 获取棋盘冲突

使用两个数组upSlashdownSlash进行两种斜线上的冲突的缓存;在后面的模拟冲突和更新棋盘直接对两个数组进行操作即可

棋子的冲突计算方式为两两组合:C_n^2,而不是棋子数+1

3. 引入攻击棋子

这里的交换策略不再是交换所有棋子,而是将冲突棋子和另一个随机棋子进行交换,被选中的冲突棋子又称为被攻击棋子;当被攻击棋子使用完,即相当于没有冲突,得解。

4. 引入加速系数

若陷入局部最优,则会一直存在可被攻击棋子,这里通过引入 循环系数 和 加速系数 对被攻击棋子 的筛选和遍历进行加速,当小于某个阈值时,会进行 棋盘的重新生成 或 被攻击棋子的重新生成

你可能感兴趣的:(算法,n后问题,局部搜索算法)