数独の類似問題のWindokuをPythonで解いてみた。

 慶應入試で出題された数独の類似問題WindokuPythonで解いてみました。

+-----+-----+-----+
|3 9 ・|・ ・ ・|・ ・ 4|
| +---+-+ +-+---+ |
|・|・ ・|9|・|・|・ 6|・|
| |   | | | |   | |
|・|・ ・|3|1|・|5 ・|9|
+-+---+-+-+-+---+-+
|2|7 ・|・|・|・|9 ・|・|
| +---+-+ +-+---+ |
|8 ・ ・|・ 2 9|・ ・ ・|
| +---+-+ +-+---+ |
|・|・ 9|・|・|7|3 ・|・|
+-+---+-+-+-+---+-+
|・|・ 1|2|9|6|・ 8|・|
| |   | | | |   | |
|・|・ 5|・|・|・|・ 9|・|
| +---+-+ +-+---+ |
|9 6 ・|・ ・ ・|7 ・ ・|
+-----+-----+-----+

 前回数独のプログラムに、「灰色の4個の3×3のマスに1〜9までの数字がすべて現れる」かチェックする部分を書き加えただけです。(^_^;
 この「Windoku」は、「Hypersudoku」とか「Four-Box Sudoku」とか「NRC-Sudoku」とも呼ばれているようです。(^_^;
 ちなみに、通常の数独のルールだけで解くと、156個の解が見つかって、解は確定しません。

● Windoku1.py

# coding: UTF-8
# Windoku1.py

from time import time
import itertools

# 慶應SFCの入試で出題されたWindoku
sQ = '''
3 9 .|. . .|. . 4
 +---+-+ +-+---+
.|. .|9|.|.|. 6|.
.|. .|3|1|.|5 .|9
-+---+-+-+-+---+-
2|7 .|.|.|.|9 .|.
 +---+-+ +-+---+
8 . .|. 2 9|. . .
 +---+-+ +-+---+
.|. 9|.|.|7|3 .|.
-+---+-+-+-+---+-
.|. 1|2|9|6|. 8|.
.|. 5|.|.|.|. 9|.
 +---+-+ +-+---+
9 6 .|. . .|7 . .
'''
##sQ = '000000010002000034000051000000006500070300080003000000000080000580000900690000000'

def toStr(lst):
    return '[%s]'%','.join(['%s'%x for x in lst])

# 列がダブっていないか調べる
def isDupClm(n,li2D):
    m = len(li2D)
    for i in range(m):
        for j in range(n):
            if li2D[n][i]==li2D[j][i]: return True
    return False

# 3x3ブロック内がダブっていないか調べる
def isDupBlk(n,li2D):
    m = n//3
    for k in range(3):
        K = 3*k
        li = [li2D[i][j] for i in range(3*m,n) for j in range(K,K+3)]
        for i in range(3):
            if li2D[n][K+i] in li: return True
    return False

# m行n列目の数があるブロック内のすべての数のリストを取得(ただし0以外)
def getBlk(m,n,li2D):
    m,n = m//3,n//3
    li = [li2D[i][j] for i in range(3*m,3*m+3) \
                     for j in range(3*n,3*n+3) if li2D[i][j]!=0]
    return li

R = range(1,10)
# フラグ: 行・列・ブロックのそれぞれで確定している数のリスト
rowFlg = [[] for i in range(9)]
colFlg = [[] for i in range(9)]
blkFlg = [[] for i in range(9)]

def mkFlgs(la):
    for i in range(9):
        rowFlg[i] = [la[i][k] for k in range(9) if la[i][k]!=0]
    for j in range(9):
        colFlg[j] = [la[k][j] for k in range(9) if la[k][j]!=0]
    for i in range(0,9,3):
        for j in range(0,9,3):
            blkFlg[i+j//3] = getBlk(i,j,la)

# 各マスの候補のリストを得る
def mkCandidateTbl(la):
    lb = [[[] for i in range(9)] for j in range(9)]
    mkFlgs(la)
    for i in range(9):
        for j in range(9):
            if la[i][j]!=0:
                lb[i][j]=[la[i][j]]
            else:
                li = rowFlg[i]+colFlg[j]+blkFlg[i//3*3+j//3]
                lb[i][j] = sorted(set(R)-set(li))
    return lb

# マスの確定サーチ: 置ける数字がひとつしかないマスを探す
def decideCell(la,lb):
    li = la[:]
    fd = False
    for i in range(9):
        for j in range(9):
            if la[i][j]!=0: continue
            if len(lb[i][j])==1:
                li[i][j] = lb[i][j][0]
                fd = True
    if not fd: li = []
    return li

# 縦方向の確定サーチ: 縦方向で確定できる数字を探す
def decideClm(la,lb):
    li = la[:]
    fd = False
    for j in range(9):
        for m in tuple(set(R)-set(colFlg[j])):
            c = k = 0
            flg = True
            for i in range(9):
                if la[i][j]!=0: continue
                if m in lb[i][j]:
                    c+=1; k = i
                if c> 1: break
            else: flg = False
            if flg: continue
            if c==1:
                li[k][j] = m
                fd = True
    if not fd: li = []
    return li

# 横方向の確定サーチ
def decideRow(la,lb):
    li = la[:]
    fd = False
    for i in range(9):
        for m in tuple(set(R)-set(rowFlg[i])):
            c = k = 0
            flg = True
            for j in range(9):
                if la[i][j]!=0: continue
                if m in lb[i][j]:
                    c+=1; k = j
                if c> 1: break
            else: flg = False
            if flg: continue
            if c==1:
                li[i][k] = m
                fd = True
    if not fd: li = []
    return li

# ブロックの確定サーチ
def decideBlk(la,lb):
    li = la[:]
    fd = False
    for i in range(0,9,3):
        for j in range(0,9,3):
            for m in tuple(set(R)-set(blkFlg[i//3*3+j//3])):
                c = ib = jb = 0
                flg = True
                for k in range(9):
                    if la[i+k//3][j+k%3]!=0: continue
                    if m in lb[i+k//3][j+k%3]:
                        c+=1; ik,jk = i+k//3,j+k%3
                    if c> 1: break
                else: flg = False
                if flg: continue
                if c==1:
                    li[ik][jk] = m
                    fd = True
    if not fd: li = []
    return li

def mkPerm(la):
    perm = [[] for i in range(9)]
    while True:
        flg = True
        b = mkCandidateTbl(la)
        li = decideCell(la,b)
        if li!=[]:
            la = li[:]; flg = False
            b = mkCandidateTbl(la)
        li = decideClm(la,b)
        if li!=[]:
            la = li[:]; flg = False
            b = mkCandidateTbl(la)
        li = decideRow(la,b)
        if li!=[]:
            la = li[:]; flg = False
            b = mkCandidateTbl(la)
        li = decideBlk(la,b)
        if li!=[]:
            la = li[:]; flg = False
            b = mkCandidateTbl(la)
        if flg: break

    for k in range(9):
        li = b[k]
        r = list(set(R)-set([n for n in la[k] if n!=0]))
        for p in itertools.permutations(r):     # Perm[k]
            p = list(p)
            for i in range(9):
                if la[k][i]!=0:
                    p[i:i] = [la[k][i]]
            flg = True
            for i in range(9):
                if la[k][i]!=0: continue
                if p[i] not in li[i]: break
            else: flg = False
            if flg: continue
            perm[k].append(p)
    return perm

def solver(sq):
    m = [[0 for i in range(9)] for j in range(9)]
    a = [[0 for i in range(9)] for j in range(9)]
    sq = sq.replace('.','0')
    lq = [c for c in sq if c in '0123456789']
    for i in range(9):
        for j in range(9):
            a[i][j] = int(lq[9*i+j])
    Perm = mkPerm(a)
    ans = []
    for m[0] in Perm[0]:
        for m[1] in Perm[1]:
            if isDupClm(1,m): continue
            if isDupBlk(1,m): continue
            for m[2] in Perm[2]:
                if isDupClm(2,m): continue
                if isDupBlk(2,m): continue
                for m[3] in Perm[3]:
                    if isDupClm(3,m): continue
                    for m[4] in Perm[4]:
                        if isDupClm(4,m): continue
                        if isDupBlk(4,m): continue
                        for m[5] in Perm[5]:
                            if isDupClm(5,m): continue
                            if isDupBlk(5,m): continue
                            for m[6] in Perm[6]:
                                if isDupClm(6,m): continue
                                for m[7] in Perm[7]:
                                    if isDupClm(7,m): continue
                                    if isDupBlk(7,m): continue
                                    for i in range(9):
                                        li = [m[j][i] for j in range(8)]
                                        m[8][i] = 45-sum(li)
                                    if isDupBlk(8,m): continue
                                    flg = True
                                    for i in range(9):
                                        if a[8][i]==0: continue
                                        if a[8][i]!=m[8][i]: break
                                    else: flg = False
                                    if flg: continue
                                    # 灰色の4個の3×3のマスのチェック
                                    flg = True
                                    for k in range(4):
                                        p,q = 1+k//2*4,1+k%2*4
                                        li = [m[i][j] for i in range(p,p+3) \
                                                      for j in range(q,q+3)]
                                        if len(set(li))!=9: break
                                    else: flg = False
                                    if flg: continue
                                    pass # チェックを潜り抜けたものだけを保存
                                    ans.append(m[:][:])
                                    return ans  # 1つ見つけたら終了
    return ans  # 全部探すときは1行上(↑)をコメントアウトしてこちらから出る

def main():
    tm = time()  # Timer Start
    a = solver(sQ)
    for i in range(len(a)):
        for j in range(9):
            print(toStr(a[i][j]))
        print('')
    print("Runtime : %.3f [sec]"%(time()-tm))   # Timer Stop & Disp

if __name__ == '__main__':
    main()

●実行結果

[3,9,7,8,6,5,1,2,4]
[5,1,4,9,7,2,8,6,3]
[6,2,8,3,1,4,5,7,9]
[2,7,6,5,4,3,9,1,8]
[8,5,3,1,2,9,6,4,7]
[1,4,9,6,8,7,3,5,2]
[7,3,1,2,9,6,4,8,5]
[4,8,5,7,3,1,2,9,6]
[9,6,2,4,5,8,7,3,1]

Runtime : 0.316 [sec]

※参考URL
慶應SFCの入試で出題されたWindokuと呼ばれる数独に似た問題の考察
Cross+A :: 数独 (Sudoku) > ウィンドク(Windoku)