1. 程式人生 > >數獨生成的python實現(半成品-未完待續)

數獨生成的python實現(半成品-未完待續)

數獨的規則,簡而言之,在9*9的表格裡,填入數字,填入的數字在其所在的3*3區域裡是唯一的,在其所在的行裡、列裡也是唯
一的,具體的參照該詞條的解釋。

數獨的生成思路

採用倒著來拆解問題:

  1. 對於每一個小方塊填入數字,需要知道該位置能填入的數字集合(final_useInBit);
  2. 從規則推出:final_useInBit = 所在3*3區域的可取數集 - 所在列數集 - 所在行數集;
  3. 要得到“所在3*3區域的可取數集(Area_canUse)”,就要得到“所在區域已用的數集(Area_used_num),再用基數集(base=[1,2,3,4,5,6,7,8,9])- Area_used_num = Area_canUse,即差集;
  4. 要得到Area_used_num,就要獲悉需要填入的位置隸屬於哪個區域。

備註:

本版本是草稿版,有許多未修改、未完善、刪減之處,有些函式甚至直接引用了全域性變數,僅做筆記。

以下是進行前面程式碼的一些初始定義:

import numpy as np
import random
import copy

#構造初始9*9矩陣,全域性變數:
sudoku = np.zeros((9,9),dtype=np.int16)
#構造基本數源,全域性變數
base = [1,2,3,4,5,6,7,8,9]

#初始化數獨矩陣第一行的數列
first = copy.deepcopy(base)#使用深拷貝的方式,避免base被改動
random.shuffle(first)#呼叫shuffle函式隨機生成
sudoku[0] = np.array(first) 
#在第一行數列已經設定的基礎上進行數獨生成

-----以上先輸入,以下的函式呼叫需在完成下面的步驟後進行-------

#呼叫函式生成數獨
create(1,0) 

在未呼叫函式create生成數獨前,sudoku的結構如下:

(圖片載入失敗...)

在實現的時候,我已經預設了第一行的資料生成(呼叫隨機函式賦值)(你可以根據你的想法做出調整,也可以不預設,但或許
我的函式就得做些修改),因此數獨的生成起點是在座標(1,0)開始的。

實現step 4的函式

get_Area(i,j),實現思路,通過傳入的位置座標,做簡單的位置判斷,返回所在區域的起始行列座標,如:

(圖片載入失敗....)

上圖座標為(4,4)值為5的,所屬的區域就應該是第5區域,起始座標值為顯示白色的0值(3,3),(區域的劃分從左到右,從
上往下進行排序,左上角為1區,右上角為3區,右下角為9區)

函式程式碼如下:

##########################################################
#Function:get_Area
#Description:通過輸入的座標,返回其所在區域的起始座標
#########################################################
def get_Area(i,j):
    if i <= 2:
        if j <= 2:
            Area_now = (0,0)
        elif 3 <= j and j <=5:
            Area_now = (0,3)
        else:
            Area_now = (0,6)
    elif 6 <= i:
        if j <= 2:
            Area_now = (6,0)
        elif 3 <= j and j <=5:
            Area_now = (6,3)
        else:
            Area_now = (6,6)
    else:
        if j <= 2:
            Area_now = (3,0)
        elif 3 <= j and j <=5:
            Area_now = (3,3)
        else:
            Area_now = (3,6)
    return Area_now

為什麼是返回區域起始座標點,而不直接返回區域值(比如1區、2區...)呢?因為這樣更方便於接下來的函式實現。

實現step 3的函式

從上面的分析可知,這一步的目的是得到“所在3*3區域的可取數集(Area_canUse)”,而實現這一步的前提是得到區域已用數
集,這些前面已經論述。

函式程式碼如下:

#########################################################
#Function:get_AreaNUM_canUse
#Description:獲得3*3區域內還未使用的的數的list
#thought:傳入區域的起始座標、當前位的座標,進行遍歷得到已經新增的數,在與基本數列
#    做差集,得到當前區域還能使用的集合
    
#########################################################
def get_AreaNUM_canUse(nowRow,nowCol):
    
    startRow,startCol = get_Area(nowRow,nowCol)
#    print('區域起始行:%s,區域起始列:%s' % (startRow,startCol))    
    Area_used_num = set()
    for i in range(startRow,nowRow):
        for j in range(startCol,startCol+3):
            Area_used_num.add(sudoku[i][j])#考慮替換成numpy
    for j in range(startCol,nowCol):
        Area_used_num.add(sudoku[nowRow][j])
       
