逛贴吧的时候遇到了一个趣味问题
这里我再重复一遍问题
如下图所示,不过黑点,把所有白色圆圈用一条线连起来,不能重复,不能斜着连(更不能像上图那样穿墙传送)
把上图用矩阵文本表示出来如下(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
调用就好
5 - 通过逻辑简单证明
实际上,有更简单的方法证明该问题是无解的
- 先把左上角的圆圈标为A
- A的上下左右相邻的可以连接的圆圈标为B
- B的上下左右相邻的可以连接的圆圈标为A
- 重复2、3步直至所有可以连接的圆圈都被标上A或B
如下图所示
图中有13个A,11个B
由于A只能连B,B只能连A,所以假设能一条线连接24个点的话(该线的一个端点必然为左上角的A点)
故该线段对应的必然是ABAB…这样AB交错的顺序,这种顺序,AB点的个数要么相等(如ABAB),要么A点比B点多1个点(如ABABA)
但是13个A点,11个B点,必然有起码一个A点是无法通过AB交错的方式连接起来,
即无法通过一条线来连接