说明
这一题我不会做,看了官方给出的标准答案之后才明白,我把我学到的思路写下来。
题目
蓝桥学院由21栋教学楼组成,教学楼编号1到21。对于两栋教学楼a和b,当a和b互质时,a和b之间有一条走廊直接相连,两个方向皆可通行,否则没有直接连接的走廊。
小蓝现在第一栋教学楼,他想要访问每栋教学楼正好一次,最终回到第一栋教学楼(即走一条哈密尔顿回路),请问他有多少种不同的访问方案?两个访问方案不同是指存在某个i,小蓝在两个访问方法中访问完教学楼i后访问了不同的教学楼。
官方给出的标准答案代码
from math import gcd
n = int(input())
m = 1 << n
dp = [[0 for j in range(n)] for i in range(m)]
load = [[False for j in range(n)] for i in range(n)]
for i in range(1, n + 1):
for j in range(1, n + 1):
if gcd(i, j) == 1:
load[i - 1][j - 1] = True
dp[1][0] = 1
for i in range(1, m):
for j in range(n):
if i >> j & 1:
for k in range(n):
if i - (1 << j) >> k & 1 and load[k][j]:
dp[i][j] += dp[i - (1 << j)][k]
print(sum(dp[m - 1]) - dp[m - 1][0])
我的理解
状态压缩
参考链接:https://blog.csdn.net/weixin_45697774/article/details/104874248
状态压缩属于运筹学的动态规划问题。动态规划问题的求解方法大致分成两种,一种是对递归问题的记忆化求解,另一种是把大问题看作是多阶段的决策求解。本题使用多阶段决策求解。
状态压缩是用二进制或者三进制等来表示某种状态,然后把这些二进制或者三进制压缩成10进制,从而达到节约存储空间的目的。
解题思路
- 题目说如果两个楼的编号互为质数,那么这两个楼之间就有一条路。那么我们就先求出来哪些楼之间存在路。
用一个二维数组load存储各个楼之间的互通关系,比如load[1][3]=True
表示1号楼和3号楼编号互质,它们之间存在通路;load[2][4]=False
表示2号楼和三号楼编号不互质,它们之间不存在通路。 - 对于状态压缩,必须需要定义一个二维数组dp(也有可能是三维数组,视具体情况而定,本题必须定义二维数组)用来存储在
i
结点时是state
状态的所有可能的数量。
dp[i][state]
表示当前正在i
结点,当前的系统状态是state
时的所有可能的数量。比如dp[8][6]=24
表示当前正在8号结点,当前已经走过的结点是3号和2号的所有可能的结果的数量是24。
对当前已经走过的结点是3号和2号的解释:state=6,6的二进制是0110,按照从右往左的顺序,编号从1开始,对应的编号是2和3的位置是1,表示已经走过了,为0表示还没有走过。如果所有的结点都走过了,那么state的二进制全部为1
- 因为
dp[i][state]
中的i
表示当前所处的结点编号,state
表示当前系统的状态。所以对于题目中的21个楼,i
的值的范围是1~21.state
的值的范围是000…01到111…11(21位二进制)。 - 状态压缩数组定义完成之后,我们需要把所有可能的情况都遍历一遍,把所有满足条件的情况相加就得到最终的结果,具体方法是:
第一步,先用一个for循环遍历state
的所有情况(000…11到111…11)
第二步,找到一个state
中二进制是1的一个结点
第三步,再找到是state
中第二个二进制是1的结点。第一个找到的1的结点编号作为当前正处在的结点,第二个找到的1的结点编号作为上一次到达的结点的编号。
第四步,把上一次的所有可能结果和这次所有可能的结果相加。
第五步,从第一部循环,直到所有的state
遍历完毕。
我写的代码
"""
蓝桥学院由21栋教学楼组成,教学楼编号1到21。
对于两栋教学楼a和b,当a和b互质时,a和b之间有一条走廊直接相连,两个方向皆可通行,否则没有直接连接的走廊。
小蓝现在第一栋教学楼,他想要访问每栋教学楼正好一次,最终回到第一栋教学楼(即走一条哈密尔顿回路),请问他有多少种不同的访问方案?
两个访问方案不同是指存在某个i,小蓝在两个访问方法中访问完教学楼i后访问了不同的教学楼。"""
import math
import time
"""运算符优先级:括号>单目运算符>乘除>加减>移位运算符>比较运算符"""
n = int(input('输入教学楼的个数:'))
m = 1 << n # m 是所有可能的状态(从000...00到1111...11)
startTime = time.time()
"""定义一个二维数组load,用来存储节点i和j是否互质的信息,比如load[i][j] = True 表示i和j互质,若load[i][j] = False 表示i和j不互质"""
load = [[False for i in range(n)] for j in range(n)]
for i in range(1,n+1):
for j in range(1,n+1):
if math.gcd(i, j) == 1: # 如果i和j互质
load[i-1][j-1] = True
"""定义状态压缩数组dp[i][state],i表示当前在结点i,当前的状态是state时的所有方案数目,比如dp[2][4]表示当前在2号结点,已经走过的结点是4(0..00100)的所有可能的方案数目"""
dp = [[0 for i in range(m)] for j in range(n+1)] # n + 1 是因为一共有个结点并且让下标从1开始,m 是因为一共有m种状态,而state只是所有状态中的一种(
# 000..00-111..11中的一个,用m就可以遍历000..00-111..11中的任意一个)
dp[1][1] = 1 # 当前还没有走,起始结点是1,此时的状态state=1(0000..01)时所有可能的结果只有1种
for i in range(1, m): # 遍历所有可能的状态,状态000..00认为不算,所以从1开始
for j in range(n): # 遍历每一个结点,j表示当前所处的结点编号
if i >> j & 1: # 如果i从右边往左边数第j位是1,表示当前在从右往左数第j位结点
for k in range(n): # 再一次遍历1到n的所有结点,看看哪一些结点已经被走过了(1),哪一些结点还没有被走过(0)
if i - (1 << j) >> k & 1 and load[j][k]: # i - (1<<j)表示把i的从右往左数第j位变成0,i -(1 << j) >> k & 1表示看看i -(1
# << j)的从右往左数第k位是不是1
dp[j+1][i] += dp[k+1][i - (1 << j)] # dp[j][i]的数目就是从上一个结点到这一个结点的数目加上上一个结点之前的数目i - (1 << j)是上一个结点的状态
su = 0
for i in range(1, n+1):
# print(dp[i][m-1])
su += dp[i][m-1] # m-1就是全1(111...11)的状态
endTime = time.time()
"""881012367360"""
print(su)
print('运行时间:', endTime - startTime)