#    print('當前區域已用數集:%s' % Area_used_num)
#    print('基數集:%s' % base)                 
    Area_canUse = set(base) - Area_used_num
#    print('最終區域可用數集:%s' % Area_canUse)
    return Area_canUse

這個函式以當前的位置座標為引數傳入,並在呼叫前面實心的get_Area函式後,返回當前所在區域的起始座標值,通過遍歷取
出~該位置之前的~在該區域的~已填入的資料,在進行做差集,得到所在區域目前還能使用的數字Area_canUse。

注:註釋掉的是除錯測試時使用,另外由於是未修改程式碼,因此直接用了一些全域性變數。

實現step 2的函式

這一步是要獲得所在位可用數集final_useInBit,分析前面已經講過。

函式程式碼如下:

#########################################################
#Function:get_bitNUM
#Description:獲取當前位可用的數
#thought:區域可用集 - 當前列已經出現的數-當前行已經出現的數 = 位可用集
#    
#########################################################
def get_bitNUM(Row,Col):    
    
    Col_usedNum = set() 
    Row_usedNum = set()
    #獲得區域可用數
    Area_canUse = get_AreaNUM_canUse(Row,Col)
    #獲得當前行已用數
    for i in range(0,Row):
        Col_usedNum.add(sudoku[i][Col])   
    #獲得當前列已用數   
    for i in range(0,Col):
        Row_usedNum.add(sudoku[Row][i])
    #求差集 
    CR_used = Col_usedNum | Row_usedNum 
    final_useInBit =  Area_canUse - CR_used 

#    print('當前位可用數集:%s' % final_useInBit)
    return list(final_useInBit)
    

到了這一步,基本必要的工具函式已經完成,接下來是考慮怎麼生成的問題。

數獨的生成的考慮

在前面的函式中,我們已經實現了給出某個位可以填的數集,但是並不是某個位填入數集中的數就能得到正確的答案,儘管它填
下去時是符合遊戲規則的,可是這一位填入的數字卻會限制或者說影響下面每一位能填入的數集,如果導致最後沒有合適的數字
可以填,數獨自然就沒法順利生成了。所以有必要思考幾個問題:

  1. 怎麼判斷這一步無法進行下去?
  2. 怎麼判斷數獨順利生成?
  3. 如果這一步無法進行下去怎麼解決?

可以這樣考慮:假設現在在起點(1,0),從該位的可填數集中取數,由於可填數集非空(作為起點它是一定非空的),從
[a,b,c,d....]中取出a後,進入下一位(1,1)的取數,此時如果該位的可填數集非空,那麼仍然從中取數並進入下一位(1,2)。但
如果(1,1)位的可填數集為空,那麼意味著該位沒有符合遊戲規則的數字可以填入,造成這一結果的,必定是由前面的選擇導致
的。因此回退到上一位(1,0)中,重新進行選擇,且不能選前面已經試過的會導致後面失敗的數字。然後繼續進行....直到座標
值為(8,8)且此時可填位為非空集,那麼意味著順利的走到了最後一步。

本函式的實現,並非只生成一個解,而是生成在確定了第一行資料後的所有解,因此有些bug(不斷的生成,並解的數量太多),搞
得有些懶,所以這一部分還沒修改掉。

函式程式碼實現:

def create(Row=0,Col=0,flag=0):
   
    if Col == 9:
        Row +=1
        Col = 0
                      
    final_useInBit = get_bitNUM(Row,Col)               
    if len(final_useInBit) == 0:
        return                 
    else:       
        for choice in final_useInBit:
            sudoku[Row][Col] = choice        
            if Row == 8 and Col == 8:
                flag += 1
                print('-'*25,'數獨式:',flag)
                print(sudoku)                
                return flag   
            
            create(Row,Col+1,flag)

前面使用到if Col == 9的判斷後賦值操作,由於每次呼叫create(Row,Col+1,flag)時,操作位在當前行向右端移動,當Col == 9的
時候實際上已經越界了,此時應該是進入下一行Row,而Col應該重置為0值

上面有個flag引數,這個不用管(目前不起效用,也沒影響),打算要修改來對數獨生成的解進行計數,使得能控制解的生成數
量,還是有很多問題,就權當記錄吧