Trang chủ Cộng đồng Nhóm học tập Thư viện Vinh danh
Guest

Đăng nhập để truy cập tất cả tính năng

Bài toán mê cung và tìm đường đi ngắn nhất

Bài toán mê cung và tìm đường đi ngắn nhất

Bài toán mê cung là một trong những bài toán tìm kiếm điển hình trong trí tuệ nhân tạo. Nó mô phỏng một hệ thống gồm các lối đi có chướng ngại vật, nơi mà một tác nhân cần tìm ra con đường tối ưu từ vị trí xuất phát đến đích. 

Cho một mê cung được biểu diễn dưới dạng một ma trận hoặc đồ thị, trong đó mỗi ô hoặc nút đại diện cho một vị trí. Một số vị trí là tường hoặc chướng ngại vật, không thể di chuyển qua. Mục tiêu là tìm đường đi từ điểm bắt đầu đến điểm đích sao cho tổng số bước hoặc chi phí di chuyển là nhỏ nhất.

Một số khái niệm quan trọng trong bài toán:

  • Không gian trạng thái: là tập hợp tất cả các vị trí mà tác nhân có thể đi đến.
  • Toán tử trạng thái: Các phép di chuyển lên, xuống, trái, phải hoặc chéo tùy vào quy tắc của bài toán.
  • Hàm đánh giá: Xác định khoảng cách từ trạng thái hiện tại đến đích (có thể sử dụng heuristic trong thuật toán tìm kiếm).

Output image

Ảnh 3‑1: Hình ảnh mê cung

 

Ví dụ cho một mê cung như ảnh 3-1. Các ô đen là tường, các ô trắng là đường đi. Điểm bắt đầu là S và điểm kết thúc là điểm E. Biễu diễn mê cung trên thành một ma trận nhị phân trong đó 0 biểu thị ô không thể đi qua và 1 biễu thị ô có thể đi qua.

1        1        0        1        1

0        1        0        1        1

0        1        1        1        0

0        0        0        1        1

Các thuật toán giải quyết bài toán mê cung là tìm kiếm theo chiều sâu (Depth-First Search – DFS), tìm kiếm theo chiều rộn (Breadth-First Search – BFS), thuật toán Dijkstra, và A*.

Mê cung thực chất là một đồ thị 2D mà trong đó mỗi ô có thể đi qua (1) là một đỉnh (node), mỗi đường đi hợp lệ giữa hai ô là một cạnh (edge). 

Có hai cách phổ biến để biểu diễn đồ thị, ma trận kề (Adjacency Matrix) và danh sách kề (Adjacency List). Ma trận kề cần O(N2) bộ nhớ để lưu trữ tất cả các đỉnh, ngay cả khi một số đỉnh không có kết nối. Danh sách kề chỉ lưu các cạnh thực tế, giúp tiết kiệm bộ nhớ đáng kể khi đồ thị có ít cạnh (đồ thị thưa). Giả sử mê cung có 10.000 ô, nhưng chỉ có 10% ô có thể đi qua thì ma trận kề sẽ tốn quá nhiều bộ nhớ, trong khi danh sách kề chỉ lưu các kết nối hợp lệ. Sử dụng danh sách kề giúp tìm kiếm nhanh hơn khi áp dụng các thuật toán như DFS, BFS, Dijkstra hay A*.

Để tìm danh sách kề từ ma trận của mê cung, ta cần tìm danh sách kề theo ý tưởng sau: duyệt qua từng ô trong ma trận, nếu ô là 1, kiểm tra các ô hàng xóm( ô lân cận) của nó (trên, dưới, trái phải). Nếu ô hàng xóm là 1 thì thêm cạnh vào danh sách kề. Dưới đây là code tìm danh sách kề:

 

