1. 问题描述:
把1~16的数字填入4x4的方格中,使得行、列以及两个对角线的和都相等,满足这样的特征时称为:四阶幻方。 四阶幻方可能有很多方案。如果固定左上角为1,请计算一共有多少种方案。
比如:
1 2 15 16
12 14 3 5
13 7 10 4
8 11 6 9
以及:
1 12 13 8
2 14 7 11
15 3 10 6
16 5 4 9
就可以算为两种不同的方案。
输出
请提交左上角固定为1时的所有方案数字
来源:http://oj.ecustacm.cn/problem.php?id=1265
2. 思路分析:
① 分析题目可以知道这是一道需要求解全部方案的题目,所以需要使用递归来搜索所有的可能性。一开始想到的是逐行生成二维列表中的数字,使用for循环尝试填进当前未被使用过的数字,然后往下递归,当递归到每一行末尾的时候(第四列的时候)那么判断当前这一行的和是否满足34(因为题目中要求每一行、每一列、对角线都是相等而1-16的和除以4那么就可以得到每一行、列、对角线的和都为34),当递归到第四行的时候依次判断对应当前列数的对应的和是否满足34如果不满足那么就需要return到上一层,并且在每一行三个数字填好之后判断当前填入的数字是否加上16都是小于34的如果小于34那么说明当前循环中的数字都是不满足题目要求的直接return上一层(这些判断都是剪枝),但是使用python语言还是运行了很久没有计算出结果,可能还是存在很多不满足条件继续递归的情况。
② 后面与同学的讨论中发现了一种比较好的方法,思路是使用一维列表来记录当前已经生成的数字,并且将一维列表的下标在二维列表中编好填入数字的顺序(其实就是将当前一维列表的数字根据固定的位置填入到二维坐标中),这样就可以在递归方法一开始的时候进行判断是否满足条件,也即可以根据一维列表中生成的元素个数对应的行、列、对角线的和提前判断是否可以往下递归,编号的顺序为如下图所示,下面这个图是规定好的顺序(可以为其他的顺序关键是要求限制的条件尽可能多这样会过滤掉很多不满足条件往下递归的例子),相当于我是将生成好的数字按照图中的顺序填进去(其实与逐行生成数字没有什么两样只是这里规定好了顺序了)。比如列表长度为4的时候那么可以判断第一行的和是否满足34,当列表长度为7的时候判断判断第一列的和是否满足34,为列表长度为10的时候判断第二列的和是否满足34,为列表长度为11的时候判断3/10/8/6对角线的和是否满足34,为12的时候判断第二行的和是否满足34....当发现不满足条件的时候直接return到上一层这样会很快计算出结果。这个思路确实是个不错的思路,按照元素个数越少但是可以判断的条件越多的一维列表下标的顺序填进二维列表中可以避免大量不满足条件的递归,这个比在二维列表中逐行生成元素判断的效率是高很多的,之后遇到类似的二维坐标满足某行、列、对角线之间的关系的搜索问题都是可以借助于这种方式解决的。
3. 代码如下:
一开始依次生成二维列表中每一行数字的代码(运行了很久没有计算出最终的结果,还是存在很多不符合条件往下递归的搜索)
from typing import List
res = 0
count = 0
def dfs(x: int, y: int, matrix: List[List[int]], nums: List[int], vis: List[int]):
global count
count += 1
global res
if x == 3 and y == 4:
# 验证对角线
if sum(matrix[i][i] for i in range(4)) != 34: return
if sum(matrix[i][3 - i] for i in range(4)) != 34: return
res += 1
print(matrix)
return
# 因为每一次递归的时候都是列数加1的当发现列数为4的时候那么就要换下一行了
if y == 4:
x += 1
y = 0
for i in range(len(nums)):
if vis[nums[i]] == 0:
# 减枝
matrix[x][y] = nums[i]
# 验证行
if y == 3:
# 前面三个数字相加加上16都小于34说明后面的数字都需要尝试了需要退回到上一层尝试其他的数字
if sum(matrix[x][i] for i in range(3)) + 16 < 34:
matrix[x][y] = 0
return
# 当前这一行数字和不等于34那么需要尝试循环中的其他数字
elif sum(matrix[x][i] for i in range(4)) != 34:
matrix[x][y] = 0
continue
# 不能够return而是continue尝试其他的可能的方案
# return
# 验证列
if x == 3:
# 当第三行的时候验证每一列是否满足要求
if sum(matrix[i][y] for i in range(3)) + 16 < 34: return
elif sum(matrix[i][y] for i in range(4)) != 34:
matrix[x][y] = 0
continue
# return
vis[nums[i]] = 1
dfs(x, y + 1, matrix, nums, vis)
matrix[x][y] = 0
vis[nums[i]] = 0
if __name__ == '__main__':
nums = [i for i in range(1, 17)]
vis = [0] * 17
matrix = [[0] * 4 for i in range(4)]
dfs(0, 0, matrix, nums, vis)
print(res)
一维列表的下标顺序在二维列表中编号:
from typing import List
res = 0
def dfs(count: int, rec: List[int], vis: List[int]):
global res
# 根据一维列表在二维列表的位置判断是否满足条件
if count >= 4 and rec[0] + rec[1] + rec[2] + rec[3] != 34:
return
elif count >= 7 and rec[0] + rec[4] + rec[5] + rec[6] != 34:
return
elif count >= 10 and rec[1] + rec[7] + rec[8] + rec[9] != 34:
return
elif count >= 11 and rec[3] + rec[6] + rec[8] + rec[10] != 34:
return
elif count >= 12 and rec[4] + rec[7] + rec[10] + rec[11] != 34:
return
elif count >= 14 and rec[5] + rec[8] + rec[12] + rec[13] != 34:
return
elif count >= 15 and rec[2] + rec[10] + rec[12] + rec[14] != 34:
return
elif count >= 16 and (rec[6] + rec[9] + rec[14] + rec[15] != 34 or rec[3] + rec[11] + rec[13] + rec[15] != 34 or rec[0] + rec[7] + rec[12] + rec[15] != 34):
return
if count == 16: res += 1
for i in range(2, 17):
# 未使用到当前的数字
if vis[i] == 0:
vis[i] = 1
rec.append(i)
dfs(count + 1, rec, vis)
# 回溯
vis[i] = 0
rec.pop()
if __name__ == '__main__':
# 使用一维列表进行优化, 一维列表在递归的时候可以将坐标映射到二维列表进行剪枝
import time
# 计算起始时间
start = time.time()
rec = [1]
vis = [0] * 17
vis[1] = 1
dfs(1, rec, vis)
print(res)
print(time.time() - start)