贴吧趣味问题——一线连24点【编程穷举证明无解】

逛贴吧的时候遇到了一个趣味问题
在这里插入图片描述
这里我再重复一遍问题
如下图所示,不过黑点,把所有白色圆圈用一条线连起来,不能重复,不能斜着连(更不能像上图那样穿墙传送)
在这里插入图片描述
把上图用矩阵文本表示出来如下(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)出发,分别按顺序往上下左右四个方向去探索(即连接上下左右的可以连接的相邻点),
在这一过程中递归地对连接后的相邻点进行进一步四周的探索(即将该相邻点当做新的起点去执行上一步骤,直至探索完成或失败,才开始下一个方向的探索)
探索的具体过程可以分下面几种情况:

  1. 该点不可连接(黑点或已经连接过的点)或超出边界,告诉上一步这一步探索失败
  2. 没有可以连接的点了,但a) 连完了所有点,探索完成,告诉上一步这一步探索成功 ,b)没连完所有点,探索失败,然后告诉上一步这一步探索是失败的
  3. 向某个方向的探索得出的结论是成功的,那么探索完成,不在探索,并且告诉上一步探索这一方向是能够探索成功的
  4. 向某个方向的探索得出的结论是失败的,那么换一个方向进行探索

结合以上分析,可以写出探索的递归方法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调用就好

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章