数独人工解法的一些技巧及其python实现
这段日子实现了十几种数独的解题技巧,说实话,花费的时间比我想象的要长得多。本来说了要看论文的,结果心里痒痒,看着论文,心里想着实现这些解法的基础数据结构等等,于是忍不住小试了一下,一发不可收拾,就这样做了两个星期。中间生了一场病,在宿舍里躺了几天,顺便看了几本书,从《万寿寺》到《红拂夜奔》到《寻找无双》,也不知道是感冒药吃多了比较敏感,还是真的感触太大,有一天晚上看完《红拂夜奔》后,竟失声痛哭起来。我体会着那种无尽的绝望,甚至已经无力悲愤了。昨夜又看了《三十而立》,逻辑上的“人人都要死,皇帝是人,所以皇帝必死”与为生存目的而喊出的“皇上万岁万岁万万岁”相对立,可是人都能接受,人是多么复杂的生物啊。嗯哼,我跑题了。
总共有十几种解题技巧,其中最直接的是显式唯一数法和隐式唯一数法。所谓显式唯一数法,是指某个格只有一个候选数可选,这个格自然就只能填这个候选数了。而隐式唯一数法的意思则是,某一行、列或宫只有一个位置可以填某个候选数,当然,这个位置肯定就填这个候选数了。这两个技巧其实根本也算不上技巧了,除了这两个条件,我们还能怎样确定某个格该填哪个数,或者哪个数该填在哪个格呢?剩下的解题技巧,都是在努力删除格子里的候选数,从而使局面浮现出一个显式唯一数,或一个隐式唯一数。除了显/隐式唯一数法,还有显/隐式数对法,显/隐式三数集法以及显/隐式四数集法。在我看来,显式与隐式,一个以位置为中心,一个以候选数为中心,前面提到,显示唯一数法是指某个位置只有一个候选数可选,而显式数对法则是,在一个行/列/宫(以后统称为房)里,有两个位置只有两个相同的候选数可选,那么在这个房的其他位置,可以删除这两个候选数;同理,显式三数集、显式四数集法是指在一个房里,有三个/四个位置只有三/四个相同的候选数可选,那么在这个房的其他位置,可以删除这几个候选数。面隐式数对法则是说,有两个候选数只有两个相同的可选位置,那么这两个位置中的其他候选数均可以删除;同理,隐式三/四数集法是说,有三/四个候选数只有三/四个相同的可选位置,那么在这些位置中,其他的候选数都可以删除。除了以上8种技巧,还有区块删除法,XY形态匹配法(XY-Wing),XYZ形态匹配法(XYZ-Wing),矩形对角法(X-Wing),三链数删减法(SwordFish),四链数删减法(JellyFish)以及唯一矩形法(Unique Rectangle)。除此之外,还有X-Chain单数链法,XY-Chain双数链法以及Forcing Chain,不过我至今没有找到比较好的实现方法,所以算未完成。关于这些技巧的详细介绍,推荐大家看附件的那份手册,其实我所做的事情他全都做了,而且做得非常完整仔细,真是相当的佩服。
既然我已经认定了技巧分两种,一种以位置为中心,一种以候选数为中心,所以我就使用了两个dict来辅助这两种思路,一种就是以位置(r,c)为键,以候选数集为值,即valid_set[(r,c)]=set([a,b,c...]);另一个则以候选数为键,以其可选位置为值,即candi_pos[i]=set([(r1,c1),...,(rn,cn)])。这样,显式唯一数法和隐式唯一数法分别利用这两个dict就可以很容易地完成了:
def _nakedSingleNumber( self ):self._changed = Falsefor pos, validset in self._valid_set.items():if len(validset)<=0:self._invalid = Falseelif len(validset) == 1:num = validset.pop()validset.add(num)print 'pos', pos, 'has only one candidate: %d' %numself._changed = Truereturn Truereturn Falsedef _hiddenSingleNumber( self ):self._changed = Falsefor num, posset in self._candi_pos.items():#_groupByRow,_groupByCol,_groupByBlock is helper functionrows = self._groupByRow(posset)for r, row in rows.items():if len(row)==1:print 'row%d has only one position for num %d' %(r, num), row[0]self._changed = Truereturn Truecols = self._groupByCol(posset)for c, col in cols.items():if len(col)==1:print 'col%d has only one position for num %d' %(c, num), col[0]self._changed = Truereturn Trueblks = self._groupByBlock(posset)for b, blk in blks.items():if len(blk)==1:print 'blk%d has only one position for num %d' %(b, num), blk[0]self._changed = Truereturn Truereturn False
def _get_n_from_m( self, n, m ):index = range(n)while index[0]<=m-n:yield indexi = n-1if index[i]<m-n+i:index[i]+=1else:index[i-1]+=1while i>0 and index[i-1]>=m-n+i:i -= 1index[i-1]+=1for j in range(i,n):index[j]=index[j-1]+1
#nakedNumberSet occurs in a [house], [name] it 'row/column/block'#num=2: pair#num=3: tripple set#num=4: quad setdef _helper_nakedNumberSet( self, house, name, num ):for i, item in house.items():candidate = []#if a position has less than or equal to [num] candidates#add it to the candidate position listfor pos in item:if len(self._valid_set[pos])<=num:candidate.append(pos)#if candidate list has at least [num] elements#get any [num] of positions #and have union of candidate numbers of these position#if this union has exactly [num] candidate numbers#we can delete these candidate numbers from cells other than these positionif len(candidate)>=num:for indices in self._get_n_from_m( num, len(candidate)):uSet = set()subset = []toRemove = []for index in indices:uSet = uSet.union(self._valid_set[candidate[index]])if len(uSet)==num:subset = [ candidate[j] for j in indices ]for j in subset:item.remove(j)for pos in item:for n in uSet:if n in self._valid_set[pos]:toRemove.append( ( pos[0], pos[1], n ))if toRemove:print 'in %s%d,' %(name,i),for (r,c) in subset:print '(%d, %d)' %(r,c),print 'have only candidates',for n in uSet:print '%d' %n,printself._removeCandidate(toRemove)self._changed = Truereturn Truereturn False
def _helper_hiddenNumberSet( self, housename, num ):con = list()for i in range(9):con.append(dict())groupBy = getattr( self, '_groupBy%s' %housename )for n, posset in self._candi_pos.items():house = groupBy( posset )for i, items in house.items():if len(items)<=num:con[i][n]=set(items)for i in range(9):if len(con[i])>=num:for indices in self._get_n_from_m( num, len(con[i])):ks = con[i].keys()nSet = [] uSet = set()toRemove = []for index in indices:nSet.append(ks[index])uSet = uSet.union(con[i][ks[index]])if len(uSet)==num:for pos in uSet:for j in self._valid_set[pos]:if j not in nSet:toRemove.append( (pos[0], pos[1], j) )if toRemove:self._changed = Trueprint 'in %s%d,' %(housename, i),for n in nSet:print '%d,' %n,print 'have only candidate postion',for n in uSet:print n,print self._removeCandidate(toRemove)return Truereturn False
def _regionDeletion( self ):self._changed = Falsefor num, posset in self._candi_pos.items():blks = self._groupByBlock(posset)#1. if in a blk, a candidate number occurs at the same row or same column#delete the candidate number from other cells of this row/columnfor b, blk in blks.items():(r,c) = blk[1]sameRow = TruesameCol = Truefor (ri,ci) in blk:if ri!=r:sameRow = Falseif ci!=c:sameCol = Falseif not (sameRow or sameCol):breakif sameRow:toRemove = []for i in range(0,c//3*3)+range((c//3+1)*3, 9):if (r,i) in self._valid_set and num in self._valid_set[(r,i)]:toRemove.append((r,i,num))if toRemove:self._changed = Trueprint 'in block%d, num %d occurs at the same row %d' %(b, num, r)self._removeCandidate(toRemove)return Trueif sameCol:toRemove = []for i in range(0, r//3*3)+range((r//3+1)*3, 9):if (i, c) in self._valid_set and num in self._valid_set[(i,c)]:toRemove.append((i,c,num))if toRemove:self._changed = Trueprint 'in block%d, num %d occurs at the same col %d' %(b, num, c)self._removeCandidate(toRemove)return True#2.1 if in a row, a candidate number occurs at the same block,#delete the candidate number from other cells of this blockrows = self._groupByRow( posset )for r, row in rows.items():(r,c) = row[1]b = (r//3)*3+c//3sameBlock = Truefor (ri, ci) in row:bi = (ri//3)*3+ci//3if b != bi:sameBlock = Falsebreakif sameBlock:toRemove = []for i in range(r//3*3,r)+range(r+1, (r//3+1)*3):for j in range(c//3*3, (c//3+1)*3):if (i, j) in self._valid_set and num in self._valid_set[(i,j)]:toRemove.append((i,j,num))if toRemove:print 'in row%d, num %d occurs at the same block %d' %(r, num, b)self._changed = Trueself._removeCandidate(toRemove)return True#2.2 if in a column, a candidate number occurs at the same block,#delete the candidate number from other cells of this blockcols = self._groupByCol( posset )for c, col in cols.items():(r,c) = col[1]b = (r//3)*3+c//3sameBlock = Truefor (ri, ci) in col:bi = (ri//3)*3 + ci//3if b!=bi:sameBlock = Falsebreakif sameBlock:toRemove = []for i in range(r//3*3, (r//3+1)*3):for j in range(c//3*3, c)+range(c+1, (c//3+1)*3):if (i,j) in self._valid_set and num in self._valid_set[(i,j)]:toRemove.append((i,j,num))if toRemove:print 'in col%d, num %d occurs at the same block %d' %(c, num, b)self._changed=Trueself._removeCandidate(toRemove)return Truereturn False
def _XYWing( self ):self._changed = Falsefor pos, vset in self._valid_set.items():if len(vset)==2:candidate = dict()for i in range(1,10):candidate[i]=[]row = get_conflict_list( pos[0]*9+pos[1] )plist = [divmod(i,9) for i in row]for i in plist:if i in self._valid_set:if len(self._valid_set[i])==2 and \len(vset.intersection(self._valid_set[i]))==1:z = self._valid_set[i].difference(vset).pop()candidate[z].append(i)toRemove = []XZ=NoneYZ=Nonefor i in range(1,10):#XZ and YZ should not be at the same row or columnif len(candidate[i])>=2 :for indices in self._get_n_from_m( 2, len(candidate[i]) ):pos1 = candidate[i][indices[0]]pos2 = candidate[i][indices[1]]uset = self._valid_set[pos1].union(self._valid_set[pos2])uset.remove(i)if uset==vset:XZ = pos1 YZ = pos2seeA = get_conflict_list(pos1[0]*9+pos1[1])seeB = get_conflict_list(pos2[0]*9+pos2[1])seeBoth = set(seeA).intersection(set(seeB))seelist = [divmod(j,9) for j in seeBoth]seelist.remove(pos)for item in seelist:if item in self._valid_set and i in self._valid_set[item]:toRemove.append((item[0], item[1], i))if toRemove:self._changed = Trueprint 'XYWing,', pos, 'as XY', 'and', XZ, 'and', YZ, 'as XZ and YZ respectively'self._removeCandidate(toRemove)return Truereturn False
def _XYZWing( self ):for pos, vset in self._valid_set.items():if len(vset)==3:index = pos[0]*9+pos[1]blk = get_block(index)plist = [divmod(i,9) for i in blk]#vset is XYZcand_XY = []for i in plist:if i in self._valid_set and len(self._valid_set[i])==2 and \self._valid_set[i].issubset(vset):cand_XY.append(i)if not cand_XY:continue#looking for YZ#first in rowrow = get_row(index)for i in set(row).intersection(set(blk)):row.remove(i)plist = [divmod(i,9) for i in row]cand_YZ = []for i in plist:if i in self._valid_set and len(self._valid_set[i])==2 and \self._valid_set[i].issubset(vset):cand_YZ.append(i)toRemove=[]for i in cand_XY:for j in cand_YZ:if self._valid_set[i].union(self._valid_set[j]) == vset:Y = self._valid_set[i].intersection(self._valid_set[j]).pop()for k in [ (pos[0], (pos[1]//3)*3+(pos[1]+3-1)%3), (pos[0], (pos[1]//3)*3+(pos[1]+1)%3) ]:if k!=i and k!=j and k in self._valid_set:if Y in self._valid_set[k]:toRemove.append((k[0],k[1],Y))if toRemove:self._changed = Trueprint 'XYZWing,', pos, 'as XYZ', 'and', i, 'and', j, 'as XY and YZ respectively'self._removeCandidate(toRemove)return True#found not YZ in row, turn to colcol = get_column(index)for i in set(col).intersection(set(blk)):col.remove(i)plist = [divmod(i,9) for i in col]cand_YZ=[]for i in plist:if i in self._valid_set and len(self._valid_set[i])==2 and \self._valid_set[i].issubset(vset):cand_YZ.append(i)toRemove = []for i in cand_XY:for j in cand_YZ:if self._valid_set[i].union(self._valid_set[j]) == vset:Y = self._valid_set[i].intersection(self._valid_set[j]).pop()for k in [( (pos[0]//3)*3+(pos[0]+3-1)%3, pos[1]), ((pos[0]//3)*3+(pos[0]+1)%3, pos[1]) ]:if k!=i and k!=j and k in self._valid_set:if Y in self._valid_set[k]:toRemove.append((k[0],k[1],Y))if toRemove:self._changed = Trueprint 'XYZWing,', pos, 'as XYZ and', i, 'and', j, 'as XY and YZ respectively'self._removeCandidate(toRemove)return Truereturn False
def _XWing( self, n ):for num, posset in self._candi_pos.items():rows = self._groupByRow(posset)pos = dict()for r, row in rows.items():if len(row)<=n:pos[r]=set()for (r,c) in row:pos[r].add(c)if len(pos)<n:continue#keys stores the rows that have less that n position for num#while pos.items() contains what column are theykeys = pos.keys()for indices in self._get_n_from_m( n, len(pos) ):uset = set()for index in indices:uset = uset.union(pos[keys[index]])#if their union have exactly n len#that mean they are candidates for X-wing(n=2), swordfish(n=3) or JellyFish(n=4)toRemove = []if len(uset)==n:removeR = range(9)for index in indices:removeR.remove(keys[index])removels = [(r,c) for (r,c) in product(removeR, list(uset)) ]for i in removels:if i in self._valid_set and num in self._valid_set[i]:toRemove.append( (i[0], i[1], num) )if toRemove:self._changed = Trueif n==2:print 'X-Wing:',elif n==3:print 'SwordFish:',elif n==4:print 'JellyFish:',print 'in row',for index in indices:print keys[index],print 'num %d occurs only on column' %num,for index in uset:print index,printself._removeCandidate(toRemove)return Truecols = self._groupByCol(posset)pos = dict()for c, col in cols.items():if len(col)<=n:pos[c]=set()for (r,c) in col:pos[c].add(r)if len(pos)<n:continuekeys = pos.keys()for indices in self._get_n_from_m( n, len(pos) ):uset = set()for index in indices:uset = uset.union(pos[keys[index]])if len(uset) == n:toRemove = []removeC = range(9)for index in indices:removeC.remove(keys[index])removels = [(r,c) for (r,c) in product(list(uset), removeC)]for i in removels:if i in self._valid_set and num in self._valid_set[i]:toRemove.append( (i[0], i[1], num) )if toRemove:self._changed = Trueif n==2:print 'X-Wing:',elif n==3:print 'SwordFish:',elif n==4:print 'JellyFish:',print 'in col',for index in indices:print keys[index],print 'num %d occurs only on row' %num,for index in uset:print index,printself._removeCandidate(toRemove)return Truereturn False
def _helper_get_rectangle( self ):for pos, vset in self._valid_set.items():if len(vset) == 2:r1 = (pos[0]//3)*3+(pos[0]+3-1)%3r2 = (pos[0]//3)*3+(pos[0]+1)%3c1 = (pos[1]//3)*3+(pos[1]+3-1)%3c2 = (pos[1]//3)*3+(pos[1]+1)%3candi = [ (r1, pos[1]), (r2, pos[1]), (pos[0], c1), (pos[0], c2) ]candidate = []for i in candi:if i in self._valid_set and vset.issubset(self._valid_set[i]):candidate.append(i)for i in candidate:if i[0]==pos[0]:col1 = [divmod(j,9) for j in get_column(pos[0]*9+pos[1])]col = [ j for j in col1 if j in self._valid_set and vset.issubset(self._valid_set[j]) ]for j in col:r,c = j[0],i[1]if (r,c) in self._valid_set and vset.issubset(self._valid_set[(r,c)]):yield (pos, i, j, (r,c))if i[1]==pos[1]:row1 = [divmod(j,9) for j in get_row(pos[0]*9+pos[1])]row = [ j for j in row1 if j in self._valid_set and vset.issubset(self._valid_set[j]) ]for j in row:r,c = i[0],j[1]if (r,c) in self._valid_set and vset.issubset(self._valid_set[(r,c)]):yield (pos, i, j, (r,c))yield None
def _uniqueRectangle( self ):self._changed = Falserec_gen = self._helper_get_rectangle()rec = rec_gen.next()rec_type = 0while rec:rec_can = self._valid_set[rec[0]]canlist = list(rec_can)extra = dict()for i in rec:dset = self._valid_set[i].difference(rec_can)while dset:d = dset.pop()if d not in extra:extra[d] = []extra[d].append(i)toRemove = []if len(extra)==1:exnum = extra.keys()[0]#uniqueRectangle1if len(extra[exnum]) == 1:rec_type = 1toRemove.append( (extra[exnum][0][0], extra[exnum][0][1], canlist[0]) )toRemove.append( (extra[exnum][0][0], extra[exnum][0][1], canlist[1]) )#uniqueRectangle2 if extra[exnum]==2 and uniqueRectangle5 if extra[exnum]==3else:commonset = set()for cell in extra[exnum]:conflict = get_conflict_list(cell[0]*9+cell[1])if commonset:commonset = commonset.intersection(set(conflict))else:commonset = set(conflict)common = [divmod(i, 9) for i in commonset]for i in common:if i in self._valid_set and exnum in self._valid_set[i]:toRemove.append( (i[0], i[1], exnum) )if toRemove:t = len(extra[exnum])if t == 2:rec_type = 2elif t == 3:rec_type = 5elif len(extra) == 2:exnum = extra.keys()if len(extra[exnum[0]])==1 and len(extra[exnum[1]])==1:exset = set()exset.add(exnum[0])exset.add(exnum[1])pos1 = extra[exnum[0]][0]pos2 = extra[exnum[1]][0]index1 = pos1[0]*9+pos1[1]index2 = pos2[0]*9+pos2[1]if index2 == index1:self._changed = TruetoRemove.append( (pos1[0],pos1[1], canlist[0]) )toRemove.append( (pos1[0], pos1[1], canlist[2]) )self._removeCandidate(toRemove)return Truedef _helper_two_extra( scan ):for i in scan:if len(self._valid_set[i])==3 and exset.issubset(self._valid_set[i]):for j in scan:#uniqueRectangle3if len(self._valid_set[j])==2 and \self._valid_set[j].issubset(self._valid_set[i]):for k in scan:for t in self._valid_set[i]:if k!=i and k!=j and t in self._valid_set[k]:toRemove.append( (k[0], k[1], t) )if toRemove:rec_type = 3print 'in rectangle',#for rect in rec:#print rect,print 'in', pos1, pos2, "'s common house'",print 'naked tripple set is used'return rec_typecount1 = 0count2 = 0for i in scan:if canlist[0] in self._valid_set[i]:count1 += 1if canlist[1] in self._valid_set[i]:count2 += 1#uniqueRectangle4if count1 == 0:toRemove.append( (pos1[0], pos1[1], canlist[1]) )toRemove.append( (pos2[0], pos2[1], canlist[1]) )elif count2 == 0:toRemove.append( (pos1[0], pos1[1], canlist[0]) )toRemove.append( (pos2[0], pos2[1], canlist[0]) )if toRemove:rec_type = 4#print 'in rectangle',#for rect in rec:#print rect,#printreturn rec_type#at the same rowif pos1[0] == pos2[0]:row = get_row(index1)row.remove(index2)scanRow = [(r,c) for (r,c) in (divmod(i, 9) for i in row) if (r,c) in self._valid_set]rec_type = _helper_two_extra( scanRow )#at the same columnif pos1[1] == pos2[1]:col = get_column(index1)col.remove(index2)scanCol = [(r,c) for (r,c) in (divmod(i,9) for i in col) if (r,c) in self._valid_set]rec_type = _helper_two_extra( scanCol )#at the same blockif pos1[0]//3==pos2[0]//3 and pos1[1]//3==pos2[1]//3:blk = get_block( index1 )blk.remove(index2)scanBlk = [(r,c) for (r,c) in (divmod(i,9) for i in blk) if (r,c) in self._valid_set]rec_type = _helper_two_extra( scanBlk )#uniqueRectangle6if pos1[0]!=pos2[0] and pos1[1]!=pos2[1]:posset1 = self._candi_pos[canlist[0]]posset2 = self._candi_pos[canlist[1]]row1 = self._groupByRow(posset1)row2 = self._groupByRow(posset2)col1 = self._groupByCol(posset1)col2 = self._groupByCol(posset2)if (len(row1[pos1[0]])==2 or len(col1[pos1[1]])==2) and \(len(row1[pos2[0]])==2 or len(col1[pos2[1]])==2):toRemove.append( (pos1[0], pos1[1], canlist[0]) )toRemove.append( (pos2[0], pos2[1], canlist[0]) )elif (len(row2[pos1[0]])==2 or len(col2[pos1[1]])==2) and \(len(row2[pos2[0]])==2 or len(col2[pos2[1]])==2):toRemove.append( (pos1[0], pos1[1], canlist[1]) )toRemove.append( (pos2[0], pos2[1], canlist[1]) )if toRemove:rec_type = 6print 'uniqueRectangle6'if not toRemove:#see if it satisfies requirements of uniqueRectangle7#A is definitely rec[0] while D is definitely rec[3]#test whether a candidate occures both only twice on D's row and columnD = rec[3]Dindex = D[0]*9+D[1]Drow = [ divmod(i,9) for i in get_row(Dindex) ]Dcol = [ divmod(i,9) for i in get_column(Dindex) ]row = [ (r,c) for (r,c) in Drow if (r,c) in self._valid_set ]col = [ (r,c) for (r,c) in Dcol if (r,c) in self._valid_set ]countAr = 0countBr = 0countAc = 0countBc = 0for i in row:if canlist[0] in self._valid_set[i]:countAr += 1if canlist[1] in self._valid_set[i]:countBr += 1for i in col:if canlist[0] in self._valid_set[i]:countAc += 1if canlist[1] in self._valid_set[i]:countBc += 1if (countAr==1 and countAc==1):toRemove.append( (D[0],D[1], canlist[1]) )elif (countBr==1 and countBc==1):toRemove.append( (D[0], D[1], canlist[0]) )rec_type= 7if toRemove:self._changed = Trueprint 'uniqueRectangle%d' %rec_type,for i in rec:print i,print self._removeCandidate( toRemove )return Truerec = rec_gen.next()return False