def generate_adjacency_list(maze):

    rowscols = len(maze), len(maze[0])

    adjacency_list = {}  # Khởi tạo danh sách kề

    directions = [(-10), (10), (0-1), (01)]  # Lên, Xuống, Trái, Phải

    # Đánh số mỗi ô có thể đi (1) theo thứ tự duyệt từ trên xuống dưới, trái sang phải

    node_mapping = {}

    node_id = 0

    for i in range(rows):

        for j in range(cols):

            if maze[i][j== 1:

                node_mapping[(ij)] = node_id

                adjacency_list[node_id= []

                node_id += 1

    # Tạo danh sách kề

    for (ij), node in node_mapping.items():

        for dxdy in directions:

            ninj = i + dxj + dy

            if (ninjin node_mapping:  # Nếu hàng xóm hợp lệ

                adjacency_list[node].append(node_mapping[(ninj)])

    return adjacency_list

 

# Mê cung đầu vào (1: đường đi, 0: tường)

maze = [

    [11011],

    [01011],

    [01110],

    [00011]

]

 

# Tạo danh sách kề

adj_list = generate_adjacency_list(maze)

 

# Hiển thị danh sách kề

for nodeneighbors in adj_list.items():

    print(f"{node}{neighbors}")

 Kết quả của code trên là:

A screenshot of a computer

AI-generated content may be incorrect.

Ảnh 3‑2: Kết quả tìm danh sách kề

 

Danh sách kề này là dữ liệu đầu vào cho các thuật toán tìm kiếm.

3.1.2. Thuật toán tìm theo chiều sâu DFS

Tìm kiếm theo chiều sâu (DFS) là một thuật toán duyệt hoặc tìm kiếm trên đồ thị bằng cách đi sâu vào một nhánh trước khi quay lại để kiểm tra các nhánh khác. Nó có thể được triển khai bằng đệ quy hoặc bằng ngăn xếp (stack).

Trong bài toán mê cung, DFS có thể tìm ra một đường đi từ điểm S (Start) đến E (End), nhưng không đảm bảo đó là đường đi ngắn nhất. Thuật toán này thường được dùng vào tìm đường trong mê cung, duyệt cây hoặc đồ thị để kiểm tra kết nối, tìm tất cả các thành phần liên thông trong một đồ thị.

DFS hoạt động dựa trên nguyên tắc LIFO (Last In, First Out), có thể triển khai bằng:

  • Đệ quy: Dùng ngăn xếp của hệ thống để lưu trạng thái.
  • Ngăn xếp tường minh: Dùng stack để kiểm soát các bước di chuyển.

Giả sử đồ thị được biểu diễn dưới dạng danh sách kề thì các bước thực hiện như sau:

  • Bắt đầu từ một đỉnh (start node).
  • Đánh dấu đỉnh đó là đã thăm.
  • Duyệt tất cả các đỉnh kề:
    • Nếu đỉnh kề chưa được thăm → gọi DFS trên đỉnh đó.
    • Nếu đã thăm → bỏ qua.
  • Quay lui (backtracking) khi không còn đỉnh nào để duyệt.

Đồ thị được vẽ ra từ ma trận mê cung ở phần 3.1.1:

Ảnh 3‑3: Đồ thị dược vẽ từ danh sách kề của ma trận nhị phân mê cung

 

Dưới đây là code vẽ đồ thị:

import networkx as nx

import matplotlib.pyplot as plt

def draw_maze_graph(adjacency_list):

    """Vẽ đồ thị từ danh sách kề của mê cung."""

    G = nx.Graph()

    

    # Thêm cạnh vào đồ thị

    for nodeneighbors in adjacency_list.items():

        for neighbor in neighbors:

            G.add_edge(nodeneighbor)

    

    # Tạo bố cục cho đồ thị

    pos = nx.spring_layout(G)

    

    # Vẽ đồ thị

    plt.figure(figsize=(66))

    nx.draw(Gposwith_labels=Truenode_color="lightblue"edge_color="gray"node_size=1000font_size=12)

    plt.title("Đồ thị biểu diễn mê cung")

    plt.show()

# Danh sách kề của mê cung

maze_graph = {

    0: [1],

    1: [04],

    2: [35],

    3: [26],

    4: [17],

    5: [269],

    6: [35],

    7: [48],

    8: [79],

    9: [5810],

    10: [911],

    11: [10]

}

# Gọi hàm vẽ đồ thị

draw_maze_graph(maze_graph)

 

Ứng với bài toán của chúng ta, vị trí bắt đầu là nút 0 và lối ra là nút 11. Thuật toán được sử dụng đệ quy và stack được biễu diễn dưới dạng mã giả.

HÀM DFS_Đệ_Quy(đồ_thị, đỉnh, đã_thăm)

    NẾU đỉnh KHÔNG thuộc đã_thăm THÌ

        ĐÁNH DẤU đỉnh là đã thăm

        HIỂN THỊ đỉnh 

        CHO MỖI đỉnh_kề trong đồ_thị[đỉnh] LÀM

            DFS_Đệ_Quy(đồ_thị, đỉnh_kề, đã_thăm)

    KẾT THÚC NẾU

KẾT THÚC HÀM

 

HÀM DFS_Stack(đồ_thị, đỉnh_bắt_đầu)

    TẠO một NGĂN_XẾP rỗng

    ĐẨY đỉnh_bắt_đầu vào NGĂN_XẾP

    TẠO một tập HỢP đã_thăm rỗng

    LẶP KHI NGĂN_XẾP KHÔNG rỗng

        LẤY RA đỉnh ← POP từ NGĂN_XẾP

        NẾU đỉnh KHÔNG thuộc đã_thăm THÌ

            ĐÁNH DẤU đỉnh là đã thăm

            HIỂN THỊ đỉnh  // Xử lý đỉnh hiện tại

            CHO MỖI đỉnh_kề trong đồ_thị[đỉnh] THEO THỨ TỰ ĐẢO NGƯỢC LÀM

                ĐẨY đỉnh_kề vào NGĂN_XẾP

    KẾT THÚC LẶP

KẾT THÚC HÀM

Cài đặt thuật toán đệ quy và stack trong python như sau:

def dfs_de_quy(graphnodevisited):

    """DFS đệ quy để duyệt mê cung."""

    if node not in visited:

        print(nodeend=" ")

        visited.add(node)

        for neighbor in graph[node]:

            dfs_de_quy(graphneighborvisited)

 

def dfs_stack(graphstart):

    """DFS sử dụng ngăn xếp để duyệt mê cung."""

    visited = set()

    stack = [start]

    while stack:

        node = stack.pop()

        if node not in visited:

            print(nodeend=" ")

            visited.add(node)

            stack.extend(reversed(graph[node]))

 

def main():

    # Danh sách kề của mê cung

    maze_graph = {

        0: [1],

        1: [04],

        2: [35],

        3: [26],

        4: [17],

        5: [269],

        6: [35],

        7: [48],

        8: [79],

        9: [5810],

        10: [911],

        11: [10]

    }

    print("DFS Đệ Quy:")

    visited_set = set()

    dfs_de_quy(maze_graph0visited_set)

    

    print("\nDFS Dùng Ngăn Xếp:")

    dfs_stack(maze_graph0)

Kết quả của chương trình: 

A number with black text

AI-generated content may be incorrect.

Ảnh 3‑4: Kết quả của thuật toán DFS cài đặt trên đệ quy và ngăn xếp

 

A screenshot of a diagram

AI-generated content may be incorrect.

Ảnh 3‑5: Ví dụ duyệt qua các nút

3.1.3. Thuật toán tìm theo chiều rộng (BFS)

BFS (Breadth-First Search) là một thuật toán tìm kiếm trên đồ thị hoặc cây, hoạt động theo chiều rộng. Thay vì đi sâu vào một nhánh như DFS, BFS sẽ duyệt theo từng mức trước khi mở rộng sang các đỉnh kế tiếp. 

Nguyên lý hoạt động của BFS là duyệt từ gốc (start), thăm tất cả các đỉnh kề của nó trước, sau đó, tiếp tục mở rộng các đỉnh ở mức tiếp theo theo thứ tự FIFO (First In, First Out) bằng hàng đợi (Queue). BFS đảm bảo tìm được đường đi ngắn nhất trong đồ thị không trọng số.

Các bước thực hiện thuật toán BFS:

  • Khởi tạo hàng đợi (queue) với đỉnh bắt đầu.
  • Tạo tập hợp đã thăm (visited) để lưu các đỉnh đã được duyệt.
  • Lặp lại các bước sau đến khi hàng đợi rỗng:
    • Lấy đỉnh đầu tiên trong hàng đợi.
    • Thêm các đỉnh kề của nó vào hàng đợi (nếu chưa được thăm).
  • Nếu gặp đỉnh đích (end), dừng lại.

Mã giả của thuật toán BFS:

HÀM BFS(đồ_thị, đỉnh_bắt_đầu):

    Tạo HÀNG_ĐỢI (queue) và thêm đỉnh_bắt_đầu vào

    Tạo danh sách đã_thăm

     LẶP KHI HÀNG_ĐỢI KHÔNG rỗng:

        LẤY đỉnh hiện tại từ HÀNG_ĐỢI

        NẾU đỉnh hiện tại là đỉnh đích:

            TRẢ VỀ đường đi

       

        THÊM đỉnh hiện tại vào danh sách đã_thăm

        CHO MỖI đỉnh_kề của đỉnh hiện tại:

            NẾU đỉnh_kề chưa được thăm:

                THÊM đỉnh_kề vào HÀNG_ĐỢI

                LƯU đường đi

       

    TRẢ VỀ "Không tìm thấy đường đi"

Cài đặt BFS trong python:

from collections import deque

 

def bfs_find_path(graphstartend):

    """BFS tìm đường đi ngắn nhất từ điểm bắt đầu đến điểm kết thúc."""

    queue = deque([(start, [start])])  # Hàng đợi lưu trạng thái (node, path)

    visited = set()

    while queue:

        nodepath = queue.popleft()

        if node == end:

            return path  # Trả về đường đi nếu tìm thấy

        if node not in visited:

            visited.add(node)

            for neighbor in graph[node]:

                if neighbor not in visited:

                    queue.append((neighborpath + [neighbor]))

    return None  # Không tìm thấy đường đi

 

Kết quả của thuật toán là: [0, 1, 4, 7, 8, 9, 10, 11]

Ảnh 3‑6: Hình ảnh tìm đường đi theo thuật toán BFS

 

3.1.4. Bài toán tìm đường đi ngắn nhất với giải thuật Dijkstra

Thuật toán Dijkstra là một thuật toán nổi tiếng dùng để tìm đường đi ngắn nhất từ một đỉnh nguồn đến các đỉnh khác trong đồ thị có trọng số không âm. Điểm nổi bật của Dijkstra:

  • Dùng hàng đợi ưu tiên để mở rộng các đỉnh có khoảng cách ngắn nhất trước.
  • Không hoạt động với trọng số âm, nhưng hoạt động hiệu quả với đồ thị không trọng số và có trọng số dương.
  • Tìm được đường đi ngắn nhất từ một đỉnh đến tất cả các đỉnh khác.

Dijkstra sử dụng một hàng đợi ưu tiên (priority queue) để chọn đỉnh gần nhất và mở rộng nó.

Các bước thực hiện thuật toán:

  1. Khởi tạo khoảng cách từ đỉnh bắt đầu đến tất cả các đỉnh là vô cực (∞), riêng đỉnh bắt đầu có khoảng cách = 0.
  2. Đưa đỉnh bắt đầu vào hàng đợi ưu tiên (ưu tiên đỉnh có khoảng cách ngắn nhất).
  3. Trong khi hàng đợi chưa rỗng:
  • Lấy ra đỉnh có khoảng cách nhỏ nhất từ hàng đợi.
  • Duyệt tất cả đỉnh kề và cập nhật khoảng cách nếu tìm được đường đi ngắn hơn.
  • Thêm các đỉnh được cập nhật vào hàng đợi ưu tiên.
  1. Khi tìm thấy đỉnh đích, truy vết lại đường đi từ đích về nguồn.

Mã giả của thuật toán:

HÀM Dijkstra(đồ_thị, đỉnh_bắt_đầu):

    Tạo hàng đợi ưu tiên (priority_queue)

    Khởi tạo khoảng cách của tất cả các đỉnh = ∞, riêng đỉnh_bắt_đầu = 0

    Đưa (đỉnh_bắt_đầu, 0) vào hàng đợi

    Trong khi hàng đợi KHÔNG rỗng:

        LẤY đỉnh hiện tại có khoảng cách nhỏ nhất

        NẾU đỉnh hiện tại là đỉnh đích:

            TRẢ VỀ đường đi

        CHO MỖI đỉnh_kề của đỉnh hiện tại:

            TÍNH khoảng cách mới

            NẾU khoảng cách mới NHỎ HƠN khoảng cách cũ:

                CẬP NHẬT khoảng cách

                ĐƯA vào hàng đợi

    TRẢ VỀ "Không tìm thấy đường đi"

Với bài toán tìm đường đi trong mê cung, coi trọng số của mỗi ô đi qua ô khác là 1, ta có thể tạo ra đồ trị trọng số từ mê cung bằng cách mở rộng hàm tìm danh sách kề.

def generate_weighted_graph(maze):

    """Chuyển ma trận mê cung thành đồ thị trọng số."""

    rowscols = len(maze), len(maze[0])

    graph = {}  # Đồ thị trọng số

    node_mapping = {}  # Ánh xạ (i, j) -> node_id

    node_id = 0

    

    # Đánh số các ô có thể đi

    for i in range(rows):

        for j in range(cols):

            if maze[i][j== 1:

                node_mapping[(ij)] = node_id

                graph[node_id= []

                node_id += 1

    

    # Tạo danh sách kề với trọng số

    directions = [(-10), (10), (0-1), (01)]  

    for (ij), node in node_mapping.items():

        for dxdy in directions:

            ninj = i + dxj + dy

            if (ninjin node_mapping:

                graph[node].append((node_mapping[(ninj)], 1))  

    return graphnode_mapping

 

Kết quả của hàm này ta có:

A screenshot of a computer code

AI-generated content may be incorrect.

Ảnh 3‑7: Kết quả tạo danh sách kề có trọng số

Từ kết quả trên ta thấy đỉnh 1 kề với đỉnh 0 và 4 và có trọng số là 1. Tương tự như các đỉnh khác. Với các bài toán khác, trọng số có thể thay đổi tùy thuộc bài toán.

Cài đặt hàm tìm kiếm Dijkstra trong python như sau:

def dijkstra(graphstartend):

    """Dijkstra tìm đường đi ngắn nhất từ start đến end trong đồ thị có trọng số."""

    priority_queue = []

    heapq.heappush(priority_queue, (0start, [start]))  # (khoảng cách, đỉnh, đường đi)

    

    shortest_distances = {nodefloat('inf'for node in graph}

    shortest_distances[start= 0

    while priority_queue:

        current_distancecurrent_nodepath = heapq.heappop(priority_queue)

        

        if current_node == end:

            return pathcurrent_distance  # Trả về đường đi và khoảng cách

        

        for neighborweight in graph[current_node]:

            distance = current_distance + weight

            if distance < shortest_distances[neighbor]:

                shortest_distances[neighbor= distance

                heapq.heappush(priority_queue, (distanceneighborpath + [neighbor]))

    

    return Nonefloat('inf')  # Không tìm thấy đường đi

 

Kết quả của chương trình ta có:

Ảnh 3‑8: Kết quả của thuật toán Dijkstra

A screenshot of a diagram

AI-generated content may be incorrect.

Ảnh 3‑9: Hình ảnh mô phỏng quá trình thực hiện thuật toán Dijkstra

 

Hàm Dijkstra thường được dùng trong việc tìm đường đi ngắn nhất với trọng số không đông đều. Trong AI thuật toán này thường được dùng để trong game hoặc robot. 

0
0
0 lượt chia sẻ
User

Bình luận

Đang tải bình luận...