貼吧趣味問題——一線連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調用就好

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