+1

Bài toán Nonogram

Bài viết được đăng lại từ đây: https://hackmd.io/@alexisparis/itknowledge2

Đặt vấn đề

9ebfe5e8-2124-474b-abe3-5c400383d9ce

Bài viết này dự kiến sẽ được hoàn thành vào dịp cận Halloween (hoặc là run rủi sẽ rơi vào đúng hôm Halloween, không chắc nữa), vậy nên trước hết tôi xin dành một lời chúc đến các bạn đọc một mùa Halloween vui vẻ (đặc biệt là các bạn trẻ đang trải nghiệm trực tiếp cảm giác cầm lồng đèn Jack O'Lantern và đi gõ cửa các nhà tại các nước Âu Mỹ, hoặc là những người được gõ cửa và chào đón bằng câu "Trick or Treat"/"Un bonbon ou Un sort").

Nếu các bạn còn nhớ về chuỗi những quảng cáo trò chơi rác đã từng gặp qua, đỉnh điểm là vào năm 2023 (chính xác là tôi gặp nó rất thường xuyên và ở IP trong nước Pháp), các bạn sẽ hình dung được cách tôi tạo ra bức hình bên trên. Với các hàng và cột gắn liền với một dãy số định sẵn, chúng ta sẽ tô đen các ô trong một bức khung trắng, sao cho số lượng và bố cục của ô đen ở mỗi hàng và cột thỏa mãn các dãy số gắn liền với chúng (và phải đảm bảo rằng nó đã được chuẩn bị một cách logic nhất).

Loại puzzle trên có rất nhiều tên gọi, từ Picross, Logigraphe, Hanjie, Griddler, Nonogram, Logimage, hay gắn với đồ án năm 3 (năm cuối) Đại học của tôi với cách gọi Tomographie discrète. Theo những gì tôi lượm vu vơ trên Wikipedia tiếng Anh, nó có liên quan đến giải thưởng của biên tập viên đồ họa người Nhật Bản Non Ishida về việc tạo ra một bức tranh lưới từ các hệ thống ánh sáng tòa nhà chọc trời, và trực tiếp khai sinh bởi nhà thiết kế puzzle đồng hương Tetsuya Nishio trong cùng khoảng thời gian. Đồ án của tôi, cũng như của sinh viên năm 3 Cử nhân tại Đại học Sorbonne niên khóa 2023-2024, chính là tạo ra một thuật toán giải quyết các puzzle như thế trên cơ sở một file .txt cung cấp sẵn các dãy số tương ứng với hàng và cột.

Ở đây tôi sẽ gọi nó là Nonogram - một cách gọi quen mồm từ những ngày đầu mày mò thực hiện bài lập trình, bắt nguồn từ tên ứng dụng game tôi đã tải về để chơi liên tục trong một tuần đầu. Trong bài này, tôi sẽ xen kẽ hai phương pháp giải vấn đề: Phương pháp tối ưu (Optimal method) và Phương pháp gắn với bản năng người chơi (Instinct-based method).

Tiếp cận bài toán

Giải quyết một chuỗi ô

Đối với một chuỗi ô, có thể là một hàng hoặc một cột của khung trò chơi, vấn đề đặt ra cho mỗi ô là nên chọn tô màu đen hay màu trắng. Việc này được thực hiện lên trên hai trường hợp khái quát: một chuỗi ô trống không và một chuỗi ô đã được tô màu một ít.

Chúng ta mô hình hóa vấn đề trên bằng giải thuật đệ quy nhị nguyên dưới đây:

Ta gọi L{0,1,2}mL\in\{0,1,2\}^m là chuỗi mm ô tô màu theo dãy số tương ứng ss, với:

  • 00 tương ứng với ô chưa được tô.
  • 11 tương ứng với ô được tô trắng.
  • 22 tương ứng với ô được tô đen.

Gọi lNl\in\Bbb N là chỉ mục định vị dãy số s=(sl)l=1s=(s_l)_{l=1}.

Gọi jNj\in\Bbb N là chỉ mục định vị chuỗi ô L=(Lj)j=0L=(L_j)_{j=0}.

Hàm T(L,s,j,l)T(L,s,j,l) sẽ trả ra giá trị đúng (\top) và sai (\bot) dựa trên chuỗi.

Chúng ta sẽ bắt đầu giải thuật với giá trị lớn nhất của các chỉ mục: T(L,s,L1,s)T(L,s,|L|-1,|s|)

  1. Bước cơ sở:

    1. l=0l=0: Khi mọi giá trị trong dãy số đã được tuân thủ. Chúng ta liệt kê hai trường hợp dưới đây:

      • j=0j=0: Không còn ô nào phía trước ô hiện tại LjL_j:

      T(L,s,0,0)=T(L,s,0,0)=\top

      • j>0j>0: Còn một cơ số ô ở phía trước ô hiện tại LjL_j:
        • Nếu tồn tại bất kì ô LiL_i (i<ji<j) đã được tô đen, vậy có nghĩa là các ô trong chuỗi LL đã bị tô không hợp lệ:

        T(L,s,j,0)=T(L,s,j,0)=\bot

        • Nếu không, chuỗi ô được mặc nhiên tô đúng:

        T(L,s,j,0)=T(L,s,j,0)=\top

    2. j<sl1j<s_l-1: Số ô của chuỗi còn lại được xem xét nhỏ hơn với giá trị ở thứ tự ll trong dãy số. Không nghi ngờ gì, chuỗi được tô không hợp lệ:

    T(L,s,j,l)=T(L,s,j,l)=\bot

    1. j=sl1j=s_l-1: Số ô của chuỗi còn lại được xem xét bằng đúng với giá trị ở thứ tự ll trong dãy số Chúng ta liệt kê hai trường hợp dưới đây:
      • l=1l=1: Giá trị được so sánh của dãy số là giá trị ở hàng đầu Chúng ta sẽ xem xét trong j+1j+1 ô còn lại như sau:
        • Nếu trong số đó có ô đã được tô trắng, vậy nghĩa là chuỗi đang không được tô hợp lệ:

        T(L,s,j,1)=T(L,s,j,1)=\bot

        • Nếu không, chuỗi ô vẫn được xác định là tô hợp lệ:

        T(L,s,j,1)=T(L,s,j,1)=\top

      • l>1l>1: Giá trị được so sánh của dãy số không phải là giá trị ở hàng đầu Không nghi ngờ gì, chuỗi đã bị tô không hợp lệ:

      T(L,s,j,l)=T(L,s,j,l)=\bot

  2. Bước đệ quy

    1. Nếu ô jj đã được tô trắng, công việc tiếp theo của ta là đệ quy để kiểm tra các ô phía trước với cùng giá trị trong dãy số:

    T(L,s,j,l)=T(L,s,j1,l)T(L,s,j,l)=T(L,s,j-1,l)

    1. Nếu ô jj đã được tô đen, chúng ta xem xét chuỗi sl+1s_l+1 ô phía trước, trên giả định ô LjL_j là ô phía cùng bên phải trong chuỗi ô đen. Để diễn giải cho dễ hiểu, ta xem xét nhóm ô từ LjslL_{j-s_l} đến Lj1L_{j-1}.

      • Nếu ô LjslL_{j-s_l} đã được tô đen, điều này có nghĩa là trong mọi trường hợp, ô LjL_j không thể được tô đen vì sẽ không thể tạo ra chuỗi thỏa mãn giá trị sls_l:

      T(L,s,j,l)=T(L,s,j,l)=\bot

      • Trong chuỗi ô từ Ljsl+1L_{j-s_l+1} đến Lj1L_{j-1}, điều này có nghĩa là không thể tô đen các ô kế tiếp để thỏa mãn việc ô LjL_j là ô cuối chuỗi:

      T(L,s,j,l)=T(L,s,j,l)=\bot

      • Nếu không phạm phải bất kỳ quy tắc nào phía trên, chúng ta sẽ đệ quy kiểm tra tiếp các ô trước đó cùng với giá trị phía trước trong dãy số:

      T(L,s,j,l)=T(L,s,jsl1,l1)T(L,s,j,l)=T(L,s,j-s_l-1,l-1)

    2. Nếu ô chưa được tô, ta có hai hàm T1(j,l)T_1(j,l)T2(j,l)T_2(j,l) tương ứng với hai trường hợp 1 và 2 của bước đệ quy:

    T(L,s,j,l)=T1(L,s,j,l)T2(L,s,j,l)T(L,s,j,l)=T_1(L,s,j,l)\lor T_2(L,s,j,l)

Tóm lại, giá trị của T(L,s,j,l)T(L,s,j,l) theo jjll tương ứng:

T(L,s,j,l)={,j=0l=0¬i(i<jLi=2),j>0l=0,j>0l>0j<sl1¬i(i<jLi=1),j>0l=1j=sl1,j>0l>1j=sl1T(L,s,j1,l),j>0l>1Lj=1{T(L,s,jsl1,l1)Ljsl2¬i(jsl<i<jLi=1),j>0l>1Lj=2T(L,s,j,l)=\begin{cases}\top,&j=0\land l=0\\\lnot\exists i(i<j\land L_i=2),&j>0\land l=0\\\bot,&j>0\land l>0\land j<s_l-1\\\lnot\exists i(i<j\land L_i=1),&j>0\land l=1\land j=s_l-1\\\bot,&j>0\land l>1\land j=s_l-1\\T(L,s,j-1,l),&j>0\land l>1\land L_j=1\\\begin{cases}T(L,s,j-s_l-1,l-1)\\L_{j-s_l}\neq2\\\lnot\exists i(j-s_l<i<j\land L_i=1)\end{cases},&j>0\land l>1\land L_j=2\end{cases}

Độ khó của chuỗi ô

Hàm T(L,s,j,l)T(L,s,j,l) nêu trên có tác dụng xác định việc tô màu chuỗi ô có hợp lệ hay không. Tuy nhiên, một chuỗi ô có thể có nhiều cách tô khác nhau cùng thỏa mãn một dãy số.

Ta hãy xem xét các ví dụ dưới đây về việc tô màu theo các dãy số ss lên trên một chuỗi gồm 11 ô vuông L=L= ⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜.

Ví dụ 1: s=(8)s=(8)

Chúng ta có các cách tô như sau:

  • Cách 1: ⬛️ ⬛️ ⬛️ ⬛️ ⬛️ ⬛️ ⬛️ ⬛️ ⬜️ ⬜️ ⬜️
  • Cách 2: ⬜️ ⬛️ ⬛️ ⬛️ ⬛️ ⬛️ ⬛️ ⬛️ ⬛️ ⬜️ ⬜️
  • Cách 3: ⬜️ ⬜️ ⬛️ ⬛️ ⬛️ ⬛️ ⬛️ ⬛️ ⬛️ ⬛️ ⬜️
  • Cách 4: ⬜️ ⬜️ ⬜️ ⬛️ ⬛️ ⬛️ ⬛️ ⬛️ ⬛️ ⬛️ ⬛️

Ví dụ 2: s=(3,5)s=(3,5)

Chúng ta có các cách tô như sau:

  • Cách 1: ⬛️ ⬛️ ⬛️ ⬜️ ⬛️ ⬛️ ⬛️ ⬛️ ⬛️ ⬜️ ⬜️
  • Cách 2: ⬛️ ⬛️ ⬛️ ⬜️ ⬜️ ⬛️ ⬛️ ⬛️ ⬛️ ⬛️ ⬜️
  • Cách 3: ⬛️ ⬛️ ⬛️ ⬜️ ⬜️ ⬜️ ⬛️ ⬛️ ⬛️ ⬛️ ⬛️
  • Cách 4: ⬜️ ⬛️ ⬛️ ⬛️ ⬜️ ⬛️ ⬛️ ⬛️ ⬛️ ⬛️ ⬜️
  • Cách 5: ⬜️ ⬛️ ⬛️ ⬛️ ⬜️ ⬜️ ⬛️ ⬛️ ⬛️ ⬛️ ⬛️
  • Cách 6: ⬜️ ⬜️ ⬛️ ⬛️ ⬛️ ⬜️ ⬛️ ⬛️ ⬛️ ⬛️ ⬛️

Ví dụ 3: s=(2,3,2)s=(2,3,2)

Chúng ta có các cách tô như sau:

  • Cách 1: ⬛️ ⬛️ ⬜️ ⬛️ ⬛️ ⬛️ ⬜️ ⬛️ ⬛️ ⬜️ ⬜️
  • Cách 2: ⬛️ ⬛️ ⬜️ ⬛️ ⬛️ ⬛️ ⬜️ ⬜️ ⬛️ ⬛️ ⬜️
  • Cách 3: ⬛️ ⬛️ ⬜️ ⬛️ ⬛️ ⬛️ ⬜️ ⬜️ ⬜️ ⬛️ ⬛️
  • Cách 4: ⬛️ ⬛️ ⬜️ ⬜️ ⬛️ ⬛️ ⬛️ ⬜️ ⬛️ ⬛️ ⬜️
  • Cách 5: ⬛️ ⬛️ ⬜️ ⬜️ ⬛️ ⬛️ ⬛️ ⬜️ ⬜️ ⬛️ ⬛️
  • Cách 6: ⬛️ ⬛️ ⬜️ ⬜️ ⬜️ ⬛️ ⬛️ ⬛️ ⬜️ ⬛️ ⬛️
  • Cách 7: ⬜️ ⬛️ ⬛️ ⬜️ ⬛️ ⬛️ ⬛️ ⬜️ ⬛️ ⬛️ ⬜️
  • Cách 8: ⬜️ ⬛️ ⬛️ ⬜️ ⬛️ ⬛️ ⬛️ ⬜️ ⬜️ ⬛️ ⬛️
  • Cách 9: ⬜️ ⬛️ ⬛️ ⬜️ ⬜️ ⬛️ ⬛️ ⬛️ ⬜️ ⬛️ ⬛️
  • Cách 9: ⬜️ ⬜️ ⬛️ ⬛️ ⬜️ ⬛️ ⬛️ ⬛️ ⬜️ ⬛️ ⬛️

Ví dụ 4: s=(3)s=(3)

Chúng ta có các cách tô như sau:

  • Cách 1: ⬛️ ⬛️ ⬛️ ⬜️ ⬜️ ⬜️ ⬜️ ⬜️ ⬜️ ⬜️ ⬜️
  • Cách 2: ⬜️ ⬛️ ⬛️ ⬛️ ⬜️ ⬜️ ⬜️ ⬜️ ⬜️ ⬜️ ⬜️
  • Cách 3: ⬜️ ⬜️ ⬛️ ⬛️ ⬛️ ⬜️ ⬜️ ⬜️ ⬜️ ⬜️ ⬜️
  • Cách 4: ⬜️ ⬜️ ⬜️ ⬛️ ⬛️ ⬛️ ⬜️ ⬜️ ⬜️ ⬜️ ⬜️
  • Cách 5: ⬜️ ⬜️ ⬜️ ⬜️ ⬛️ ⬛️ ⬛️ ⬜️ ⬜️ ⬜️ ⬜️
  • Cách 6: ⬜️ ⬜️ ⬜️ ⬜️ ⬜️ ⬛️ ⬛️ ⬛️ ⬜️ ⬜️ ⬜️
  • Cách 7: ⬜️ ⬜️ ⬜️ ⬜️ ⬜️ ⬜️ ⬛️ ⬛️ ⬛️ ⬜️ ⬜️
  • Cách 8: ⬜️ ⬜️ ⬜️ ⬜️ ⬜️ ⬜️ ⬜️ ⬛️ ⬛️ ⬛️ ⬜️
  • Cách 9: ⬜️ ⬜️ ⬜️ ⬜️ ⬜️ ⬜️ ⬜️ ⬜️ ⬛️ ⬛️ ⬛️

Ví dụ 5: s=(3,2)s=(3, 2)

Chúng ta có các cách tô như sau:

  • Cách 1: ⬛️ ⬛️ ⬛️ ⬜️ ⬛️ ⬛️ ⬜️ ⬜️ ⬜️ ⬜️ ⬜️
  • Cách 2: ⬛️ ⬛️ ⬛️ ⬜️ ⬜️ ⬛️ ⬛️ ⬜️ ⬜️ ⬜️ ⬜️
  • Cách 3: ⬛️ ⬛️ ⬛️ ⬜️ ⬜️ ⬜️ ⬛️ ⬛️ ⬜️ ⬜️ ⬜️
  • Cách 4: ⬛️ ⬛️ ⬛️ ⬜️ ⬜️ ⬜️ ⬜️ ⬛️ ⬛️ ⬜️ ⬜️
  • Cách 5: ⬛️ ⬛️ ⬛️ ⬜️ ⬜️ ⬜️ ⬜️ ⬜️ ⬛️ ⬛️ ⬜️
  • Cách 6: ⬛️ ⬛️ ⬛️ ⬜️ ⬜️ ⬜️ ⬜️ ⬜️ ⬜️ ⬛️ ⬛️
  • Cách 7: ⬜️ ⬛️ ⬛️ ⬛️ ⬜️ ⬛️ ⬛️ ⬜️ ⬜️ ⬜️ ⬜️
  • Cách 8: ⬜️ ⬛️ ⬛️ ⬛️ ⬜️ ⬜️ ⬛️ ⬛️ ⬜️ ⬜️ ⬜️
  • Cách 9: ⬜️ ⬛️ ⬛️ ⬛️ ⬜️ ⬜️ ⬜️ ⬛️ ⬛️ ⬜️ ⬜️
  • Cách 10: ⬜️ ⬛️ ⬛️ ⬛️ ⬜️ ⬜️ ⬜️ ⬜️ ⬛️ ⬛️ ⬜️
  • Cách 11: ⬜️ ⬛️ ⬛️ ⬛️ ⬜️ ⬜️ ⬜️ ⬜️ ⬜️ ⬛️ ⬛️
  • Cách 12: ⬜️ ⬜️ ⬛️ ⬛️ ⬛️ ⬜️ ⬛️ ⬛️ ⬜️ ⬜️ ⬜️
  • Cách 13: ⬜️ ⬜️ ⬛️ ⬛️ ⬛️ ⬜️ ⬜️ ⬛️ ⬛️ ⬜️ ⬜️
  • Cách 14: ⬜️ ⬜️ ⬛️ ⬛️ ⬛️ ⬜️ ⬜️ ⬜️ ⬛️ ⬛️ ⬜️
  • Cách 15: ⬜️ ⬜️ ⬛️ ⬛️ ⬛️ ⬜️ ⬜️ ⬜️ ⬜️ ⬛️ ⬛️
  • Cách 16: ⬜️ ⬜️ ⬜️ ⬛️ ⬛️ ⬛️ ⬜️ ⬛️ ⬛️ ⬜️ ⬜️
  • Cách 17: ⬜️ ⬜️ ⬜️ ⬛️ ⬛️ ⬛️ ⬜️ ⬜️ ⬛️ ⬛️ ⬜️
  • Cách 18: ⬜️ ⬜️ ⬜️ ⬛️ ⬛️ ⬛️ ⬜️ ⬜️ ⬜️ ⬛️ ⬛️
  • Cách 19: ⬜️ ⬜️ ⬜️ ⬜️ ⬛️ ⬛️ ⬛️ ⬜️ ⬛️ ⬛️ ⬜️
  • Cách 20: ⬜️ ⬜️ ⬜️ ⬜️ ⬛️ ⬛️ ⬛️ ⬜️ ⬜️ ⬛️ ⬛️
  • Cách 21: ⬜️ ⬜️ ⬜️ ⬜️ ⬜️ ⬛️ ⬛️ ⬛️ ⬜️ ⬛️ ⬛️

Ví dụ 6: Một số trường hợp cụ thể chỉ có một cách tô

  1. s=()s=(): ⬜️ ⬜️ ⬜️ ⬜️ ⬜️ ⬜️ ⬜️ ⬜️ ⬜️ ⬜️ ⬜️

  2. s=(11)s=(11): ⬛️ ⬛️ ⬛️ ⬛️ ⬛️ ⬛️ ⬛️ ⬛️ ⬛️ ⬛️ ⬛️

  3. s=(3,7)s=(3,7): ⬛️ ⬛️ ⬛️ ⬜️ ⬛️ ⬛️ ⬛️ ⬛️ ⬛️ ⬛️ ⬛️

  4. s=(2,4,3)s=(2,4,3): ⬛️ ⬛️ ⬜️ ⬛️ ⬛️ ⬛️ ⬛️ ⬜️ ⬛️ ⬛️ ⬛️

  5. s=(1,1,1,1,1,1)s=(1,1,1,1,1,1): ⬛️ ⬜️ ⬛️ ⬜️ ⬛️ ⬜️ ⬛️ ⬜️ ⬛️ ⬜️ ⬛️

Thông qua các ví dụ trên, chúng ta có thể đưa ra các kết luận như sau: Độ khó của chuỗi ô phụ thuộc vào tổng các giá trị trong dãy số và độ dài của dãy.

Trong trường hợp dãy số trống (s=()s=()) hoặc dãy số một giá trị duy nhất (s=(s1)s=(s_1)) tương ứng với độ dài của chuỗi (s1=Ls_1=|L|), ta chỉ có duy nhất một cách tô chuỗi.

Nếu độ dài và các giá trị của dãy đảm bảo được đầu và cuối chuỗi được tô đen và giữa các chuỗi con chỉ cần một ô tô trắng để phân cách, thì chuỗi cũng chỉ có duy nhất một cách tô.

Đối với dãy số chỉ có một giá trị, số cách tô phụ thuộc vào độ lớn của giá trị ấy. Khi độ lớn của giá trị càng nhỏ, số cách tô màu chuỗi càng tăng.

Đối với dãy số có từ hai giá trị trở lên, ta đặt γ=sjssj+s1\gamma=\sum_{s_j\in s}s_j+|s|-1.

Số ô trắng tối thiểu để ngăn cách các chuỗi con được tô đen là s1|s|-1.

Giá trị của γ\gamma càng nhỏ thì số cách tô màu chuỗi càng tăng.

Bổ đề: Bài toán chia kẹo Euler (Stars and Bars counting method)

Cho phương trình i=1kxi=n\sum_{i=1}^kx_i=n với tập nghiệm S{x:=(xi)i=1kxNk}S\subset\left\{x:=(x_i)_{i=1}^k\vert x\in\Bbb N^k\right\}

Tập nghiệm SS có độ lớn là Cn+k1k1=(n+k1)!(k1)!n!C_{n+k-1}^{k-1}=\frac{(n+k-1)!}{(k-1)!n!}

Chứng minh

Gọi f(n,k)f(n,k) số nghiệm của phương trình i=1kxi=n\sum_{i=1}^kx_i=n. Mệnh đề Π(n,k)\Pi(n,k) đúng nếu f(n,k)=Cn+k1k1f(n,k)=C_{n+k-1}^{k-1}.

  1. Bước cơ sở: Nếu n=1n=1k=1k=1, tổng số nghiệm của phương trình x1=0x_1=0f(1,1)=1=C10=C1+1111f(1,1)=1=C_1^0=C_{1+1-1}^{1-1}. Mệnh đề Π(1,1)\Pi(1,1) đúng.

  2. Bước quy nạp: Giả sử xk=t{i}i=1nx_k=t\in\{i\}_{i=1}^n, bằng quy tắc chuyển vế chúng ta có: i=1k1xi=nt\sum_{i=1}^{k-1}x_i=n-t. Số nghiệm lúc này là f(nt,k1)f(n-t,k-1). Vậy f(n,k)=t=0nf(nt,k1)f(n,k)=\sum_{t=0}^nf(n-t,k-1). Giả sử mệnh đề Π(nt,k1)\Pi(n-t,k-1) đúng, ta có: f(nt,k1)=Cnt+k2k2f(n-t,k-1)=C_{n-t+k-2}^{k-2}. Ta vì vậy có:

f(n,k)=t=0nf(nt,k1)=t=0nCnt+k2k2=t=0n(nt+k2)!(k2)!(nt)!=(n+k1)!(k1)!n!=Cn+k1k1\begin{array}{l}f(n,k)&=\sum_{t=0}^nf(n-t,k-1)\\&=\sum_{t=0}^nC_{n-t+k-2}^{k-2}\\&=\sum_{t=0}^n\frac{(n-t+k-2)!}{(k-2)!(n-t)!}\\&=\frac{(n+k-1)!}{(k-1)!n!}\\&=C_{n+k-1}^{k-1}\end{array}

Bằng cách chứng minh quy nạp, ta kết luận mệnh đề Π(n,k)\Pi(n,k) thỏa mãn với mọi giá trị của nnkk.

Định luật

Áp dụng bổ đề

Khi một chuỗi ô được tô màu theo dãy số, giả sử tồn tại trên chuỗi các "khe" được ngăn cách bởi các chuỗi con được tô đen, bao gồm 2 khe ở hai đầu chuỗi, và các khe ở giữa. Mỗi khe nằm ở giữa đều mặc định chứa một ô trắng ban đầu, các khe ngoài cùng được mặc định trống rỗng.

Bài toán được đặt ra: Làm sao xếp các ô trắng còn lại vào các khe.

Gọi kk là số khe, ta có k=sk=|s|.

Gọi nn là tổng số ô trắng "tự do", ta có n=Lγn=|L|-\gamma với γ\gamma đã được nêu phía trên.

Gọi xix_i là số ô trắng nằm trong các khe, ta có phương trình i=1kxi=n\sum_{i=1}^kx_i=n.

Như vậy ta có định luật:

Đặt n=Lγ=L(sjssj+s1)n=|L|-\gamma=|L|-(\sum_{s_j\in s}s_j+|s|-1).

Đặt k=s+1k=|s|+1.

Số cách tô màu chuỗi LL thỏa mãn dãy số là Cn+k1k1C_{n+k-1}^{k-1}

Hệ quả

Quan sát ví dụ 1, ta thấy năm ô từ L3L_3 đến L7L_7 đều được tô đen trong tất cả các cách tô.

Quan sát ví dụ 2, ta thấy bốn ô L2L_2L6L_6 đến L8L_8 đều được tô đen trong tất cả các cách tô.

Quan sát ví dụ 3, ta thấy duy nhất ô L5L_5 được tô đen trong tất cả các cách tô.

Quan sát ví dụ 4 và 5, không có ô nào được tô cố định màu qua các cách.

Ta lý giải vấn đề này như sau: Một ô được tô màu đen cố định khi không thể tô màu trắng trong bất kỳ cách tô nào.

Với mỗi chuỗi con tô theo dãy số, sự chênh lệch trong việc tô phụ thuộc vào số ô trắng "tự do". Nói cách khác, quay trở lại phần bổ đề trên, sự chênh lệch nằm ở việc tồn tại một khe có nhiều ô trắng "tự do" nhất được bỏ vào, trong khi các khe khác không có

Như vậy, tổng số ô được tô đen cố định là max(0,sjssjn.s)\max(0,\sum_{s_j\in s}s_j-n.|s|), từ đó một chuỗi con sẽ có các ô tô đen cố định khi và chỉ khi sj>ns_j>n.

Một chuỗi ô có độ khó thấp nhất là chuỗi ô không có ô trắng "tự do", vì thế ta có hệ quả:

Chuỗi ô chỉ có 1 cách tô duy nhất để thỏa mãn dãy số LL khi và chỉ khi γ=L\gamma=|L|.

Xác định chỉ mục các ô tô đen cố định.

Để xác định vùng tô cố định của một chuỗi con trên chuỗi ô, ta có công thức:

jl[δl+n,δl+sl]j_l^*\in[\delta_l+n,\delta_l+s_l]

Với δl=0<i<l(si+i)\delta_l=\sum_{0<i<l}(s_i+i) là số ô tối thiểu được tô từ trái sang cho đến khi tô chuỗi sls_l.

Các thuật ngữ

Quy hoạch động

Quy hoạch động (Dynamic programming) được định nghĩa là chia vấn đề thành các vấn đề nhỏ, sao cho giữa các vấn đề nhỏ không có sự độc lập với nhau. Để thực thi mô típ này, một cấu trúc dữ liệu được sử dụng để lưu trữ các kết quả của vấn đề nhỏ, sao cho chúng được tái sử dụng ở các vấn đề nhỏ khác và cả những vấn đề lớn hơn.

Thông thường, các trang web tôi đọc qua đều sử dụng Fibonacci làm ví dụ cho việc áp dụng phương pháp trên:

Fk={0,k=01,k=1Fk1+Fk2,k>1F_k=\begin{cases}0,&k=0\\1,&k=1\\F_{k-1}+F_{k-2},&k>1\end{cases}

Bằng phương pháp đệ quy truyền thống, ta có:

Hàm Fibonacci(k)\text{Fibonacci}(k) như sau:

  • Nếu k=0k=0 thì:
    • Trả ra 00.
  • Còn nếu k=1k=1 thì:
    • Trả ra 11.
  • Kết thúc điều kiện
  • Trả ra Fibonacci(k1)+Fibonacci(k2)\text{Fibonacci}(k-1)+\text{Fibonacci}(k-2)

Kết thúc hàm

Sự đệ quy của hàm tạo ra một cây nhị phân với số nút đệ quy là O(2k)O(2^k).

Ở mỗi nút, phần phi đệ quy không có vòng lặp, nên độ phức tạp cục bộ là O(1)O(1).

Vậy độ phức tạp chung của hàm là O(2k)O(2^k).

Ta có hai cách tiếp cận quy hoạch động sau đây

  1. Phương pháp top-down memoization (giải quyết từ trên xuống dưới và ghi nhớ thông qua phép đệ quy):

Gọi memory(mi)i=0k\text{memory}\gets(m_i)_{i=0}^{k} với giá trị khởi tạo của m0m_000, m1m_111 và các giá trị khác được khởi tạo là 1-1. Hàm Fibonacci(k)\text{Fibonacci}(k) như sau:

  • Nếu k1k\leq1 thì:
    • Trả ra mkm_k.
  • Nếu không:
    • Nếu mk=1m_k=-1 thì:
      • mkFibonacci(k1)+Fibonacci(k2)m_k\gets\text{Fibonacci}(k-1)+\text{Fibonacci}(k-2).
    • Kết thúc điều kiện
  • Kết thúc điều kiện
  • Trả ra mkm_k.

Kết thúc hàm

  1. Phương pháp bottom-up tabulation (giải quyết từ dưới lên trên để làm cơ sở thông qua phép lặp):

Gọi memory(mi)i=0k\text{memory}\gets(m_i)_{i=0}^{k} với giá trị khởi tạo của giá trị mim_i1-1. Hàm Fibonacci(k)\text{Fibonacci}(k) như sau:

  • Với mọi i{j}j=0ki\in\{j\}_{j=0}^k tiến hành:
    • Nếu i=0i=0 thì:
      • mi0m_i\gets0.
    • Còn nếu i=1i=1 thì:
      • mi1m_i\gets1.
    • Nếu không:
      • mimi1+mi2m_i\gets m_{i-1}+m_{i-2}.
    • Kết thúc điều kiện
  • Kết thúc vòng lặp
  • Trả ra mkm_k.

Kết thúc hàm

Đặc điểm của phương pháp quy hoạch động chính là mỗi bài toán con sẽ chỉ được tính toán đúng một lần. Vì thế độ phức tạp của hai hàm Fibonacci(k)\text{Fibonacci}(k) trên là O(k)O(k).

Như vậy là phương pháp quy hoạch động rõ ràng tối ưu về mặt thời gian.

Vì vậy, giải thuật quy hoạch động thường được sử dụng trong các trường hợp sau:

  • Các bài toán con "gối nhau": Khi kết quả từ bài toán con này hoàn toàn có thể tái sử dụng cho bài toán con kia.
  • Tối ưu hóa cấu trúc con nhằm tối ưu hóa bài toán lớn: Thường thấy trong các bài toán lên kế hoạch và đồ thị.

Giải thuật tham lam

Giải thuật tham lam (Greedy algorithm) là giải thuật giải quyết một vấn đề dựa trên các lựa chọn đã được xếp hạng, phương pháp tối ưu nhất trong thời điểm sẽ được chọn và áp dụng rồi không được đề cập lại sau đó.

Một ví dụ cho dạng thuật toán này là thuật toán Kruskal, dùng để tìm cây khung nhỏ nhất trong đồ thị.

Đầu vào: Đồ thị vô hướng G=(V,E,c)G=(V,E,c) với VV là tập hợp các đỉnh, EE là tập hợp các cạnh và cc đo giá trị mỗi cạnh.

Đầu ra: Cây khung ngắn nhất T=(VT,ET)T=(V_T,E_T) xuất phát từ đồ thị GG.

  • VTVV_T\gets V.
  • ETE_T\gets\emptyset.
  • L=(Lk)k=1Ei<j    c(Li)c(Lj)L=(L_k)_{k=1}^{|E|}|_{i<j\implies c(L_i)\leq c(L_j)}
  • Với mọi e:={u,v}Le:=\{u,v\}\in L tiến hành:
    • Nếu chưa tồn tại con đường từ uu sang vv thì:
      • ETET{e}E_T\gets E_T\cup\{e\}.
    • Kết thúc điều kiện
  • Kết thúc vòng lặp

Trong thuật toán nêu trên, các cạnh của đồ thị GG đã được sắp xếp theo thứ tự tăng dần của giá trị.

Giải quyết một chuỗi ô

Phương pháp 1

Vì ở tất cả các cách, có thể tồn tại các ô được tô đen cố định trong chuỗi. Vì vậy, phương pháp nguyên thủy nhất được sử dụng đến chính là "vét cạn" (brute force): Qua mỗi lần giải quyết chuỗi, chúng ta tổng hợp tất cả các cách tô màu để tìm ra các ô được tô cố định.

Ta có đoạn mã giả sau:

Hàm ToMau(L,s,j,l)\text{ToMau}(L,s,j,l) như sau:

  • Nếu l=0l=0 thì:
    • Với mọi i<ji<j tiến hành:
      • Nếu Li=2L_i=2 thì:
        • Trả ra \bot.
      • Kết thúc điều kiện
    • Kết thúc vòng lặp
    • Với mọi i<ji<j tiến hành:
      • Nếu Li=0L_i=0 thì:
        • Li1L_i\gets 1.
      • Kết thúc điều kiện
    • Kết thúc vòng lặp
  • Nếu không:
    • Nếu j<sl1j<s_l-1 thì:
      • Trả ra \bot.
    • Còn nếu j=sl1j=s_l-1 thì:
      • Với mọi i<ji<j tiến hành:
        • Nếu Li=1L_i=1 thì:
          • Trả ra \bot.
        • Kết thúc điều kiện
      • Kết thúc vòng lặp
      • Với mọi i<ji<j tiến hành:
        • Nếu Li=0L_i=0 thì:
          • Li1L_i\gets 1.
        • Kết thúc điều kiện
      • Kết thúc vòng lặp
    • Nếu không:
      • Nếu Lj=1L_j=1 thì:
        • Trả ra ToMau(L,s,j1,l)\text{ToMau}(L,s,j-1,l).
      • Còn nếu Lj=2L_j=2 thì:
        • Nếu Ljsl=2L_{j-s_l}=2 thì:
          • Trả ra \bot.
        • Kết thúc điều kiện
        • Với mọi i{k}k=jsl+1j1i\in\{k\}_{k=j-s_l+1}^{j-1} tiến hành:
          • Nếu Lj=1L_j=1 thì:
            • Trả ra \bot.
          • Kết thúc điều kiện
        • Kết thúc vòng lặp
        • Với mọi i{k}k=jsl+1j1i\in\{k\}_{k=j-s_l+1}^{j-1} tiến hành:
          • Nếu Lj=0L_j=0 thì:
            • Lj1L_j\gets1.
          • Kết thúc điều kiện
        • Kết thúc vòng lặp
        • Trả ra T(L,s,jsl1,l1)T(L,s,j-s_l-1,l-1).
      • Nếu không:
        • L1L0:j+1[Lj1]L^1\gets L_{0:j+1}[L_j\gets1].
        • L2L0:j+1[Lj2]L^2\gets L_{0:j+1}[L_j\gets2].
        • b1ToMau(L1,s,j,l)b_1\gets\text{ToMau}(L^1,s,j,l).
        • b2ToMau(L2,s,j,l)b_2\gets\text{ToMau}(L^2,s,j,l).
        • Nếu ¬b1¬b2\lnot b_1\land\lnot b_2 thì:
          • Trả ra \bot.
        • Còn nếu b1¬b2b_1\land\lnot b_2 thì:
          • Với mọi i{k}k=0ji\in\{k\}_{k=0}^{j} tiến hành:
            • Nếu Li=0Li10L_i=0\land L^1_i\neq0 thì:
              • LiLi1L_i\gets L^1_i.
            • Kết thúc điều kiện
          • Kết thúc vòng lặp
        • Còn nếu b2¬b1b_2\land\lnot b_1 thì:
          • Với mọi i{k}k=0ji\in\{k\}_{k=0}^{j} tiến hành:
            • Nếu Li=0Li10L_i=0\land L^1_i\neq0 thì:
              • LiLi2L_i\gets L^2_i.
            • Kết thúc điều kiện
          • Kết thúc vòng lặp
        • Nếu không:
          • Với mọi i{k}k=0j1i\in\{k\}_{k=0}^{j-1} tiến hành:
            • Nếu Li=0Li10Li1=Li2L_i=0\land L^1_i\neq0\land L^1_i=L^2_i thì:
              • LiLi1L_i\gets L^1_i.
            • Kết thúc điều kiện
          • Kết thúc vòng lặp
        • Kết thúc điều kiện
      • Kết thúc điều kiện
    • Kết thúc điều kiện
  • Kết thúc điều kiện
  • Trả ra \top.

Kết thúc hàm

Nhìn tổng quan, thuật toán này được xây dựng trên nền tảng của hàm T(L,s,j,l)T(L,s,j,l) được đề cập ở đầu bài. Bên cạnh phương pháp xác định tô đúng, tô sai, ở mỗi công đoạn của hàm có thêm giai đoạn truy ngược (backtracking) để bổ sung và hoàn thiện.

Tính chất

Tính chất 1: Thuật toán ToMau(L,s,j,l)\text{ToMau}(L,s,j,l) chỉ tô các ô trống bên trong chuỗi L0:j+1L_{0:j+1}, không đụng vào các ô có sẵn màu.

Chứng minh (Phản chứng):

Giả sử tồn tại ô LiL_i nằm giữa L0L_0LjL_j là ô được chỉnh sửa màu. Gọi LL' là phiên bản cũ sao cho Li0L'_i\neq 0LiLiL'_i\neq L_i.

Nếu Li=1L'_i=1 thì hàm ToMau(L,s,i,l)\text{ToMau}(L',s,i,l) sẽ trực tiếp trả đệ quy ra ToMau(L,s,i1,l)\text{ToMau}(L',s,i-1,l).

Còn nếu Li=2L'_i=2 thì xảy ra 2 trường hợp:

  • LiL'_i là ô cuối chuỗi con. Nếu LiL'_i là ô hợp lệ dựa trên các điều kiện phía trước thì hàm sẽ trả đệ quy về ToMau(L,s,isl1,l1)\text{ToMau}(L',s,i-s_l-1,l-1), nếu không sẽ tự động trả ra \bot và kết thúc chương trình.

  • LiL'_i là ô phía trước chuỗi nhận LkL_k (k>ik>i) là ô cuối. Vòng lặp của ToMau(L,s,k,l)\text{ToMau}(L',s,k,l) sẽ bỏ qua LiL'_i.

Vì vậy, không có cách nào để LiL'_i trở thành LiL_i. Điều này trái với giả thuyết ban đầu.

Tính chất 2: Thuật toán ToMau(L,s,j,l)\text{ToMau}(L,s,j,l) đảm bảo hoàn thiện các ô đen cố định bên trong chuỗi L0:j+1L_{0:j+1} rỗng.

Chứng minh (Phản chứng):

Giả sử tồn tại ô LiL_i nằm giữa L0L_0LjL_j là ô đen cố định nhưng không được tô vào trong chuỗi rỗng. Vì là ô đen nên chắc chắn sẽ chỉ được tô khi l>0l>0.

Nếu LiL_i nhận LjL_j là ô đen cuối chuỗi, LiL_i sẽ được tô đen qua vòng lặp truy ngược của ToMau(L,s,j,l)\text{ToMau}(L,s,j,l).

Nếu LjL_j là ô rỗng, vì là ô đen cố định nên sẽ Li1=Li2=2L^1_i=L^2_i=2, từ đó LiL_i cũng sẽ được tô đen qua vòng lặp truy ngược.

Điều này chứng tỏ giả thiết trên không tồn tại.

Độ phức tạp

Trường hợp 1: Ở các bước cơ sở

Khi tiến hành chạy thuật toán, các bước cơ sở tiến hành các vòng lặp để kiểm tra:

  • Các ô phía trước có hợp lệ chưa.
  • Các ô phía trước có được tô đầy đủ không.

Độ phức tạp của bước cơ sở là c0=O(j)c_0=O(j).

Trường hợp 2: Ở các bước đệ quy

Khi Lj=1L_j=1, thuật toán trả đệ quy thẳng về (j1,l)(j-1,l). Độ phức tạp là cj,Lj=1=1+cj1O(j)c_{j,L_j=1}=1+c_{j-1}\in O(j).

Khi Lj=2L_j=2, thuật toán trả đệ quy về (jsl1,l1)(j-s_l-1,l-1) sau sl|s_l| vòng lặp kiểm tra các ô phía trước. Vậy độ phức tạp là cj,Lj=2=sj+cjsl11+cj1c_{j,L_j=2}=|s_j|+c_{j-s_l-1}\approx1+c_{j-1}.

Khi Lj=0L_j=0, thuật toán sẽ chia hai trường hợp thông qua hai nút đệ quy song song rồi sử dụng vòng lặp truy ngược để tổng hợp. Vậy độ phức tạp là cj=j+cj,Lj=1+cj,Lj=2=j+2cj1O(j2j)c_j=j+c_{j,L_j=1}+c_{j,L_j=2}=j+2c_{j-1}\in O(j2^j)

Độ phức tạp của phương pháp 1 là O(m2m)O(m2^m).

Phương pháp 2

Đối với phương pháp này, chúng ta sẽ sử dụng nguyên bản hàm nhị nguyên TT ở đầu bài viết, thay vì cải biên lại như ở trong phương pháp 1.

Ta có đoạn mã giả như sau:

Hàm ToMau(L,s)\text{ToMau}(L,s) như sau:

  • Với mọi i{k}i=0L1i\in\{k\}_{i=0}^{|L|-1} tiến hành:
    • Nếu Li=0L_i=0 thì:
      • b1T(L[Li1],s,j,l)b_1\gets T(L[L_i\gets1],s,j,l).
      • b2T(L[Li2],s,j,l)b_2\gets T(L[L_i\gets2],s,j,l).
      • Nếu ¬b1¬b2\lnot b_1\land\lnot b_2 thì:
        • Trả ra \bot.
      • Còn nếu b1¬b2b_1\land\lnot b_2 thì:
        • Li1L_i\gets1.
      • Còn nếu b2¬b1b_2\land\lnot b_1 thì:
        • Li2L_i\gets2.
      • Kết thúc điều kiện
    • Nếu không:
      • bT(L,s,j,l)b\gets T(L,s,j,l).
      • Nếu ¬b\lnot b thì:
        • Trả ra \bot.
      • Kết thúc điều kiện
    • Kết thúc điều kiện
  • Kết thúc vòng lặp
  • Trả ra \top.

Kết thúc hàm

Về cơ bản, phương pháp này cũng có tính chất vét cạn: Ở đây thay vì thử hết các khả năng tô màu của cả chuỗi, thì gói gọn lại trong thử hết khả năng tô màu từng ô và kết luận.

Tính chất

Tính chất 1: Thuật toán ToMau(L,s,j,l)\text{ToMau}(L,s,j,l) chỉ tô các ô trống bên trong chuỗi L0:j+1L_{0:j+1}, không đụng vào các ô có sẵn màu.

Chứng minh (Phản chứng):

Giả sử tồn tại ô LiL_i nằm giữa L0L_0LjL_j là ô được chỉnh sửa màu. Gọi LL' là phiên bản cũ sao cho Li0L'_i\neq 0LiLiL'_i\neq L_i.

Nếu LiL'_i hiển thị màu không tô được, thuật toán sẽ ngắt luôn vòng lặp và trả ra \bot.

Nếu LiL'_i hiển thị màu tô được, thì trên lý thuyết LiL_i sẽ không được tô và để trống, vì hai màu đều thỏa mãn điều kiện của chuỗi ô.

Những điều trên chứng tỏ giả thiết không tồn tại.

Tính chất 2: Thuật toán ToMau(L,s,j,l)\text{ToMau}(L,s,j,l) đảm bảo hoàn thiện các ô đen cố định bên trong chuỗi L0:j+1L_{0:j+1} rỗng.

Chứng minh (Phản chứng):

Giả sử tồn tại ô LiL_i nằm giữa L0L_0LjL_j là ô đen cố định nhưng không được tô vào trong chuỗi rỗng. Vì là ô đen nên chắc chắn sẽ chỉ được tô khi l>0l>0.

Nếu LiL_i không được tô, điều này có nghĩa là Li=1L_i=1 cũng hợp lệ.

Nếu Li=1L_i=1 hợp lệ, nghĩa là LiL_i không phải là ô đen cố định. Điều này trái với giả thiết ban đầu.

Độ phức tạp

Độ phức tạp của T(L,s,j,l)T(L,s,j,l):

Ở các trường hợp cơ sở, trong trường hợp tệ nhất, thuật toán vẫn phải thực hiện vòng lặp O(j)O(j) để kiểm tra các ô. Vậy c0,0=O(j)c_{0,0}=O(j).

Ở trường hợp đệ quy, ta thấy các trường hợp ô LjL_j đã tô màu cũng giống với phương pháp 1, trừ việc có backtracking. Độ phức tạp của thuật toán vì thế vẫn là cj,l=1+cj1,lO(j)c_{j,l}=1+c_{j-1,l}\in O(j).

Trường hợp LjL_j chưa được tô màu, độ phức tạp là: cj,l=1+2cj1,lO(2j)c_{j,l}=1+2c_{j-1,l}\in O(2^j).

LL có độ dài mm, nên độ phức tạp là O(2m)O(2^m).

Độ phức tạp của ToMau\text{ToMau}:

Vì hàm sử dụng vòng lặp qua các ô, số vòng lặp là Θ(m)\Theta(m).

Nhân số vòng lặp với số lần đệ quy, ta có: Θ(m).2.O(2m)=O(m2m)\Theta(m).2.O(2^m)=O(m2^m).

Độ phức tạp của phương pháp 2 là O(m2m)O(m2^m).

So sánh hai phương pháp trên

Hai phương pháp trên đều có độ phức tạp là Om2m)Om2^m), giải quyết được vấn đề tô chuỗi ô thông qua thỏa mãn 2 tính chất: Tô các ô trống trên cơ sở tôn trọng sự tồn tại các màu có sẵn, và đảm bảo tô các ô đen cố định khi chuỗi rỗng.

Tuy nhiên chúng ta sẽ đi sâu vào phân tích các "vấn đề con" ở hai phương pháp.

Ở phương pháp 1, thuật toán tô màu trực tiếp trên các ô đan xen với việc kiểm tra tính hợp lệ của màu sắc. Khi nút đệ quy thực hiện trên ô rỗng, thuật toán chia ra làm hai chuỗi phụ độc lập với nhau, sau đó truy ngược để tổng hợp các nút giống nhau giữa các chuỗi phụ.

Ở phương pháp 2, thuật toán giải quyết từng ô trong chuỗi thông qua phép lặp và thử. Ở mỗi ô LjL_j, khi ô có màu, thuật toán chỉ thuần túy kiểm tra lại tính hợp lệ của toàn chuỗi, còn ở ô rỗng thì sẽ lại chia ra làm các chuỗi phụ để thử nghiệm. Đặc điểm của các chuỗi phụ này là có chung dữ liệu của chuỗi con L0:jL_{0:j}.

Vì vậy, ta nhận định: Phương pháp 2 chia thành các bài toán con không độc lập với nhau về dữ liệu. Đây chính là tiền đề để ta áp dụng quy hoạch động vào giải chuỗi.

Phương pháp tối ưu

Áp dụng quy hoạch động

Chúng ta sẽ áp dụng phương pháp top-down memoization lên trên phương pháp 2.

Trên cơ sở hàm đệ quy T(L,s,j,l)T(L,s,j,l), chúng ta sử dụng một bảng bộ nhớ hai chiều để lưu trữ bộ nhớ các kết quả T(L,s,j,l)T(L,s,j,l) mỗi khi trả ra kết quả sai. Gọi memory=(mj,l)j=0,l=0\text{memory}=(m_{j,l})_{j=0,l=0} là bộ nhớ hai chiều, được khởi tạo với mọi mj,l=m_{j,l}=\top.

Ở mỗi lần tiến hành T(L,s,j,l)T(L,s,j,l), khi T(L,s,j,l)T(L,s,j,l) trả ra sai, memory\text{memory} sẽ ghi nhận mj,lm_{j,l}\gets\bot.

Ở mỗi nút đệ quy của T(L,s,j,l)T(L,s,j,l), điều kiện memory=\text{memory}=\bot sẽ khống chế việc thuật toán đi xa hơn, bởi vậy độ phức tạp của các bài toán con xoay quanh ở các vòng lặp: O(j)O(j)

Độ phức tạp của phương pháp tối ưu là O(m2)O(m^2).

Một số cải tiến khác

Cải tiến để một số trường hợp hàm ToMau\text{ToMau} chạy ở độ phức tạp O(m)O(m): Chúng ta sẽ thêm những ràng buộc ở các trường hợp s=0|s|=0γ=L\gamma=|L|.

  • Nếu |s|=0 thì:
    • Với mọi LiLL_i\in L tiến hành:
      • Li1L_i\gets 1.
    • Kết thúc vòng lặp
    • Trả ra \top.
  • Còn nếu γ=L\gamma=|L| thì:
    • l=1l=1.
    • done0\text{done}\gets0.
    • Với mọi LiLL_i\in L tiến hành:
      • Nếu done=sl\text{done}=|s_l| thì:
        • ll+1l\gets l+1.
        • done0\text{done}\gets0.
        • Li1L_i\gets1.
      • Nếu không:
        • donedone+1\text{done}\gets\text{done+1}.
        • Li2L_i\gets2.
      • Kết thúc điều kiện
    • Kết thúc vòng lặp
  • Kết thúc điều kiện

Cải tiến để một số trường hợp hàm ToMau\text{ToMau} chạy ở độ phức tạp O(logm)O(\log m): Đối với chuỗi ô rỗng toàn phần, chúng ta tính toán số ô trắng tự do nn, sau đó lặp qua ss để tìm ra các chỉ mục của những ô tô đen cố định thông qua công thức toán nêu trên. Sau đó, chúng ta lặp qua danh sách chỉ mục trên để tô đen các ô, thay vì gọi các hàm đệ quy ban đầu.

Giải quyết toàn bộ khung

Áp dụng phương pháp giải chuỗi ô

Ta đến với mã giả sau đây:

Đầu vào: Ma trận rỗng M\mathcal M, danh sách dãy số hàng Sv\mathcal S_v, danh sách dãy số cột Sh\mathcal S_h.

  • Khởi tạo L\mathscr L là tập hợp các chỉ mục hàng.
  • Khởi tạo C\mathscr C là tập hợp các chỉ mục cột.
  • Khi LC\mathscr L\neq\emptyset\land\mathscr C\neq\emptyset tiến hành:
    • Với mọi iLi\in\mathscr L tiến hành:
      • Tô màu hàng ii của ma trận M\mathcal M dựa trên dãy số Sv[i]\mathcal S_v[i].
      • Nếu hàm tô màu trả ra \bot thì:
        • Trả ra ,()\bot,(). //()() là ma trận rỗng
      • Kết thúc điều kiện
      • Thêm chỉ mục các ô trong hàng được tô mới vào tập hợp C\mathscr C.
      • Loại bỏ ii ra khỏi tập hợp L\mathscr L.
    • Kết thúc vòng lặp
    • Với mọi jCj\in\mathscr C tiến hành:
      • Tô màu hàng jj của ma trận M\mathcal M dựa trên dãy số Sh[j]\mathcal S_h[j].
      • Nếu hàm tô màu trả ra \bot thì:
        • Trả ra ,()\bot,().
      • Kết thúc điều kiện
      • Thêm chỉ mục các ô trong cột được tô mới vào tập hợp L\mathscr L.
      • Loại bỏ jj ra khỏi tập hợp C\mathscr C.
    • Kết thúc vòng lặp
  • Kết thúc vòng lặp
  • Nếu tồn tại một ô trong ma trận chưa được tô thì:
    • Trả ra ,M\varnothing,\mathcal M.
  • Kết thúc điều kiện
  • Trả ra ,M\top,\mathcal M.

Bước giải quyết khung trên đây áp dụng phương pháp bottom-up memoization: Các hàng và cột được giải lần lượt từ thấp lên cao, cho đến khi không còn hàng và cột nào có thể giải quyết thêm nữa.

Gọi (l,c)(\mathscr l,\mathscr c) là kích thước hàng × cột của ma trận M\mathcal M.

Đặt m=max(l,c)m=\max(\mathscr l,\mathscr c)

Vậy độ phức tạp của thuật toán, trên cơ sở áp dụng hàm tô màu tối ưu, là Θ(l3+c3)\Theta(\mathscr l^3+\mathscr c^3) hoặc Θ(m3)\Theta(m^3).

Áp dụng giải thuật tham lam

Khi chủ thể giải quyết bài toán là máy tính, các chỉ mục được thực hiện theo thứ tự từ nhỏ đến lớn. Tuy nhiên, người chơi thực tế không giải quyết bài toán như vậy. Họ có khuynh hướng chọn lựa hàng và cột "dễ giải quyết" để xử lý trước.

Vì vậy, chúng ta sẽ áp dụng giải thuật tham lam như sau, để phương pháp giải gắn với bản năng của con người hơn.

Một chuỗi ô ra sao được gọi là "dễ giải"? Qua toàn bộ những gì chúng ta phân tích từ đầu bài, có hai giá trị quyết định đến độ khó của chuỗi ô:

  • Số ô trống trong chuỗi
  • Giá trị n=Lγn=|L|-\gamma: tương ứng với số ô trắng tự do trong chuỗi.

Bởi thế, ta đề xuất cách chấm điểm chuỗi ô như sau:

scoreL=min({iLiLLi=0},Lγ)\text{score}_L=\min(|\{i|L_i\in L\land L_i=0\}|,|L|-\gamma)

Ở mỗi vòng lặp trong bước giải khung, chúng ta sắp xếp thứ tự các chỉ mục trong L\mathscr LC\mathscr C theo thứ tự tăng dần của giá trị score\text{score}, sau đó tiến hành tô màu các chuỗi ô theo thứ tự trên.

Câu hỏi: Giải thuật tham lam có tối ưu hóa việc giải quyết trò chơi không?

Đáp án: Trên lý thuyết, việc sắp xếp lại các chỉ mục theo thứ tự có độ phức tạp là O(m)O(m). Hơn nữa, điều này thực hiện trước khi mỗi vòng lặp xảy ra, và không làm giảm đi số vòng lặp bắt buộc. Vì vậy thuật toán vẫn là Θ(m3)\Theta(m^3) và không tối ưu được phép giải trên máy tính.

Vét cạn

11_inc

Chúng ta có mẫu puzzle như trong hình. Khi chúng ta áp dụng thuần túy phép giải khung như trên, thứ tự tiến hành sẽ là:

  • Ở bước giải các hàng:
    • Hàng 1 có C2+2121=C31=3C_{2+2-1}^{2-1}=C_3^1=3 cách xếp, có 22 ô trắng tự do tương ứng với dãy số. Do 121\leq 2 nên không có ô đen cố định. Hàng 1 bị để trống.
    • Hàng 2 có C1+3131=C32=3C_{1+3-1}^{3-1}=C_3^2=3 cách xếp, có 11 ô trắng tự do tương ứng với dãy số. Do 111\leq 1 nên không có ô đen cố định. Hàng 2 bị để trống.
    • L\mathscr L trở thành tập rỗng.
  • Ở bước giải các cột:
    • Cột 1 có C1+2121=C21=2C_{1+2-1}^{2-1}=C_2^1=2 cách xếp, có 11 ô trắng tự do tương ứng với dãy số. Do 111\leq 1 nên không có ô đen cố định. Cột 1 bị để trống và tương tự với 3 cột còn lại.
    • C\mathscr C trở thành tập rỗng. Do không có ô nào được tô nên L\mathscr L vẫn là tập rỗng.
  • Thuật toán kết thúc và trả ra \varnothing với ma trận để trống toàn tập.

Để giải quyết vấn đề trên, chúng ta sẽ thay thế phần trả ra \varnothing bằng việc trả ra dòng mã giả dưới đây:

  • Gọi mijm_{ij} một ô trống bất kỳ trong ma trận dang dở.
  • Tô màu bằng phương pháp giải khung ở trên khi mij1m_{ij}\gets1. (Khởi tạo {L{i}C{j}\begin{cases}\mathscr L\gets\{i\}\\\mathscr C\gets\{j\}\end{cases}).
  • Tô màu bằng phương pháp giải khung ở trên khi mij2m_{ij}\gets2. (Khởi tạo {L{i}C{j}\begin{cases}\mathscr L\gets\{i\}\\\mathscr C\gets\{j\}\end{cases}).
  • Ở mỗi hàm tô màu, đệ quy cho đến khi ma trận giải hết hoặc trả ra \bot.

Chúng ta kết thúc với bức hình:

11

Độ phức tạp của thuật toán vét cạn là O(2m3)O(2^{m^3})

Kết luận về cách giải ở người chơi

Từ việc phân tích thao tác của máy tính trong việc giải Nonogram puzzle thông qua các giải thuật khác nhau, ta đi đến kết luận về phương pháp giải của con người:

  1. Ở ma trận rỗng, người chơi chọn các hàng và cột có độ khó thấp nhất để giải trước.
  2. Người chơi lấp tối đa khung hình bằng các ô tô đen cố định và các ô trắng (nếu có).
  3. Khi ma trận được lấp một phần, xu hướng chọn hàng và cột thiên về hàng và cột có ít ô chưa tô màu nhất.
  4. Khi không thể giải bằng logic nữa, xu hướng vét cạn sẽ được áp dụng với các ô rỗng.

Tài liệu tham khảo

Về Nonogram: Đồ án học phần LU3IN003 - Algorithmique II niên khóa 2023-2024 của trường Đại học Sorbonne, Paris

Về các giải thuật:

  1. Tài liệu học học phần UL3IN003 - Algorithmique II của trường Đại học Sorbonne, Paris bởi các Phó Giáo sư Fanny P. và Olivier S.
  2. manhhomienbienthuy - Quy hoạch động - một thuật toán thần thánh
  3. Phong Tr. - Thuật toán tham lam - Greedy Algorithm

Về phần Toán học: Nỗ lực cá nhân có sự hỗ trợ của Tiến sĩ Manuel A., ChatGPT và Wikipedia


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí