三阶幻方
三阶幻方是最简单的幻方,又叫九宫格,是由1,2,3,4,5,6,7,8,9九个数字组成的一个三行三列的矩阵(如右图示),其对角线、横行、纵向的
的和都为15,称这个最简单的幻方的幻和为15。中心数为5。
三阶幻方共有八组结果
8
276
951
438
294
753
618
438
951
276
492
357
816
618
753
294
672
159
834
816
357
492
834
159
672
使用深度优先搜索算法,暴力搜索所有解空间,找到符合要求的一组数,放入ans数组
注意
1.浅拷贝的问题
a = [1, 2, 3]
b = a
c = a.copy()
b[0] = 111
c[0] = 222
print(a, b, c)
[111, 2, 3] [111, 2, 3] [222, 2, 3]
2.与js不同,Python中对空数组执行pop操作会报错,但js不会
js
a = []
console.log(a.pop());
undefined
py
a=[]
a.pop()
IndexError: pop from empty list
实现代码
'''
三阶幻方8个
四阶幻方7040个
'''
# 幻方阶数
n = 3
# 幻和,即每行,每列和对角线的和
s = n * n * (n * n + 1) // 2 // n
# 数组
a = []
# 使用过的数字
used = set()
# 答案数组
ans = []
# 检查是否满足幻方条件
def check():
flag = True
for i in range(n):
# 行的幻和
r = sum([a[i * n + j] for j in range(n)])
# 列的幻和
c = sum([a[i + j * n] for j in range(n)])
flag = flag and r == c == s
# 主对角线和次对角线的幻和
s1 = sum([a[i * n + i] for i in range(n)])
s2 = sum([a[n * i - i] for i in range(1, n + 1)])
flag = flag and s1 == s2 == s
return flag
# 搜索算法,参数表示准备放入第m个数字
def dfs(m):
# 表示已经放入足够的数字了,进行检验
if m == n * n:
if check():
print(a)
ans.append(a.copy())
# 弹出最后一位,继续递归,并且移除used集合中的数
used.remove( a.pop())
return
# 行剪枝
if m > 0 and m % n == 0 and sum([a[int(m / n - 1) * n + i] for i in range(n)]) != s:
used.remove(a.pop())
return
# 对角线剪枝
if m == n * (n - 1) + 1:
if sum([a[n * i - i] for i in range(1, n + 1)]) != s:
used.remove(a.pop())
return
# 列剪枝,在放入最后一行时,进行列剪枝
if m > n * (n - 1):
c = m - n * (n - 1) - 1
if sum([a[c + i * n] for i in range(n)]) != s:
used.remove(a.pop())
return
# 数组长度不够时
for i in range(1, n * n + 1):
if i not in used:
a.append(i)
used.add(i)
dfs(m + 1)
# print(a, m)
if len(a) > 0:
used.remove(a.pop())
def show(arr):
s = ''
for index, i in enumerate(arr):
s += str(i)
if index % n == n - 1:
s += '\n'
return s
dfs(0)
print(len(ans))
for i in ans:
print(show(i))
执行结果分析
该方法完全采用暴力搜索,查看各个函数的执行次数
其中check函数只有当数组元素个数大于9时才会执行,所以执行次数为
9!=362880
其中dfs函数构造数组由一位开始所以总执行次数为
使用scipy求解
from scipy.special import comb, perm
print(
sum([perm(9, i) for i in range(10)])
)
986410.0
现在进行剪枝操作
减少搜索范围
第一种剪枝方法,在dfs函数开始处进行剪枝,此时可以看到,dfs和check函数的执行次数已经显著下降了,我们只用了每行的和必须为幻和这个条件
# 搜索算法,参数表示准备放入第m个数字
def dfs(m):
if m > 0 and m % n == 0 and sum([a[int(m / n - 1) * n + i] for i in range(n)]) != s:
t = a.pop()
used.remove(t)
return
# 表示已经放入足够的数字了,进行检验
if m == n * n:
if check():
ans.append(a.copy())
t = a.pop()
# 弹出最后一位,继续递归,并且移除used集合中的数
used.remove(t)
return
# 数组长度不够时
for i in range(1, n * n + 1):
if i not in used:
a.append(i)
used.add(i)
dfs(m + 1)
# print(a, m)
if len(a) > 0:
t = a.pop()
used.remove(t)
第二种剪枝方法
直接在放入每行最后一个数字的时候进行剪枝,该数字应该为幻和减去每行之和,相比于暴力搜索同样减少了很多
# 搜索算法,参数表示准备放入第m个数字
def dfs(m):
# 表示已经放入足够的数字了,进行检验
if m == n * n:
if check():
ans.append(a.copy())
t = a.pop()
# 弹出最后一位,继续递归,并且移除used集合中的数
used.remove(t)
return
# 准备放入每行的最后一个数字时
if m > 0 and m % n == n - 1:
r = int(m / n)
t = s - sum([a[r * n + i] for i in range(n - 1)])
# 如果t已经被使用了,则说明该行之和不可能为幻和则回溯
if t in used:
used.remove(a.pop())
return
else:
a.append(t)
used.add(t)
dfs(m + 1)
else:
for i in range(1, n * n + 1):
if i not in used:
a.append(i)
used.add(i)
dfs(m + 1)
# print(a, m)
if len(a) > 0:
t = a.pop()
used.remove(t)
加上列剪枝
加上对角线剪枝
完整剪枝代码
# 搜索算法,参数表示准备放入第m个数字
def dfs(m):
# 表示已经放入足够的数字了,进行检验
if m == n * n:
if check():
ans.append(a.copy())
# 弹出最后一位,继续递归,并且移除used集合中的数
used.remove( a.pop())
return
# 行剪枝
if m > 0 and m % n == 0 and sum([a[int(m / n - 1) * n + i] for i in range(n)]) != s:
used.remove(a.pop())
return
# 对角线剪枝
if m == n * (n - 1) + 1:
if sum([a[n * i - i] for i in range(1, n + 1)]) != s:
used.remove(a.pop())
return
# 列剪枝,在放入最后一行时,进行列剪枝
if m > n * (n - 1):
c = m - n * (n - 1) - 1
if sum([a[c + i * n] for i in range(n)]) != s:
used.remove(a.pop())
return
# 数组长度不够时
for i in range(1, n * n + 1):
if i not in used:
a.append(i)
used.add(i)
dfs(m + 1)
# print(a, m)
if len(a) > 0:
used.remove(a.pop())