逛貼吧的時候遇到了一個趣味問題
這裏我再重複一遍問題
如下圖所示,不過黑點,把所有白色圓圈用一條線連起來,不能重複,不能斜着連(更不能像上圖那樣穿牆傳送)
把上圖用矩陣文本表示出來如下(0代表黑點,即不能連的點):
1, 0, 1, 1, 1
1, 1, 1, 1, 1
1, 1, 1, 1, 1
1, 1, 1, 1, 1
1, 1, 1, 1, 1
1-思路分析
倘若問題有解,即能夠一條線連接所有點
那一條線必然有兩個端點,由於左上角的1只有一個相鄰點可以連接,必然爲一個端點(非端點必須是起碼有兩個可以連接的相鄰點的)
不妨以左上角的1爲起點去探索,如果遞歸嘗試所有方向後均不能一條線連接所有點,則說明該問題無解
2-遞歸實現代碼(回溯法)
這裏的代碼思路和我之前的博客:【教程】python遞歸三部曲(基於turtle實現可視化)-三、迷宮探索基本是一樣的
感興趣的話,也可以對比着看看
一條線連的探索過程爲:
從起點(左上角的1)出發,分別按順序往上下左右四個方向去探索(即連接上下左右的可以連接的相鄰點),
在這一過程中遞歸地對連接後的相鄰點進行進一步四周的探索(即將該相鄰點當做新的起點去執行上一步驟,直至探索完成或失敗,纔開始下一個方向的探索)
探索的具體過程可以分下面幾種情況:
- 該點不可連接(黑點或已經連接過的點)或超出邊界,告訴上一步這一步探索失敗
- 沒有可以連接的點了,但a) 連完了所有點,探索完成,告訴上一步這一步探索成功 ,b)沒連完所有點,探索失敗,然後告訴上一步這一步探索是失敗的
- 向某個方向的探索得出的結論是成功的,那麼探索完成,不在探索,並且告訴上一步探索這一方向是能夠探索成功的
- 向某個方向的探索得出的結論是失敗的,那麼換一個方向進行探索
結合以上分析,可以寫出探索的遞歸方法searchNext
,全部代碼如下
problem_board = [
[1, 0, 1, 1, 1],
[1, 1, 1, 1, 1],
[1, 1, 1, 1, 1],
[1, 1, 1, 1, 1],
[1, 1, 1, 1, 1]
]
def check_all_linked(board):
for row in board:
for v in row:
if v == 1:
return False
return True
def search_next(board, ci, ri, v):
# 1. 該點不可連接(黑點或已經連接過的點)或超出邊界,告訴上一步這一步探索失敗
if not (0 <= ci < len(board[0]) and 0 <= ri < len(board)):
# 超出邊界
return False
if board[ri][ci] != 1:
# 黑點或已經連接過的點
return False
board[ri][ci] = v + 1
direction = [
(1, 0),
(0, -1),
(-1, 0),
(0, 1),
]
for d in direction:
dc, dr = d
found = search_next(board, ci + dc, ri + dr, v+1)
if found:
# 3. 向某個方向的探索得出的結論是成功的,那麼探索完成,不在探索,並且告訴上一步探索這一方向是能夠探索成功的
return True
else:
# 4. 向某個方向的探索得出的結論是失敗的,那麼換一個方向進行探索
pass
# 2. 沒有可以連接的點了
# a) 連完了所有點, 探索完成,告訴上一步這一步探索成功
if check_all_linked(board):
return True
# b)沒連完所有點**,探索失敗,然後告訴上一步這一步探索是失敗的
board[ri][ci] = 1
return False
r = search_next(problem_board, 0, 0, 1)
print(r)
輸出爲
False
3-turtle實現可視化
import turtle
# 建立窗體
SCR = turtle.Screen()
SCR.setup(800, 800) # 設置窗體大小
radius = 40
distance = 120
problem_board = [
[1, 0, 1, 1, 1],
[1, 1, 1, 1, 1],
[1, 1, 1, 1, 1],
[1, 1, 1, 1, 1],
[1, 1, 1, 1, 1]
]
simple_problem_board_0 = [
[1, 0, 1, 1],
[1, 1, 1, 1],
[1, 1, 1, 1],
[1, 1, 1, 1],
]
simple_problem_board_1 = [
[1, 0, 1],
[1, 1, 1],
[1, 1, 1]
]
dot_t = turtle.Turtle()
dot_t.hideturtle()
dot_t.speed(0)
dot_t.width(2)
link_t = turtle.Turtle()
link_t.hideturtle()
link_t.pensize(5)
link_t.penup()
link_t.speed(0)
link_t.color("blue")
def draw_white_dot(x, y):
dot_t.penup()
dot_t.goto(x, y - radius)
dot_t.setheading(0)
dot_t.pendown()
dot_t.circle(radius)
def draw_black_dot(x, y):
dot_t.penup()
dot_t.goto(x, y - radius)
dot_t.setheading(0)
dot_t.pendown()
dot_t.begin_fill()
dot_t.circle(radius)
dot_t.end_fill()
def draw_board(board):
r = len(board)
c = len(board[0])
width = (r-1) * distance
height = (c-1) * distance
sx = - width // 2
sy = height // 2
link_t.goto(sx, sy)
link_t.pendown()
for ri in range(r):
for ci in range(c):
xi = sx + ci * distance
yi = sy - ri * distance
v = board[ri][ci]
if v == 0:
draw_black_dot(xi, yi)
else:
draw_white_dot(xi, yi)
def check_all_linked(board):
for row in board:
for v in row:
if v == 1:
return False
return True
def search_next(board, ci, ri, v, di):
# 1. 該點不可連接(黑點或已經連接過的點)或超出邊界,告訴上一步這一步探索失敗
if not (0 <= ci < len(board[0]) and 0 <= ri < len(board)):
# 超出邊界
return False
if board[ri][ci] != 1:
# 黑點或已經連接過的點
return False
if v > 1:
link_t.setheading(di * 90)
link_t.forward(distance)
board[ri][ci] = v + 1
# 必須按照右上左下的順序,與畫筆方向才能一致
direction = [
(1, 0),
(0, -1),
(-1, 0),
(0, 1),
]
for i, d in enumerate(direction):
dc, dr = d
found = search_next(board, ci + dc, ri + dr, v+1, i)
if found:
# 3. 向某個方向的探索得出的結論是成功的,那麼探索完成,不在探索,並且告訴上一步探索這一方向是能夠探索成功的
return True
else:
# 4. 向某個方向的探索得出的結論是失敗的,那麼換一個方向進行探索
pass
# 2. 沒有可以連接的點了
# a) 連完了所有點, 探索完成,告訴上一步這一步探索成功
if check_all_linked(board):
return True
# b)沒連完所有點**,探索失敗,然後告訴上一步這一步探索是失敗的
board[ri][ci] = 1
for _ in range(2):
link_t.undo()
return False
# draw_board(simple_problem_board_1)
# search_next(simple_problem_board_1, 0, 0, 1, 0)
# draw_board(simple_problem_board_0)
# search_next(simple_problem_board_0, 0, 0, 1, 0)
draw_board(problem_board)
search_next(problem_board, 0, 0, 1, 0)
turtle.done()
問題是回溯法的時間複雜度過高,所以導致這個繪製動畫要耗時很久
這裏在第四部做個優化
4-可視化動畫優化
在search_next
方法中發現圓點已經被連線分成兩個不相連的部分的時候,就已經可以說明當前的連線方式有問題,探索失敗並通知上一步
故這裏將第三部分的代碼添加上advanced_search_next
方法,待添加的代碼如下
def check_board_separated(board):
check_board = [row[:] for row in board]
sr = -1
sc = -1
for ri in range(len(check_board)):
for ci in range(len(check_board[0])):
if check_board[ri][ci] == 1:
sr = ri
sc = ci
break
if sr >= 0:
break
if sr < 0:
return False
direction = [
(1, 0),
(0, -1),
(-1, 0),
(0, 1),
]
check_board[sr][sc] = 2
to_explore = [(sc, sr)]
while len(to_explore) > 0:
new_to_explore = []
for item in to_explore:
ci, ri = item
for i, d in enumerate(direction):
dc, dr = d
nc, nr = dc + ci, dr + ri
if (0 <= nc < len(board[0]) and 0 <= nr < len(board)) and check_board[nr][nc] == 1:
check_board[nr][nc] = 2
new_to_explore.append((nc, nr))
to_explore = new_to_explore
for ri in range(len(check_board)):
for ci in range(len(check_board[0])):
if check_board[ri][ci] == 1:
return True
return False
def advanced_search_next(board, ci, ri, v, di):
# 1. 該點不可連接(黑點或已經連接過的點)或超出邊界,告訴上一步這一步探索失敗
if not (0 <= ci < len(board[0]) and 0 <= ri < len(board)):
# 超出邊界
return False
if board[ri][ci] != 1:
# 黑點或已經連接過的點
return False
if v > 1:
link_t.setheading(di * 90)
link_t.forward(distance)
board[ri][ci] = v + 1
if check_board_separated(board):
board[ri][ci] = 1
if v > 1:
link_t.undo()
link_t.undo()
return False
# 必須按照右上左下的順序,與畫筆方向才能一致
direction = [
(1, 0),
(0, -1),
(-1, 0),
(0, 1),
]
for i, d in enumerate(direction):
dc, dr = d
found = advanced_search_next(board, ci + dc, ri + dr, v+1, i)
if found:
# 3. 向某個方向的探索得出的結論是成功的,那麼探索完成,不在探索,並且告訴上一步探索這一方向是能夠探索成功的
return True
else:
# 4. 向某個方向的探索得出的結論是失敗的,那麼換一個方向進行探索
pass
# 2. 沒有可以連接的點了
# a) 連完了所有點, 探索完成,告訴上一步這一步探索成功
if check_all_linked(board):
return True
# b)沒連完所有點**,探索失敗,然後告訴上一步這一步探索是失敗的
board[ri][ci] = 1
for _ in range(2):
link_t.undo()
return False
最後調用時
把search_next
改爲advanced_search_next
調用就好