使用Dancing link X (舞蹈链)求解dominosa游戏
puzzle team club做了一个网站,里面有好多解谜游戏可以玩。身为程序员玩这种游戏,就应该想一些使用程序解决的方法。
但是里面好多游戏还蛮难解的。。。我是说写出简明扼要的程序,并且像在ACM赛场一样执行和开发时间占优。所有游戏都需要用搜索完成毫无疑问。
但是不少游戏都涉及到图论,单纯的搜索很难一下想到方法,像slither link(连接回路)、hashi(搭桥)、nirikabe(围城)和slithes(连接)这几个游戏就是跟图论相关。
其他看起来不像图论的游戏,像skyscrapers(摩天大楼)、star battle(星星战斗),属于有限次数完全覆盖,很难一下想到简明的方法。
这里只求解能使用舞蹈链解决的,相对简单的dominosa游戏(链接:https://www.puzzle-dominosa.com/),这个游戏跟舞蹈链天生一对(误),跟数独的规则差不多,场上所有数字都必须用到1次,用到的数字用于连接数对(任意顺序数对算同种),所有数对必须并且只出现1次,属于精确覆盖题目。
这里就可以想怎么转化为舞蹈链了,这里数对的所有可能连接方式就是舞蹈链的行,而数对的限制以及占用数字的限制则是舞蹈链的列,一旦有一个列被使用,舞蹈链会把其他用到这个列的所有行去除,以达到快速剪枝的目的。
在这个游戏里面,数对可以横向连接((i,j)-(i,j+1)),也可竖向连接((i,j)-(i+1,j)),这两种连接方式分别是(r-1)c种和r(c-1)种,就是说行数有大约2cr;数字一般有0-(c-1)这几个数字,可以组成c(c+1)/2种数对(这个站点的dominosa游戏列数一般比行数大1),并且所有r*c个方格必须占据,只能占据1次,这意味着列数大约有3cr/2,并且这个站点的这种谜题只有1个解答,搜索宽度不会太长,最多只有20*19的谜题,执行时间不会太长。(这个站点还可以找到一个画方格游戏shikaku,也可以用精确覆盖,但是那个游戏长宽大得不像话)
基于这个思路,我们开始写Python代码(DLX模板是用的https://github.com/DavideCanton/DancingLinksX这个链接里面的代码,我们只是构造表而已):
首先,构造舞蹈表的列头:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
def get_names(r, c, m): cnt = 0 for i in range(r): for j in range(c): yield f'L({i},{j})' # location pair cnt = cnt + 1 for i in range(m+2): for j in range(i): yield f'O({j},{i-1})' # number pair cnt = cnt + 1
第一个二重循环对应占用数字条件列头,第一个二重循环对应占用数对条件列头
构造每个可能的数对对应条件约束:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
def compute_row(i, j, a, b, isHorizontal, r, c): # H is 0 .. r*(c-1)-1 # V is r*(c-1) .. r*(c-1)+c*(r-1)-1 # O is r*c*2-r-c .. r*c*2-r-c+(m-1)*m//2-1 if a < b: t = a a = b b = t row = [] if isHorizontal: row.append(c*i+j) row.append(c*i+j+1) else: row.append(c*i+j) row.append(c*(i+1)+j) row.append(r*c+a*(a+1)//2+b) return row
完成舞蹈链遍历,还要把结果按照正确的格式输出。
为了连接字符串方便,这里声明个给单个或多个字符串赋值的函数:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
def insertToStr(origin, i, j, value): if(origin[i][j] != '+'): origin[i] = origin[i][:j] + value + origin[i][j+1:]
为了能按照指定格式输出答案,我们这里声明一个类:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
class PrintFirstSol: def __init__(self, r, c): self.r = r self.c = c def __call__(self, sol): maxNum = 0 chessWidth = 0 for v in sol.values(): k = list(map(lambda a:int(a),v[2].split('(')[1].replace(')','').split(','))) maxNum = maxNum if k[0] < maxNum else k[0] maxNum = maxNum if k[1] < maxNum else k[1] while maxNum > 0: chessWidth += 1 maxNum //= 10 line = ['+','|'] for _ in range(self.c): line[0] += ('-' * chessWidth + '+') line[1] += (' ' * chessWidth + '|') solImg = [line[0]] for _ in range(self.r): solImg.append(line[1]) solImg.append(line[0]) timeDelay = 0 for v in sol.values(): v.sort() v[0] = list(map(lambda a:int(a),v[0].split('(')[1].replace(')','').split(','))) v[1] = list(map(lambda a:int(a),v[1].split('(')[1].replace(')','').split(','))) v[2] = v[2].split('(')[1].replace(')','').split(',') # This code had a bug there, I fix it and chess image which higher than 9 don't mess # 之前这里代码有bug,我修掉之后,棋盘大小超过9的排布不会混乱 i = v[0][0] j = v[0][1] i1 = v[1][0] j1 = v[1][1] if i > i1: i, i1 = i1, i if j > j1: j, j1 = j1, j if(v[0][1] != v[1][1]): solImg[2*i+1] = solImg[2*i+1][:(1+chessWidth)*j]\ + ('|%' + str(chessWidth) + 'd')%(chess[i][j])\ + ' ' + ('%' + str(chessWidth) + 'd|')%(chess[i][j+1])\ + solImg[2*i+1][(1+chessWidth)*(j+2)+1:] else: startCut = (1+chessWidth)*j+1 endCut = (1+chessWidth)*(j+1) solImg[2*i+1] = solImg[2*i+1][:startCut] + ('%' + str(chessWidth) + 'd')%(chess[i][j])\ + solImg[2*i+1][endCut:] solImg[2*i+2] = solImg[2*i+2][:startCut] + (' ' * chessWidth)\ + solImg[2*i+2][endCut:] solImg[2*i+3] = solImg[2*i+3][:startCut] + ('%' + str(chessWidth) + 'd')%(chess[i+1][j])\ + solImg[2*i+3][endCut:] # # solImg = reduce(lambda a,b:a+'\n'+b,solImg) print(solImg) with open('result.txt','w') as f:f.write(solImg) return True
主要的处理步骤在main()这个函数里面:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
def main(): start = time.time() with open('dominosaChess.txt','r') as f: chessStr = f.read() rowStrs = chessStr.split('\n') rowsize = len(rowStrs) colSize = len(rowStrs[0].split(' ')) maxnum = 0 for rowStr in rowStrs: row = [] for colStr in rowStr.split(' '): maxnum = maxnum if int(colStr) < maxnum else int(colStr) row.append(int(colStr)) chess.append(row) #print(chess) d = DancingLinksMatrix(get_names(rowsize, colSize, maxnum)) for i in range(rowsize): for j in range(colSize-1): row = compute_row(i, j, chess[i][j], chess[i][j+1], True, rowsize, colSize) #print(row) d.add_sparse_row(row, already_sorted=True) # for i in range(rowsize-1): for j in range(colSize): row = compute_row(i, j, chess[i][j], chess[i+1][j], False, rowsize, colSize) #print(row) d.add_sparse_row(row, already_sorted=True) d.end_add() p = PrintFirstSol(rowsize, colSize) AlgorithmX(d, p)() end = time.time() print('the DLX runtime is : ' + str(end-start) + 's')
总的代码就是这样:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
""" dominosa solver using Dancing Links. """ from functools import reduce from dlmatrix import DancingLinksMatrix from alg_x import AlgorithmX import math import time __author__ = 'Funcfans' chess = [] def insertToStr(origin, i, j, value): if(origin[i][j] != '+'): origin[i] = origin[i][:j] + value + origin[i][j+1:] def get_names(r, c, m): cnt = 0 for i in range(r): for j in range(c): yield f'L({i},{j})' # location pair cnt = cnt + 1 for i in range(m+2): for j in range(i): yield f'O({j},{i-1})' # number pair cnt = cnt + 1 # def compute_row(i, j, a, b, isHorizontal, r, c): # H is 0 .. r*(c-1)-1 # V is r*(c-1) .. r*(c-1)+c*(r-1)-1 # O is r*c*2-r-c .. r*c*2-r-c+(m-1)*m//2-1 if a < b: t = a a = b b = t row = [] if isHorizontal: row.append(c*i+j) row.append(c*i+j+1) else: row.append(c*i+j) row.append(c*(i+1)+j) row.append(r*c+a*(a+1)//2+b) return row class PrintFirstSol: def __init__(self, r, c): self.r = r self.c = c def __call__(self, sol): maxNum = 0 chessWidth = 0 for v in sol.values(): k = list(map(lambda a:int(a),v[2].split('(')[1].replace(')','').split(','))) maxNum = maxNum if k[0] < maxNum else k[0] maxNum = maxNum if k[1] < maxNum else k[1] while maxNum > 0: chessWidth += 1 maxNum //= 10 line = ['+','|'] for _ in range(self.c): line[0] += ('-' * chessWidth + '+') line[1] += (' ' * chessWidth + '|') solImg = [line[0]] for _ in range(self.r): solImg.append(line[1]) solImg.append(line[0]) timeDelay = 0 for v in sol.values(): v.sort() v[0] = list(map(lambda a:int(a),v[0].split('(')[1].replace(')','').split(','))) v[1] = list(map(lambda a:int(a),v[1].split('(')[1].replace(')','').split(','))) v[2] = v[2].split('(')[1].replace(')','').split(',') # This code had a bug there, I fix it and chess image which higher than 9 don't mess # 之前这里代码有bug,我修掉之后,棋盘大小超过9的排布不会混乱 i = v[0][0] j = v[0][1] i1 = v[1][0] j1 = v[1][1] if i > i1: i, i1 = i1, i if j > j1: j, j1 = j1, j if(v[0][1] != v[1][1]): solImg[2*i+1] = solImg[2*i+1][:(1+chessWidth)*j]\ + ('|%' + str(chessWidth) + 'd')%(chess[i][j])\ + ' ' + ('%' + str(chessWidth) + 'd|')%(chess[i][j+1])\ + solImg[2*i+1][(1+chessWidth)*(j+2)+1:] else: startCut = (1+chessWidth)*j+1 endCut = (1+chessWidth)*(j+1) solImg[2*i+1] = solImg[2*i+1][:startCut] + ('%' + str(chessWidth) + 'd')%(chess[i][j])\ + solImg[2*i+1][endCut:] solImg[2*i+2] = solImg[2*i+2][:startCut] + (' ' * chessWidth)\ + solImg[2*i+2][endCut:] solImg[2*i+3] = solImg[2*i+3][:startCut] + ('%' + str(chessWidth) + 'd')%(chess[i+1][j])\ + solImg[2*i+3][endCut:] # # solImg = reduce(lambda a,b:a+'\n'+b,solImg) print(solImg) with open('result.txt','w') as f:f.write(solImg) return True def main(): start = time.time() with open('dominosaChess.txt','r') as f: chessStr = f.read() rowStrs = chessStr.split('\n') rowsize = len(rowStrs) colSize = len(rowStrs[0].split(' ')) maxnum = 0 for rowStr in rowStrs: row = [] for colStr in rowStr.split(' '): maxnum = maxnum if int(colStr) < maxnum else int(colStr) row.append(int(colStr)) chess.append(row) #print(chess) d = DancingLinksMatrix(get_names(rowsize, colSize, maxnum)) for i in range(rowsize): for j in range(colSize-1): row = compute_row(i, j, chess[i][j], chess[i][j+1], True, rowsize, colSize) #print(row) d.add_sparse_row(row, already_sorted=True) # for i in range(rowsize-1): for j in range(colSize): row = compute_row(i, j, chess[i][j], chess[i+1][j], False, rowsize, colSize) #print(row) d.add_sparse_row(row, already_sorted=True) d.end_add() p = PrintFirstSol(rowsize, colSize) AlgorithmX(d, p)() end = time.time() print('the DLX runtime is : ' + str(end-start) + 's') if __name__ == "__main__": main()
这里在代码同目录下创建一个dominosaChess.txt文件,并且输入以下布局:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
7 5 1 2 6 7 6 7 3 2 1 1 1 6 7 0 2 3 5 7 3 6 2 0 4 2 0 1 4 4 5 6 1 6 1 5 7 2 0 5 4 3 3 0 4 2 7 0 0 7 6 5 1 3 4 5 5 4 1 0 6 2 0 6 4 5 3 4 7 3 2 3
可以得到以下输出:
+---+---+-+-+---+-+ |7 5|1 2|6|7|6 7|3| +-+-+-+-+ | +-+-+ | |2|1 1|1|6|7|0|2|3| | +---+ +-+-+ | +-+ |5|7 3|6|2 0|4|2|0| +-+---+-+-+-+-+-+ | |1|4 4|5 6|1|6|1|5| | +-+-+---+ | | +-+ |7|2|0|5 4|3|3|0|4| +-+ | +---+-+-+-+ | |2|7|0|0 7|6|5 1|3| | +-+-+---+ +---+-+ |4|5 5|4 1|0|6 2|0| +-+-+-+-+-+-+---+ | |6 4|5 3|4 7|3 2|3| +---+---+---+---+-+ the DLX runtime is : 0.005000591278076172s
可以看出来,这种方法解dominosa比其他方法解要快很多。
把答案填在dominosa里面并提交:
大功告成!