0096 数独
* * * *
拉格朗日计划
* * * *
数独

数独(日语原意为数的位置)是一种热门的益智游戏,它的起源已不可考,但它与欧拉发明拉丁方阵很相似。数独的规则是在9×9网格的空格(用数字0表示)中填入合适的数字,使得每行、每列以及每个3×3的九宫格中都恰好包含数字1至9。

下面是一个例子及其解。



经过精心设计的数独题都有唯一解,且通常能够通过逻辑推理来找到其解。

有些数独题中存在至少一种填写空格的顺序,使得按这一顺序填写时,每个空格中可以填入的数事实上是唯一的。

另一些数独题中则不论以何种顺序填写,在某些空格处都存在多个可以填入的数字从而需要逐一尝试。

显然解题时的逻辑复杂度和搜索深度决定了题目的难度,上例中的题属于第一种情况,因此是较简单的题。

在文件sudoku.txt中有五十个不同难度的数独题,它们都只有唯一解(文件中的第一个题就是上例中的题)。

解此五十题,取每题的解中左上角的三个数字(即第一行的前三列,如上例中取483)所形成的三位数,并求这些数的和。

本题难度:



解答

经典的回溯题,用位运算判断每个空格还能填入的数字,结果是24702

s=int("111111111",base=2)
d2c=dict([[1 << i,str(i+1)] for i in range(9)])
digits=d2c.keys()

def findBlock(p,q):
    m,n=3*(p/3),3*(q/3)
    return [(m,n),(m+1,n),(m+2,n),(m,n+1),(m+1,n+1),(m+2,n+1),(m,n+2),(m+1,n+2),(m+2,n+2)]
def findCandidate(t):
    return [i for i in digits if i&t]
def noMove(f):
    return all(c==0 for r in f for c in r)
def tryOut(board,toFill,completed):
    if completed==81:
        return board
    elif completed < 81 and noMove(toFill):
        return ""
    else:
        m,mi,mj=min([sum(d&toFill[i][j]>0 for d in digits),i,j] for i in range(9) for j in range(9) if toFill[i][j]!=0)
        block=findBlock(mi,mj)
        for c in findCandidate(toFill[mi][mj]):
            b=[[d2c[c] if i==mi and j==mj else board[i][j] for j in range(9)] for i in range(9)]
            f=[[0 if i==mi and j==mj else ((toFill[i][j]|c)^c if i==mi or j==mj or (i,j) in block else toFill[i][j]) for j in range(9)] for i in range(9)]
            newBoard=tryOut(b,f,completed+1)
            if newBoard:
                return newBoard

grids=[]
with open("096_sudoku.txt") as f:
    for i,line in enumerate(f):
        if i%10==0:
            grids.append([])
        else:
            grids[-1].append(list(line.strip()))

res=0

for board in grids:
    db=[[0 if c=="0" else 1 << (int(c)-1) for c in r] for r in board]
    filled=sum(sum(c>0 for c in r) for r in db)     
    toFill=[]
    nextChoice=[]
    for i,r in enumerate(db):
        toFill.append([]) 
        for j,c in enumerate(r):
            if c!=0:
                toFill[-1].append(0)
            else:
                t=0
                for k in range(9):
                    t=t|db[i][k]
                    t=t|db[k][j]
                for p,q in findBlock(i,j):
                    t=t|db[p][q]
                toFill[-1].append(s^t)
                if toFill[-1][-1] in digits:
                    nextChoice.append((i,j))
                
    k=0
    while k < len(nextChoice):
        i,j=nextChoice[k]
        t=toFill[i][j]
        toFill[i][j],board[i][j]=0,d2c[t]
        for r in range(9):
            if toFill[i][r]!=0 and toFill[i][r] not in digits:
                toFill[i][r]=(toFill[i][r]|t)^t
                if toFill[i][r] in digits:
                    nextChoice.append((i,r))
            if toFill[r][j]!=0 and toFill[r][j] not in digits:
                toFill[r][j]=(toFill[r][j]|t)^t
                if toFill[r][j] in digits:
                    nextChoice.append((r,j))
        for p,q in findBlock(i,j):
            if toFill[p][q]!=0 and toFill[p][q] not in digits:
                toFill[p][q]=(toFill[p][q]|t)^t
                if toFill[p][q] in digits:
                    nextChoice.append((p,q))
        k+=1
    
    filled+=k
    res+=int("".join(tryOut(board,toFill,filled)[0][:3]) if filled < 81 else "".join(board[0][:3]))

print res