Tag: thuật toán quay lui

  • Thuật toán giải sudoku bằng quay lui backtracking

    Thuật toán giải sudoku bằng quay lui backtracking

    Thuật toán giải sudoku bằng quay lui backtracking

    Trong bài này chúng ta sẽ sử dụng thuật toán quay lui để giải quyết bài toán giải Sudoku. Để hiểu thuật toán quay lui là gì mời bạn xem bài Thuật toán quay lui và minh họa.

    Xem thêm: Python: Bài toán xếp hậu sử dụng đệ quy

    Bài viết tham khảo từ techwithtim.net

    1. Giới thiệu luật chơi và cách giải Sudoku

    Sudoku là một trò chơi giải đố theo wiki định nghĩa như sau:

    Sudoku (数独すうどく (số độc) sūdoku?) (suːˈdoʊkuː/, /-ˈdɒ-/, /sə-/, ban đầu có tên gọi là Number Place) là một trò chơi câu đố sắp xếp chữ số dựa trên logic theo tổ hợp. Mục tiêu của trò chơi là điền các chữ số vào một lưới 9×9 sao cho mỗi cột, mỗi hàng, và mỗi phần trong số chín lưới con 3×3 cấu tạo nên lưới chính (cũng gọi là “hộp”, “khối”, hoặc “vùng”) đều chứa tất cả các chữ số từ 1 tới 9. Câu đố đã được hoàn thành một phần, người chơi phải giải tiếp bằng việc điền số. Mỗi câu đố được thiết lập tốt có một cách làm duy nhất.

    Cách giải trò chơi Sudoku, mời các bạn xem video hướng dẫn sau:

    2. Thuật toán giải Sudoku

    Sau đây ta sẽ tìm thuật toán giải Sudoku bằng kỹ thuật backtracking, ngôn ngữ lập trình sử dụng là Python. Các bước tiến hành như sau:

    • Viết hàm in câu đố Sudoku ra màn hình.
    • Tìm vị trí các ô trống trong Sudoku.
    • Với mỗi vị trí ô trống vừa tìm được, lần lượt thử đặt số từ 1 đến 9 vào ô trống đó. Kiểm tra xem sau khi thử đặt số đó vào ô trống đó thì có hợp lệ (thỏa mãn các điều kiện về luật chơi của Sudoku hay không). Nếu hợp lệ thì tiếp tục tìm các ô trống tiếp theo và lại thử, nếu không thì thử với số_tiếp_theo
    • Lặp lại quy trình trên cho đến khi không còn ô trống nào trên câu đố, hoặc không tìm được lời giải.

    Input, một câu đố sudoku biểu diễn bởi danh sách list 2 chiều (một danh sách gồm 9 phần tử, mỗi phần tử là dòng – lại là một danh sách gồm 9 phần tử  tương ứng với 9 ô trong một dòng) với các ô trống được quy ước điền bởi số 0, ví dụ Sudoku cau_do được biểu diễn như sau:

    cau_do = [
        [7,8,0,4,0,0,1,2,0],
        [6,0,0,0,7,5,0,0,9],
        [0,0,0,6,0,1,0,7,8],
        [0,0,7,0,4,0,2,6,0],
        [0,0,1,0,5,0,9,3,0],
        [9,0,4,0,6,0,0,0,5],
        [0,7,0,3,0,0,0,1,2],
        [1,2,0,0,0,7,4,0,0],
        [0,4,9,2,0,6,0,0,7]
    ]

    Chú ý rằng chỉ số index trong Python được đánh từ 0 trở đi, do đó các vị trí của từng ô trong bảng số sẽ là cau_d0[0][0] cho đến cau_do[8][8], ở đây cau_do[d][c] là ô số ở vị trí dòng d và cột c.

    2.1. Viết hàm in câu đố Sudoku ra màn hình

    Ở đây chúng ta sử dụng giao diện dòng lệnh, chưa sử dụng giao diện đồ họa GUI nên sẽ sử dụng hàm print() của Python để in một đối tượng ra màn hình CMD.

    Ta sẽ viết hàm in_sudoku để in một câu đố Sudoku có tên là q ra màn hình, sử dụng biến dc để biểu diễn dòng và cột.

    Nếu dòng d là dòng thứ 3 hoặc 6 thì ta sẽ in ra màn hình một dòng gồm các kí tự - - - - - - - - - - - để ngăn cách, mục đích là biểu diễn cho các khối ô vuông 3x3 của Sudoku. Tương tự, nếu cột ở vị trí 3 hoặc 6 thì ta sẽ in ra kí tự | để ngăn cách. Nếu cột ở vị trí thứ 8 thì ta sẽ xuống dòng mới.

    def in_sudoku(q):
        for d in range(len(q)):
            if d % 3 == 0 and d != 0:
                print("- - - - - - - - - - -")
            for c in range(len(q[0])):
                if c % 3 == 0 and c != 0:
                    print("| ", end ="")
                if c == 8:
                    print(str(q[d][c]))
                else:
                    print(str(q[d][c]) + " ", end = "")

    Thử in với cau_do ở phần đầu, chúng ta được kết quả như sau, ở đây tôi dùng SublimeText để code:

    thuật toán giải sudoku

    2.2. Viết hàm tìm các ô trống trong Sudoku

    Mục tiêu của chúng ta là tìm các vị trí ô trống trong câu đố q

    def tim_o_trong(q):
        for d in range(len(q)):
            for c in range(len(q[0])):
                if q[d][c] == 0:
                    return d, c
        return None

    2.3.  Viết hàm kiểm tra tính hợp lệ của một câu đố

    def kiem_tra(q, gia_tri, dong, cot):
        for i in range(len(q[0])):
            if q[i][cot] == gia_tri and i != dong:
                return False
        for i in range(len(q)):
            if q[dong][i] == gia_tri and i != cot:
                return False
    
        x = cot // 3
        y = dong // 3
    
        for i in range(y*3, y*3+3):
            for j in range(x*3, x*3+3):
                if q[i][j] == gia_tri and i != dong and j != cot:
                    return False
        return True

    2.4. Viết hàm chính để tìm lời giải cho một câu đố Sudoku

    def giai(q):
        tim_thay = tim_o_trong(q)
        if not tim_thay:
            return True
        else:
            d, c = tim_thay
        for i in range(1,10):
            if kiem_tra(q, i, d, c):
                q[d][c] = i
                if giai(q):
                    return True
                else:
                    q[d][c] = 0
            
        return False

    3. Chương trình Python giải Sudoku hoàn chỉnh

    cau_do = [
        [7,8,0,4,0,0,1,2,0],
        [6,0,0,0,7,5,0,0,9],
        [0,0,0,6,0,1,0,7,8],
        [0,0,7,0,4,0,2,6,0],
        [0,0,1,0,5,0,9,3,0],
        [9,0,4,0,6,0,0,0,5],
        [0,7,0,3,0,0,0,1,2],
        [1,2,0,0,0,7,4,0,0],
        [0,4,9,2,0,6,0,0,7]
    ]
    
    def in_sudoku(q):
        for d in range(len(q)):
            if d % 3 == 0 and d != 0:
                print("- - - - - - - - - - -")
            for c in range(len(q[0])):
                if c % 3 == 0 and c != 0:
                    print("| ", end ="")
                if c == 8:
                    print(str(q[d][c]))
                else:
                    print(str(q[d][c]) + " ", end = "")
    
    
    def giai(q):
        tim_thay = tim_o_trong(q)
        if not tim_thay:
            return True
        else:
            d, c = tim_thay
        for i in range(1,10):
            if kiem_tra(q, i, d, c):
                q[d][c] = i
                if giai(q):
                    return True
                else:
                    q[d][c] = 0
            
        return False
            
        
    
    def tim_o_trong(q):
        for d in range(len(q)):
            for c in range(len(q[0])):
                if q[d][c] == 0:
                    return d, c
        return None
    
    
    def kiem_tra(q, gia_tri, dong, cot):
        for i in range(len(q[0])):
            if q[i][cot] == gia_tri and i != dong:
                return False
        for i in range(len(q)):
            if q[dong][i] == gia_tri and i != cot:
                return False
    
        x = cot // 3
        y = dong // 3
    
        for i in range(y*3, y*3+3):
            for j in range(x*3, x*3+3):
                if q[i][j] == gia_tri and i != dong and j != cot:
                    return False
        return True
    
    in_sudoku(cau_do)
    giai(cau_do)
    print('Loi giai cua Sudoku tren la:')
    in_sudoku(cau_do)

    Cho chạy chương trình, chúng ta được kết quả như hình sau:

    giải sudoku bằng Python

  • Thuật toán quay lui và minh họa

    Thuật toán quay lui và minh họa

    Thuật toán quay lui và minh họa

    1. Thuật toán quay lui là gì?

    Thuật toán quay lui (Backtracking) là một kĩ thuật thiết kế giải thuật dựa trên đệ quy. Ý tưởng của quay lui là tìm lời giải từng bước, mỗi bước chọn một trong số các lựa chọn khả dĩ và đệ quy. Người đầu tiên đề ra thuật ngữ này (backtrack) là nhà toán học người Mỹ D. H. Lehmer vào những năm 1950.

    2. Thuật toán quay lui sử dụng khi nào?

    Thuật toán quay lui thường được sử dụng để giải bài toán liệt kê các cấu hình (như bài toán sinh các xâu nhị phân). Mỗi cấu hình được xây dựng bằng cách xác định từng phần tử. Mỗi phần tử lại được chọn bằng cách thử tất cả các khả năng.
    Các bước trong việc liệt kê cấu hình dạng X[1…n]:
    • Xét tất cả các giá trị X[1] có thể nhận, thử X[1] nhận các giá trị đó. Với mỗi giá trị của X[1] ta sẽ:
    • Xét tất cả giá trị X[2] có thể nhận, lại thử X[2] cho các giá trị đó. Với mỗi giá trị X[2] lại xét khả năng giá trị của X[3]…tiếp tục như vậy cho tới bước:
    • Xét tất cả giá trị X[n] có thể nhận, thử cho X[n] nhận lần lượt giá trị đó.
    • Thông báo cấu hình tìm được.

    Để cài đặt thuật toán quay lui, chúng ta sử dụng một chương trình con (hàm function, thủ tục procedure) và gọi đến hàm đó trong chương trình chính của mình. Mô hình của thuật toán quay lui sử dụng ngôn ngữ Python như sau:

    def quay_lui(i):
       for j in [tập_các_phương_án_x[i] có thể nhận]:
          <thử đặt x[i] = j>
          if <x[i] là phần tử cuối cùng trong cấu hình>:
             <thông báo cấu hình>
          else:
             <ghi nhận việc cho x[i] nhận giá trị j nếu cần>
             <gọi đệ quy đến quay_lui(i+1)>
             <bỏ ghi nhận việc cho x[i] nhận giá trị j để thử giá trị khác nếu cần>

    Thuật toán quay lui sẽ bắt đầu bằng lời gọi quay_lui(1)

    3. Minh họa của thuật toán quay lui (Backtracking)

    3.1. Sử dụng thuật toán quay lui để sinh các dãy nhị phân độ dài n

    Dưới đây, chúng ta cùng xem mã chương trình sinh các dãy nhị phân có độ dài n bằng cách sử dụng thuật toán quay lui.

    n = 3
    x = n*[0]
    
    
    def fine_print(x):
       tmp = ''
       for i in x:
          tmp += str(i)
       return tmp
    
    
    def bin_gen(i):
       for j in range(0,3):
          x[i] = j
          if i == n-1:
             print(fine_print(x))
          else:
             bin_gen(i+1)
    
    bin_gen(0)

    Các giải khác bằng cách sử dụng vòng lặp, xin mời bạn đọc xem tại đây Thuật toán sinh các dãy nhị phân có độ dài n

    3.2. Sử dụng backtracking để giải Sudoku

    thuật toán giải sudoku

    Mời các bạn xem chi tiết trong bài Thuật toán giải sudoku bằng quay lui backtracking

    3.3. Sử dụng quay lui để giải bài toán xếp hậu

    Xét bàn cờ tổng quát kích thước nxn. Một quân hậu trên bàn cờ có thể ăn được các quân khác nằm tại các ô cùng hàng, cùng cột hoặc cùng đường chéo. Hãy tìm các xếp n quân hậu trên bàn cờ sao cho không quân nào ăn quân nào. Mời bạn xem chi tiết trong bài Python: Bài toán xếp hậu sử dụng đệ quy

    bài toán xếp hậu với n=8

    4. Đặc điểm của thuật toán quay lui

    Bản chất của quay lui là một quá trình tìm kiếm theo chiều sâu(Depth-First Search).
    • Ưu điểm: Việc quay lui là thử tất cả các tổ hợp để tìm được một lời giải. Thế mạnh của phương pháp này là nhiều cài đặt tránh được việc phải thử nhiều trường hợp chưa hoàn chỉnh, nhờ đó giảm thời gian chạy.
    • Nhược điểm: Trong trường hợp xấu nhất độ phức tạp của quay lui vẫn là cấp số mũ. Vì nó mắc phải các nhược điểm sau:
    • Rơi vào tình trạng “thrashing”: quá trình tìm kiếm cứ gặp phải bế tắc với cùng một nguyên nhân.
      • Thực hiện các công việc dư thừa: Mỗi lần chúng ta quay lui, chúng ta cần phải đánh giá lại lời giải trong khi đôi lúc điều đó không cần thiết.
      • Không sớm phát hiện được các khả năng bị bế tắc trong tương lai. Quay lui chuẩn, không có cơ chế nhìn về tương lai để nhận biết được nhánh tìm kiếm sẽ đi vào bế tắc.
  • Thuật toán sinh các dãy nhị phân có độ dài n

    Thuật toán sinh các dãy nhị phân có độ dài n

    Thuật toán sinh các dãy nhị phân có độ dài n

    Trong bài viết này chúng ta sẽ tìm cách liệt kê toàn bộ các dãy nhị phân có độ dài n cho trước.

    • Yêu cầu: Liệt kê tất cả các dãy nhị phân có độ dài n. (dãy nhị phân là dãy chỉ gồm hai số 01)
    • Input: Nhập vào n – độ dài của chuỗi nhị phân cần in ra.
    • Output : In ra màn hình tất cả các chuỗi nhị phân có độ dài n nhập từ đầu vào.

    Ví dụ:

    • Input: 3
    • Output:
    • 000   001   010   011   100   101   110   111

    1. Liệt kê các dãy nhị phân độ dài n bằng phương pháp sinh tuần tự

    Phương pháp sinh tuần tự sử dụng để liệt kê tất cả các phương án [cấu hình] của bài toán tổ hợp, sao cho các bài toán đó thỏa mãn:

    • Có thể xác định được một thứ tự trên tập các cấu hình tổ hợp cần liệt kê. Từ đó có thể biết đượccấu hình đầu tiên và cấu hình cuối cùng trong thứ tự đó.
    • Xây dựng được thuật toán từ một cấu hình chưa phải cấu hình cuối, sinh ra được cấu hình kế tiếp nó.

    Theo đó, thuật toán sinh tuần tự các cấu hình được mô tả như sau:

    <Xây dựng cấu hình đầu tiên>
    while True:
       <Đưa ra cấu hình đang có>
       <Từ cấu hình đang có sinh ra cấu hình kế tiếp nếu còn>
       if <hết cấu hình>:
          break

    Lưu ý rằng, Python không có câu lệnh điều khiển luồn repeat... until như một số ngôn ngữ khác, nên ta phải sử dụng vòng lặp while (Xem chi tiết trong bài Bài 7. Câu lệnh vòng lặp while trong Python)

    Vì kiểu xâu string là immutable nên chúng ta sẽ sử dụng kiểu danh sách list để thao tác trên đó, và định nghĩa hàm fine_print() để in toàn bộ các phần tử của danh sách dưới dạng xâu kí tự.

    Một lưu ý là các chỉ số index trong Python luôn được đánh số từ 0, do đó phần tử thứ nhất của một danh sách list x sẽ có chỉ số là 0, tức là phần tử x[0]

    n = 3
    s = n*[0]
    
    def fill_char(s,i):
       for j in range(i,n):
          s[j] = 0
    def fine_print(x):
       tmp = ''
       for i in x:
          tmp += str(i)
       return tmp
    
    
    while True:
       print(fine_print(s))
       i = n - 1
       while (i > -1) and s[i] == 1:
          i = i - 1
       s[i] = 1
       fill_char(s,i+1)
       if i == -1:
          break

    Kết quả thu được như sau:

    000
    001
    010
    011
    100
    101
    110
    111

    2. Sinh các dãy nhị phân độ dài n bằng thuật toán quay lui (Backtracking)

    liệt kê các chuỗi nhị phân bằng thuật toán quay lui

    Vẫn sử dụng kiểu danh sách list và hàm fine_print() như ở phần trên, chúng ta có chương trình liệt kê tất cả các xâu nhị phân có độ dài n sử dụng thuật toán quay lui như sau:

    n = 3
    x = n*[0]
    
    
    def fine_print(x):
       tmp = ''
       for i in x:
          tmp += str(i)
       return tmp
    
       
    def bin_gen(i):
       for j in range(0,2):
          x[i] = j
          if i == n-1:
             print(fine_print(x))
          else:
             bin_gen(i+1)
    
    bin_gen(0)

    Thuật toán quay lui có ưu điểm là rất dễ cài đặt so với phương pháp sinh tuần tự, hơn nữa có thể dễ dàng mở rộng để giải quyết nhiều bài toán khác nhau. Chẳng hạn, ta dễ dàng giải quyết bài toán sinh các dãy tam phân bằng cách thay range(0,2) bởi range(0,3). Kết quả thu được như sau:

    000
    001
    002
    010
    011
    012
    020
    021
    022
    100
    101
    102
    110
    111
    112
    120
    121
    122
    200
    201
    202
    210
    211
    212
    220
    221
    222