題目 :Tromino 謎題
Tromino是指一個由棋盤上的三個1*1方塊組成的 L 型骨牌。如何用 Tromino 覆蓋一個缺少了了一個方塊(可以在棋盤上任何位置)的2^n*2^n棋盤(下圖展示了n=3情況)。除了這個缺失的方塊,Tromino應該覆蓋棋盤上的所有方塊,Tromino可以任意轉向但不能由重疊。
設計內容及要求:
(1)為此問題設計一個分治算法,分析算法的時間復雜度;
(2)實現所設計的算法,並以圖形化界面演示覆蓋過程。
1問題分析與解決思路
通過對問題的分析,題目要求用分治法來解決該問題。該問題中缺失方塊的位置是任意的,棋盤大小是2的n次方(n為正整數)的矩陣,所以我們先來考慮最小規模即當n=1時的情況。這種情況下無論缺失的方塊在哪個位置,我們只需要將剩下的三個方塊填充就好,相當於放置一個骨牌。當n=2時,方塊數為4*4,划分子問題前,我們先將棋盤分為四個象限,確定缺失方塊的象限后,將其它三個象限距離中心位置最近的一個方塊填充。此時我們再將其划分為四個方塊數為2*2的矩陣,將已經填充的方塊看作缺失方塊,則每個小規模矩陣都有一個缺失方塊,即它們是規模相同的子問題,依次遞歸最后將整個棋盤填充完整。如圖2.1.3所示,將8*8棋盤中的1,2,3填充后,再將其划分為四個部分得到如圖2.1.2所示的4*4棋盤,將4*4棋盤中的1,2,3方塊填充后再划分得到如圖2.1.1所示的2*2棋盤,填充后繼續遞歸最終完成整個棋盤的填充。實際編程時可讓相應的位置坐標賦值為k*i(k為中點位置,i為象限,i取值為1-4),界面動畫展示時根據不同的值用不同顏色顯示方塊即可實現不同骨牌的區分。
圖1.1 n=1時填充 圖1.2 n=2 時填充及划分 圖1.3 n=3 時填充及划分
2 模型建立與算法描述
記棋盤為size*size的二維數組T,初始化為0,缺失方塊位置為x,y,參數m、n分別記錄棋盤橫向位置的開始和結束(列的取值范圍,更確切的說n為縱向長度,即下標的最大值+1),參數l,r分別記錄棋盤縱向位置的開始和結束(行的取值范圍,r同n)
建立數學模型:
T[k][j-1]=T[k][j]=T[k-1][j]=1*k,(x<k且y<j)
T[k-1][j-1]=T[k][j]=T[k-1][j]=2*k,(x>=k且y<j)
T[k-1][j-1]=T[k][j-1]=T[k-1][j]=3*k,(x>=k且y>=j)
T[k-1][j-1]=T[k][j-1]=T[k][j]=4*k,(x<k且y>=j)
其中k=(m+n)/2,j=(l+r)/2
我們將上述分析過程和解決的思路進一步歸納為以下步驟:
(1)如果n-m>=2,執行步驟(2),否則結束過程。
(2)將棋盤進行划分象限,(找尋中心位置即第一象限右下角坐標k,j),判斷缺失方塊的象限,將其它三個象限距離中心最近的位置T[k-1][j-1]、T[k][j-1]、T[k][j]、T[k-1][j]中的與缺失方塊不在一個象限的其余三個的值置為缺失方塊象限與k的乘積。執行步驟(3)
(3)以象限划分將棋盤划分為四個等規模的子問題,將已賦值位置和缺失位置都看做缺失位置參數,相應象限范圍作為參數m,n,l,r。分別對四個子問題重復上述步驟(1)(2)
算法偽代碼描述:
T=[[0 for i in range(size)] for i in range(size)]#定義二維0矩陣作為棋盤
算法 Tromino(m,n,l,r,x,y)
#輸入:矩陣范圍m,n,l,r及缺失位置坐標x,y
#采用分治思想將棋盤T進行賦值,得到僅有位置x,y值為0的棋盤矩陣T
if n-m >=2:
k=int((m+n)/2)
j=int((l+r)/2)
#缺失位置在第一象限
if x<k and y<j:
T[k][j-1]=1*k
T[k][j]=1*k
T[k-1][j]=1*k
#將四個子問題遞歸
Tromino(m,k,l,j,x,y)
Tromino(k,n,l,j,k,j-1)
Tromino(k,n,j,r,k,j)
Tromino(m,k,j,r,k-1,j)
#缺失位置在第二象限
elif x>=k and y<j:
T[k-1][j-1]=2*k
T[k][j]=2*k
T[k-1][j]=2*k
Tromino(m,k,l,j,k-1,j-1)
Tromino(k,n,l,j,x,y)
Tromino(k,n,j,r,k,j)
Tromino(m,k,j,r,k-1,j)
#缺失位置在第三象限
elif x>=k and y>=j:
T[k-1][j-1]=3*k
T[k][j-1]=3*k
T[k-1][j]=3*k
Tromino(m,k,l,j,k-1,j-1)
Tromino(k,n,l,j,k,j-1)
Tromino(k,n,j,r,x,y)
Tromino(m,k,j,r,k-1,j)
#缺失位置在第四象限
else:
T[k-1][j-1]=4*k
T[k][j-1]=4*k
T[k][j]=4*k
Tromino(m,k,l,j,k-1,j-1)
Tromino(k,n,l,j,k,j-1)
Tromino(k,n,j,r,k,j)
Tromino(m,k,j,r,x,y)
3 算法實現與復雜度分析
3.1 數據結構
用python中的二維列表表示棋盤。分析題目可知棋盤是2^n階方陣,使用python中的二維列表(類似於c語言中的二維數組)可以很方便的描述棋盤的特性,並且像c語言中的數組那樣可以很容易的進行取某一位置的值的操作。
3.2 實現步驟
(1)考慮最小規模即棋盤大小為2*2,此時n=0,m=2,l=0,r=2,假設x=0,y=1。此時中心位置k=(m+n)/2=1,j=(l+r)/2=1,判斷缺失位置在第一象限,將剩余的二、三、四象限的離中心最近位置進行賦值,完成一個骨牌的放置。在這種情況下,棋盤剛好填滿。
(2)當棋盤規模大於2*2時,此時只需在上述步驟放置好第一個骨牌后,將棋盤按象限划分為四個相同規模的子問題,分別重復執行上述操作即可
3.3 實現技巧
(1)分治:將棋盤划分為多個相同規模的子問題,得到子問題的解,最終合並得到整個問題的解;
(2)遞歸:用遞歸實現對划分的子問題求解,層層細分再層層整合最終求得問題的解。
3.4 時間、空間復雜度分析
1 時間復雜度分析
本題采用分治算法,每次將問題分為4個規模相同的子問題,每個子問題的基本操作為賦值運算(3次),經計算該算法下在棋盤大小為n(方塊數為2^n*2^n)的情況下,基本操作次數為3*4^(n-2),時間效率類型屬於Ω(2^n)類型。
2 空間復雜度分析
在本題中,所使用的變量為二維列表,其它變量也不隨運行規模的增大而增多,其空間復雜度為O(n^2+C),其中n表示棋盤真正的大小,即數組矩陣的行數或列數,C為常數。
4 程序實現及運行結果分析
4.1 程序實現(源碼Python3.6)
from tkinter import * from tkinter import ttk from tkinter import scrolledtext import time import threading class TrominoApp: #變量、組件定義及初始化 def __init__(self,master): self.speed=0.2 self.select=0 self.colors=['blue','red','green','brown','purple','pink','yellow','orange','gold','crimson','orchid','indigo']#顏色數組 #標簽 self.lb1=Label(master,text="棋盤大小(2^n)",font=('微軟雅黑',8),bg='Wheat') self.lb1.place(x=130,y=30,anchor=NW)#定位 self.lb2=Label(master,text="缺失方塊x坐標",font=('微軟雅黑',8),bg='Wheat') self.lb2.place(x=330,y=30,anchor=NW) self.lb3=Label(master,text="缺失方塊y坐標",font=('微軟雅黑',8),bg='Wheat') self.lb3.place(x=530,y=30,anchor=NW) #文本框 self.t1=Entry(master,width=10,insertborderwidth=10)#輸入文本框 self.t1.place(x=220,y=30)#設置文本框的位置 self.t2=Entry(master,width=10,insertborderwidth=10) self.t2.place(x=420,y=30) self.t3=Entry(master,width=10,insertborderwidth=10) self.t3.place(x=620,y=30) self.t4=scrolledtext.ScrolledText(master ,width=36,bg='lightyellow', height=31,borderwidth = 3,font=('Arial',13),wrap=WORD)#滾動文本框 self.t4.place(x=10,y=70) #按鈕 self.bt1=Button(master,text="查看結果",width=8,height=1,bg='Plum',font= ('微軟雅黑',8),command=self.run)#設置按鈕及點擊事件 self.bt1.place(x=460,y=630) self.bt2=Button(master,text="動畫演示",width=8,height=1,bg='Plum',font= ('微軟雅黑',8),command=self.cartoon)#設置按鈕及點擊事件 self.bt2.place(x=600,y=630) self.bt3=Button(master,text="重置",width=8,height=1,bg='Plum',font= ('微軟雅黑',8),command=self.CliktheButton2)#設置按鈕及點擊事件 self.bt3.place(x=730,y=30) self.bt4=Button(master,text="清空結果",width=8,height=1,bg='Plum',font= ('微軟雅黑',8),command=self.clear)#設置按鈕及點擊事件 self.bt4.place(x=730,y=630) #畫布 self.canvas1 =Canvas(master , width = 512, # 指定Canvas組件的寬度 height = 512, # 指定Canvas組件的高度 bg = 'white') # 指定Canvas組件的背景色 self.canvas1.place(x=370,y=70)#畫布定位 #============================"查看結果"按鈕點擊事件 def run(self): self.select=0 self.CliktheButton1() #============================"動畫演示"按鈕點擊事件 def cartoon(self): self.select=1 self.CliktheButton1() #==========================="重置"按鈕點擊事件 def CliktheButton2(self): #清空輸入框 self.t1.delete('0','end') self.t2.delete('0','end') self.t3.delete('0','end') #清空畫布 x=ALL self.canvas1.delete(x) return 0 #==========================="清空結果"按鈕點擊事件 def clear(self): self.t4.delete(1.0,END) #清空畫布 x=ALL self.canvas1.delete(x) return 0 #============================由run和cartoon調用 def CliktheButton1(self): #獲取輸入值 self.n=int(self.t1.get()) self.x=int(self.t2.get()) self.y=int(self.t3.get()) if self.n<1: self.t4.insert(END,'\n++++++++++++++++++++++++++++++++\n錯誤!!!棋盤大小必須大於0\n') else: self.size=2**self.n #信息及結果顯示 if self.x>=self.size or self.y>=self.size: self.t4.insert(END,'\n++++++++++++++++++++++++++++++++\n錯誤!!!坐標越界\n') else: self.t4.insert(END,'\n++++++++++++++++++++++++++++++++\n棋盤大小:'+str(self.size)+'*'+str(self.size)+'\n'+'x:'+str(self.x)+' '+'y:'+str(self.y)+'\n') self.cellwidth=round(int(self.canvas1['width'])/self.size)#設置方塊大小 self.T=[[0 for i in range(self.size)] for i in range(self.size)] t1=time.perf_counter() self.Tromino(0,self.size,0,self.size,self.x,self.y) t=time.perf_counter()-t1 self.t4.insert(END,'運行時間:'+str(t)+'\n') #清空畫布 x=ALL self.canvas1.delete(x) #結果演示 self.cellx=self.x*self.cellwidth self.celly=self.y*self.cellwidth self.canvas1.create_rectangle(self.cellx,self.celly,self.cellx+self.cellwidth,self.celly+self.cellwidth,fill="black",outline="black") self.canvas1.update()#更新畫布 time.sleep(0.3) if self.select: self.Display(0,self.size,0,self.size,self.x,self.y)#調用動畫演示函數 else: for i in range(self.size): for j in range(self.size): index=self.T[i][j] if index==0: color='black' else: color=self.colors[index%(len(self.colors)-1)] self.cellx=i*self.cellwidth self.celly=j*self.cellwidth self.canvas1.create_rectangle(self.cellx,self.celly,self.cellx+self.cellwidth,self.celly+self.cellwidth,fill=color,outline="black") self.canvas1.update()#更新畫布 #============================動畫演示函數由CliktheButton1調用 def Display(self,m,n,l,r,x,y): k=int((m+n)/2) j=int((l+r)/2) if n-m >=2: if x<k and y<j:#1--缺失位置在第一象限 #第二、三、四象限離中心最近位置置為1*k,相當於放置一個骨牌 color=self.colors[(1*k)%(len(self.colors)-1)] self.cellx=k*self.cellwidth self.celly=(j-1)*self.cellwidth self.canvas1.create_rectangle(self.cellx,self.celly,self.cellx+self.cellwidth,self.celly+self.cellwidth,fill=color,outline="black") self.cellx=k*self.cellwidth self.celly=j*self.cellwidth self.canvas1.create_rectangle(self.cellx,self.celly,self.cellx+self.cellwidth,self.celly+self.cellwidth,fill=color,outline="black") self.cellx=(k-1)*self.cellwidth self.celly=j*self.cellwidth self.canvas1.create_rectangle(self.cellx,self.celly,self.cellx+self.cellwidth,self.celly+self.cellwidth,fill=color,outline="black") self.canvas1.update()#更新畫布 time.sleep(self.speed) #遞歸 分為四個相同規模的棋盤 四個象限 self.Display(m,k,l,j,x,y)#1 self.Display(k,n,l,j,k,j-1)#2 self.Display(k,n,j,r,k,j)#3 self.Display(m,k,j,r,k-1,j)#4 elif x>=k and y<j:#2 color=self.colors[(2*k)%(len(self.colors)-1)] self.cellx=(k-1)*self.cellwidth self.celly=(j-1)*self.cellwidth self.canvas1.create_rectangle(self.cellx,self.celly,self.cellx+self.cellwidth,self.celly+self.cellwidth,fill=color,outline="black") self.cellx=(k-1)*self.cellwidth self.celly=j*self.cellwidth self.canvas1.create_rectangle(self.cellx,self.celly,self.cellx+self.cellwidth,self.celly+self.cellwidth,fill=color,outline="black") self.cellx=k*self.cellwidth self.celly=j*self.cellwidth self.canvas1.create_rectangle(self.cellx,self.celly,self.cellx+self.cellwidth,self.celly+self.cellwidth,fill=color,outline="black") self.canvas1.update()#更新畫布 time.sleep(self.speed) #遞歸 self.Display(m,k,l,j,k-1,j-1)#1 self.Display(k,n,l,j,x,y)#2 self.Display(k,n,j,r,k,j)#3 self.Display(m,k,j,r,k-1,j)#4 elif x>=k and y>=j:#3 color=self.colors[(3*k)%(len(self.colors)-1)] self.cellx=(k-1)*self.cellwidth self.celly=j*self.cellwidth self.canvas1.create_rectangle(self.cellx,self.celly,self.cellx+self.cellwidth,self.celly+self.cellwidth,fill=color,outline="black") self.cellx=(k-1)*self.cellwidth self.celly=(j-1)*self.cellwidth self.canvas1.create_rectangle(self.cellx,self.celly,self.cellx+self.cellwidth,self.celly+self.cellwidth,fill=color,outline="black") self.cellx=k*self.cellwidth self.celly=(j-1)*self.cellwidth self.canvas1.create_rectangle(self.cellx,self.celly,self.cellx+self.cellwidth,self.celly+self.cellwidth,fill=color,outline="black") self.canvas1.update()#更新畫布 time.sleep(self.speed) #遞歸 self.Display(m,k,l,j,k-1,j-1)#1 self.Display(k,n,l,j,k,j-1)#2 self.Display(k,n,j,r,x,y)#3 self.Display(m,k,j,r,k-1,j)#4 else:#4 color=self.colors[(4*k)%(len(self.colors)-1)] self.cellx=(k-1)*self.cellwidth self.celly=(j-1)*self.cellwidth self.canvas1.create_rectangle(self.cellx,self.celly,self.cellx+self.cellwidth,self.celly+self.cellwidth,fill=color,outline="black") self.cellx=k*self.cellwidth self.celly=(j-1)*self.cellwidth self.canvas1.create_rectangle(self.cellx,self.celly,self.cellx+self.cellwidth,self.celly+self.cellwidth,fill=color,outline="black") self.cellx=k*self.cellwidth self.celly=j*self.cellwidth self.canvas1.create_rectangle(self.cellx,self.celly,self.cellx+self.cellwidth,self.celly+self.cellwidth,fill=color,outline="black") self.canvas1.update()#更新畫布 time.sleep(self.speed) #遞歸 self.Display(m,k,l,j,k-1,j-1)#1 self.Display(k,n,l,j,k,j-1)#2 self.Display(k,n,j,r,k,j)#3 self.Display(m,k,j,r,x,y)#4 ##############################核心算法 分治法骨牌填充棋盤 def Tromino(self,m,n,l,r,x,y): if n-m >=2: #定位中心位置 k=int((m+n)/2) j=int((l+r)/2) if x<k and y<j:#1--缺失位置在第一象限 #第二、三、四象限離中心最近位置置為1*k,相當於放置一個骨牌 self.T[k][j-1]=1*k#2 self.T[k][j]=1*k#3 self.T[k-1][j]=1*k#4 #遞歸 分為四個相同規模的棋盤 四個象限 self.Tromino(m,k,l,j,x,y)#1 self.Tromino(k,n,l,j,k,j-1)#2 self.Tromino(k,n,j,r,k,j)#3 self.Tromino(m,k,j,r,k-1,j)#4 elif x>=k and y<j:#2 self.T[k-1][j-1]=2*k#1 self.T[k][j]=2*k#3 self.T[k-1][j]=2*k#4 #遞歸 self.Tromino(m,k,l,j,k-1,j-1)#1 self.Tromino(k,n,l,j,x,y)#2 self.Tromino(k,n,j,r,k,j)#3 self.Tromino(m,k,j,r,k-1,j)#4 elif x>=k and y>=j:#3 self.T[k-1][j-1]=3*k#1 self.T[k][j-1]=3*k#2 self.T[k-1][j]=3*k#4 #遞歸 self.Tromino(m,k,l,j,k-1,j-1)#1 self.Tromino(k,n,l,j,k,j-1)#2 self.Tromino(k,n,j,r,x,y)#3 self.Tromino(m,k,j,r,k-1,j)#4 else:#4 self.T[k-1][j-1]=4*k#1 self.T[k][j-1]=4*k#2 self.T[k][j]=4*k#3 #遞歸 self.Tromino(m,k,l,j,k-1,j-1)#1 self.Tromino(k,n,l,j,k,j-1)#2 self.Tromino(k,n,j,r,k,j)#3 self.Tromino(m,k,j,r,x,y)#4 #=================================主函數,界面窗口創建及調用 def main(): root=Tk() root.title('Tromino演示系統')#設置主窗口標題 root.configure(background='lightblue') #geometry(‘axb+c+d’) axb代表初始化時主窗口的大小,c,d代表了初始化時窗口所在位置 root.geometry('900x800+10+5') app=TrominoApp(root) root.resizable(0,0) root.mainloop() #==================================程序運行入口 if __name__ == "__main__" : main()
4.2 測試及運行結果分析
1 測試分析
如圖4.2.1-4.2.6所示為本題的運行結果截圖。其中黑色方塊表示缺失方塊,為區分每個骨牌,用同種顏色表示一個骨牌,不同骨牌用不同顏色表示。
圖4.2.1所示為輸入棋盤大小為0時系統顯示的錯誤信息提示,圖4.2.2為棋盤大小為1(即2*2),缺失位置為(1,1)時,系統顯示結果,右邊畫布顯示圖形可視化,顯示結果正確(由於列表下標從0開始計數因此,坐標(1,1)對應第二行第二列);左邊信息框顯示棋盤信息及運行花費時間,此時花費時間為1.650000000008589*10e-5s。圖4.2.3--圖4.2.6為改變棋盤大小和缺失位置時演示結果,結果顯示運行時間與棋盤大小成正相關。
圖4.2.1 圖4.2.2 圖4.2.3
圖4.2.4 圖4.2.5 圖4.2.6
2 運行結果分析
表2.4.2.1和圖2.4.2.9為某次執行時設置不同棋盤大小得到的運行時間之間的比較。當棋盤大小在較小范圍內時,用時變化較小,隨后(棋盤大小不斷增大)就呈現指數爆炸增長。
表4.2.1 棋盤大小-運行時間表
圖4.2.9 棋盤大小-運行時間折線圖