1 điểm bởi GN⁺ 2024-10-07 | 1 bình luận | Chia sẻ qua WhatsApp
  • sudoku của Dyalog APL trả về tất cả các ma trận lời giải có thể có từ ma trận câu đố trong đó ô trống được đặt là 0, đồng thời triển khai cùng một bài toán theo nhiều cách theo phong cách APL/K
  • Đối tượng mặc định là Sudoku 9×9, trong đó mỗi khối 3×3, hàng và cột phải chứa các số từ 1 đến 9 không lặp lại
  • Đầu vào prob chứa 1-9 ở các ô đã điền và 0 ở ô trống; có thể chỉ định cả khối không vuông như 2×3, 3×4 bằng đối số trái tùy chọn shape
  • Thuật toán giải của Veli-Matti Jantunen vector hóa ma trận, tạo chỉ mục hàng/cột/khối, rồi giảm dần các ứng viên và mở rộng từ nhóm bị ràng buộc mạnh nhất
  • Các ví dụ s33s22 mỗi ví dụ có 3 lời giải, 3 4 sudoku s34 có 2 lời giải; bài viết cũng giới thiệu one-liner K 5 của Arthur Whitney cùng nhiều bản tái triển khai bằng APL

Đầu vào Sudoku và kết quả của hàm sudoku

  • Câu đố Sudoku là một lưới gồm các khối 3×3 được bố trí thành 3×3, và mỗi ô hoặc để trống hoặc có một số từ 1 đến 9
  • Lời giải phải thỏa mãn cả ba điều kiện không trùng lặp
    • Mỗi khối 3×3 chứa các số từ 1 đến 9 không lặp lại
    • Mỗi hàng 9 ô chứa các số từ 1 đến 9 không lặp lại
    • Mỗi cột 9 ô chứa các số từ 1 đến 9 không lặp lại
  • Ma trận prob dùng các số 1-9 cho ô đã điền và 0 cho ô trống
  • Đối số trái tùy chọn shape chỉ định hình dạng khối của câu đố không phải dạng vuông mặc định
    • Với ma trận 6×6 có vùng con là 2×3, gọi theo dạng 2 3 sudoku mat
  • Kết quả là một vector chứa tất cả các ma trận lời giải
    • Nếu không có lời giải, trả về
    • Tình huống lỗi có thể được biểu thị bằng ''; tài liệu ghi là “không nên xảy ra, nhưng khi số kết quả cực kỳ lớn”

Luồng giải của Veli-Matti Jantunen

  • Thuật toán xử lý ma trận Sudoku như một vector, và biểu diễn hàng, cột, vùng Sudoku lần lượt bằng vector chỉ mục
  • Sau khi vượt qua các kiểm tra cơ bản, thuật toán kiểm tra từng phương án trong danh sách ứng viên
  • Ở mỗi bước, thuật toán lọc các phần tử có thể có của mọi ô
    • Nếu có dù chỉ một ô không có giá trị khả dĩ nào, ứng viên lời giải đó bị loại
    • Nếu một ô có từ hai số ứng viên trở lên, thuật toán chọn ô trong nhóm bị ràng buộc mạnh nhất và thêm các tổ hợp ứng viên của ô đó vào danh sách
    • Nếu mỗi ô chỉ còn lại đúng một số, nó được xử lý như một lời giải rồi chuyển sang ứng viên tiếp theo
  • Cùng phần này cũng bao gồm hàm Shuffle, dùng để xáo một bảng Sudoku hiện có thành một bảng khác

One-liner của Arthur Whitney và các triển khai thay thế

  • Triển khai sudoku thay thế của David Crossley nhận cấu hình N×N làm đầu vào, dành cho trường hợp kích thước khối N*÷2 là số nguyên
    • Đầu vào phải là một bố cục hợp lệ, trong đó một số ô có các số từ 1 đến N và các ô còn lại là 0
    • Mỗi hàng, cột và khối phải chứa tất cả các số từ 1 đến N trong kết quả
    • Bên trong triển khai có các hàm phụ trợ như valid, search, rules, sole, singles, uniques, matches, NinN, setup
  • Lời giải K 5 của Arthur Whitney được trình bày dưới dạng một dòng mã
x(,/{@[x;y;]'(!10)^x*|/p[;y]=p,:,3/:-3!p:!9 9}')/&~* x
  • Phil Last cung cấp triển khai sudoku chuyển mã của Whitney sang D-function
  • Bản viết lại của Morten Kromberg định nghĩa tường minh một số thành phần của K, có hình thức gần với bản gốc hơn
    • Giống phiên bản K, nó nhận và trả về vector 81 phần tử thay vì ma trận
  • Triển khai Sudoku của Roger Hui có dạng tổng quát hơn, xử lý được cả câu đố không vuông
    • svec tạo vector lời giải, còn pvexpvec triển khai các bố cục khả dĩ
    • avl tạo danh sách các số khả dĩ, còn emt tìm chỉ mục hàng/cột của các ô trống
    • rcb, box, cmap, CMAP cấu thành quan hệ xung đột giữa hàng/cột/khối

Câu đố ví dụ và số lượng lời giải

  • s33 là bài mẫu 9×9, và kết quả của sudoku s333 lời giải
  • Hàm sbox chia các khối bên trong để hiển thị lưới Sudoku dễ đọc hơn
    • 0 được hiển thị bằng dấu chấm (·)
    • Được xuất ra dưới dạng ma trận ký tự có vẽ ranh giới khối
  • s22 là bài mẫu 4×4, và kết quả của sbox¨ sudoku s223 lời giải
  • s34 là bài mẫu dùng khối 3×4
    • Hiển thị bài toán theo dạng phân tách khối bằng 3 4 sbox s34
    • Kết quả của 3 4 sudoku s342 lời giải

Liên kết tham khảo và mục nên xem cùng

  • sudoku_bfs được liên kết như một ví dụ minh họa thuật toán này
  • Phần “Learn” của TryAPL có demo theo từng bước: http://www.TryAPL.org
  • Có video minh họa hành vi khi chạy: http://www.youtube.com/watch?v=DmT80OseAGs
  • Các mục nên xem cùng được nêu là queens, sudoku_bfs, X, sudokuX

1 bình luận

 
GN⁺ 2024-10-07
Các ý kiến trên Hacker News
  • Dòng đó được viết bằng K. K là ngôn ngữ do Arthur Whitney tạo ra dựa trên APL và Scheme
    x(,/{@[x;y;]'(!10)^x*|/p[;y]=p,:,3/:-3!p:!9 9}')/&~*x

  • Thỉnh thoảng tôi ước lượng độ phức tạp của mã bằng cách so sánh số dòng mã với kết quả đầu ra bên dưới
    tar -cf - . | gzip | base64 | wc -l
    Nói cách khác là xem “nó nén tốt đến mức nào?”. Nhìn APL làm tôi nhớ đến lúc lỡ gửi đầu ra gzip ra terminal
    p←{(↑⍵)∘{(⍺∨.=⍵)/⍳n×n∘}¨,⍵},(n*÷2){⍵,⍺⊥⌊⍵÷⍺}'⍳n n←⍴⍵
    Thật ấn tượng khi có người còn lần theo loại mã này và hỏi “có tìm được bug không?”. Nó có cảm giác như dữ liệu nhị phân đã nén mà mọi người đều đã có cùng một cuốn từ điển

    • Tôi thật sự tò mò các lập trình viên APL nghĩ thế nào về khả năng bảo trì và tính dễ đọc. Không biết họ có chú thích mã cực kỳ kỹ lưỡng hoặc viết tài liệu riêng không
    • Nếu hỏi “có tìm được bug không?” thì tôi thấy ngay vài cái. Có các lỗi cú pháp như dấu nháy đơn chưa đóng và không có toán hạng bên phải; còn n n←⍴⍵ trông như một tín hiệu rằng nó đặt n hai lần và kỳ vọng là hai chiều, nhưng tùy ý định thì _ n←⍴⍵ hoặc n←⊃⌽⍴⍵ sẽ tự nhiên hơn
      Ngoài ra sẽ lỗi nếu ⍴⍵ không phải một số nguyên đơn lẻ hoặc vector rỗng, nên rốt cuộc chẳng khác gì n←⍴⍵ và chỉ khiến khó hiểu hơn. Nhiều dấu , trùng lặp và ↑⍵ cũng có thể loại bỏ, và toàn bộ biểu thức thực ra gần như trở thành p←(n+1)⍴⊂⍳n×n←⍴⍵, tức là một cấu trúc trả ra n+1 vector 1..n²
      Nhìn bề ngoài thì kỳ lạ, nhưng nếu học các ký hiệu và phép toán cơ bản, APL lại thẳng thắn đến bất ngờ. Chỉ là để thành thạo thì cần thời gian, và khi đạt đến điểm đó nó có cảm giác như siêu năng lực
    • Nghĩ đến chuyện có hàng tỷ người đọc và viết các ký tự không phải tiếng Anh, tôi không chắc việc có người đọc được APL có gì đặc biệt hay đáng ngạc nhiên hơn không
  • Nói rằng những người ủng hộ ngôn ngữ này nhấn mạnh tốc độ, khả năng xử lý mảng dễ dàng và cú pháp giàu sức biểu đạt là đúng
    https://en.m.wikipedia.org/wiki/K_(programming_language)

    • Tuy nhiên tôi không chắc khả năng bảo trì có phải cũng là ưu điểm hay không
  • Số dòng mã là một chỉ số không tốt vì mỗi ngôn ngữ có cách dùng dòng khác nhau
    Một thước đo tốt hơn có thể là đếm số nút trong cây cú pháp theo các ký hiệu không kết thúc có ý nghĩa như “hằng số” hay “lời gọi hàm”. Xa hơn nữa, nếu tính cả độ sâu và hệ số phân nhánh của cây đó thì càng tốt

    • Tôi khó đồng ý với kiểu nói rằng chỉ ngữ nghĩa mới quan trọng. Trải nghiệm người dùng của ngôn ngữ, độ rõ ràng, lối tư duy và sức biểu đạt cũng quan trọng, và kích thước trực quan của mã có ảnh hưởng đến những điều này
      Lời giải một dòng chiếm rất ít không gian màn hình, nên là lợi thế lớn khi xử lý vấn đề phức tạp. Việc di chuyển mắt trong màn hình nhẹ nhàng hơn nhiều so với cuộn qua lại giữa các file, và tải nhận thức là điều quan trọng
      Ngay cả khi không biết K, việc các hằng số hiện ra cạnh nhau trông như đang dùng biểu diễn dữ liệu trực tiếp của bài toán. Nếu văn hóa K khuyến khích loại mã như vậy và kéo tư duy về phía trực tiếp, đơn giản, tôi sẽ muốn đưa loại nước sốt đặc chế đó vào nhóm
    • Các hàm tích hợp và API thư viện hệ thống làm hỏng thước đo kiểu này. Ví dụ HQ9+ làm khá tốt riêng việc in “Hello, world!”
      https://cliffle.com/esoterica/hq9plus/
    • Thước đo ưa dùng để đo lượng thông tin, như trong lý thuyết thông tin thuật toán, đơn giản là số bit
      https://en.wikipedia.org/wiki/Algorithmic_information_theory
    • Đoạn mã một dòng này rõ ràng được viết cho vui, và không ai có lý trí lại khẳng định đó là mã dễ đọc. Tranh luận định nghĩa ở đây là lạc trọng tâm. Điểm chính là “với K, có thể viết mã cực kỳ cô đặc
  • Tôi thường tự hỏi liệu khi dùng các ngôn ngữ như APL/K, lập trình viên có thật sự có thể suy nghĩ về vấn đề hiệu quả hơn không

    • Với tư cách một lập trình viên kdb+/Q, tôi cho rằng điều đó tùy vào loại vấn đề. Khi xử lý mảng dữ liệu, rõ ràng dễ hơn khi nghĩ và viết việc cộng hai mảng rồi lấy trung bình là avg a+b
      Nếu là ngôn ngữ không lấy mảng làm trung tâm, rất có thể sẽ cần kiểm tra biên, một vòng lặp for lớn, các biến tạm để chứa tổng và số lượng, v.v. Một việc trong ngôn ngữ như C mất khoảng 6 dòng thì trong Q kết thúc bằng 6 ký tự
      Tuy vậy mọi ngôn ngữ đều có tính năng giúp suy luận tốt hơn về một số vấn đề nhất định. Các ngôn ngữ hàm có kiểu dữ liệu đại số và pattern matching, chẳng hạn OCaml hay F#, tốt hơn một switch hoặc if-else-if lớn; còn ngôn ngữ có cú pháp đường như async/await thì có lợi cho xử lý đồng thời
    • Với nhóm bài toán dễ vector hóa, ngôn ngữ lấy mảng làm trung tâm giúp tư duy và lời giải hiệu quả hơn, vì có thể trừu tượng hóa cấu trúc dữ liệu và chi tiết lặp
      Khi làm quant, tôi dùng kdb+/q rất nhiều trong hơn 5 năm cho các chiến lược tần suất trung bình, nhưng khi chuyển sang giao dịch tần suất cao, nơi các phép tính sổ lệnh không dễ hoặc không hiệu quả để vector hóa, việc tiếp tục dùng ngôn ngữ lấy mảng làm trung tâm lại khiến việc suy luận về vấn đề phức tạp hơn
    • Tôi từng nghe trong một bài trình bày về Dyalog, một ngôn ngữ hiện đại thuộc họ APL, lập luận rằng ký pháp như vậy giúp nhận ra một số idiom dễ hơn
      https://youtu.be/PlM9BXfu7UY?si=ORtwI1qmfmzhJGZX&t=3598
      Phần đó nằm trong ngữ cảnh trình biên dịch, nhưng toàn bộ bài nói xem Dyalog và APL như một hệ ký pháp toán học. Mạch chính là việc tối ưu hóa biểu thức toán học có thể dễ hơn tối ưu hóa mã thông thường
    • Hillel Wayne thỉnh thoảng bàn về chủ đề này trong newsletter. Tôi đã bị thuyết phục rằng ông ấy thật sự suy nghĩ tốt hơn về một số vấn đề trong ngôn ngữ mảng, nhưng vẫn chưa hình dung được trải nghiệm đó có cảm giác thế nào
    • Điểm hay của phong cách ngôn ngữ mảng là khi thảo luận các biến thể thuật toán, đoạn mã liên quan chỉ dài vài ký tự nên có thể chèn thẳng vào phần thân bài. Với các ngôn ngữ dọc truyền thống, nơi cần nhiều dòng hoặc hàng chục dòng để nói cùng nội dung, ta phải liên tục trộn các khối mã với phần giải thích
  • Một trong những điểm quan trọng nhất ở đây là bộ sinh bài toán ở phía trên rất rõ ràng. Đây chính là khác biệt giữa các ngôn ngữ ký hiệu kiểu Iverson, bao gồm J và K, với các ngôn ngữ khác
    Nó không có sự tao nhã và sức mạnh của lời giải một dòng, nhưng rất gọn gàng và dễ hiểu ngay cả khi không có chú thích nghiêm ngặt. Tuy vậy, tôi nghĩ lamp không phải là một ký hiệu chú thích hay
    Lời giải một dòng thật đáng kinh ngạc, và lập trình ngầm định hay đến mức làm xoắn cả não. Ý tưởng dùng khả năng nén độc đáo của các ngôn ngữ dựa trên glyph để mô tả và thực hiện lập trình hàm, rồi lại áp dụng nó cho toàn bộ mảng, đúng là thiên tài
    https://www.jsoftware.com/papers/fork.htm

    • Việc có thể viết mọi thứ trên một dòng không có khoảng trắng không có nghĩa là nhất thiết nên làm vậy
      Tất nhiên, nếu loại bỏ khả năng đó thì có thể buộc người ta viết mã dài dòng hơn, nhưng như thế sẽ làm giảm đáng kể thế mạnh của nó với tư cách một công cụ tương tác. Các ngôn ngữ kiểu Iverson hữu ích cho công việc tương tác vì có thể viết mã rất ngắn. Khi đó mã thậm chí còn không được lưu lại, nên đúng nghĩa là mã write-only
      Khi viết mã để đưa vào file, bạn có thể chọn phong cách mình muốn, và khi đó tôi khuyên nên viết ít nén hơn. Dù vậy, ngay cả khi viết theo phong cách dài dòng, các ngôn ngữ kiểu Iverson vẫn cho mã ngắn hơn nhiều so với hầu hết ngôn ngữ khác
  • Phần lớn mọi người ngại vì các ký hiệu, nhưng vấn đề của tôi không phải vậy
    Tôi thích APL và các ngôn ngữ mảng, và những gì học được đã giúp ích rất nhiều cả khi dùng các ngôn ngữ khác. Nhưng chúng không trở thành công cụ hằng ngày của tôi; không phải vì ký hiệu, mà vì sau khoảng 3–4 năm thỉnh thoảng động đến, tôi đã đụng phải một bức tường không vượt qua được
    Ở các ngôn ngữ khác thường có một cách tiếp cận chung để ít nhất cũng giải quyết vấn đề một cách tạm ổn, rồi sau này nếu tìm ra “bí quyết” của vấn đề đó thì có thể sửa lại cho tao nhã và hiệu quả hơn. APL thì cảm giác như không có lối vòng tạm thời như vậy: hoặc bạn biết bí quyết, hoặc không biết
    Tôi không rõ thực tế có đúng như vậy không, hay nếu học đủ nhiều bí quyết thì trực giác giải quyết vấn đề sẽ hình thành, hay rốt cuộc vẫn chỉ toàn là bí quyết, hoặc đơn giản là tôi chưa đọc tài liệu chiến lược cốt lõi nào đó

    • Cảm giác đó không sai. Khi học các ngôn ngữ mảng, rất dễ có ấn tượng như vậy. Người dùng lâu năm dễ nhìn một bài toán rồi nói “sao lại giải phức tạp thế, chỉ cần dùng ⍸⍣¯1 là được mà?”, trong khi rất có thể chưa từng có ai nói với bạn rằng có phép toán nghịch đảo và cách dùng nó
      Đến nay tôi đã dùng những ngôn ngữ này nhiều năm, nhưng những bức tường mã mà một số lập trình viên mảng tạo ra vẫn hơi gây ngợp. Tôi hiểu vì sao họ viết như vậy, nhưng cá nhân tôi thích mã có một chút khoảng trắng hơn
      Tôi đang làm một ngôn ngữ mảng dựa trên APL, và một trong các mục tiêu ban đầu là biến phong cách mệnh lệnh thành công dân hạng nhất mà không trừng phạt người mới dùng những thứ như câu lệnh if. Tôi xem phong cách này nằm khoảng giữa kiểu APL thuần túy và các ngôn ngữ mệnh lệnh thông thường
      https://codeberg.org/loke/array/src/branch/master/array/standard-lib/output3.kap
    • Bức tường bạn nói đến là một vấn đề thực sự trong lộ trình nhập môn APL hiện nay. Năm ngoái tôi cũng đã thuyết trình đúng về chủ đề này, và chắc chắn đó không phải lỗi cá nhân
      Nhưng đó cũng không phải giới hạn của bản thân ngôn ngữ. Theo kinh nghiệm của tôi, quá trình xuyên qua bức tường đó chính là lúc mô hình tư duy bắt đầu khớp vào nhau. Chỉ sau khi dành khoảng 500 giờ hack một nguyên mẫu parser YAML trong một năm, các mảnh ghép mới bắt đầu ăn khớp
      Cốt lõi có vẻ là sự kết hợp giữa các nguyên tắc thiết kế hướng dữ liệu, cách tận dụng cụ thể các đặc tính kiểu Iverson của ký pháp tốt trong kiến trúc phần mềm, và việc làm quen với các idiom cùng cách chúng biểu đạt khái niệm miền
      https://dyalog.tv/Dyalog23/?v=J4cg6SV92C4
      https://www.jsoftware.com/papers/tot.htm
  • Có một video về chủ đề này
    https://www.youtube.com/watch?v=DmT80OseAGs
    Bạn có thể tự thử lời giải tại https://tryapl.org/

  • Có thể sẽ thú vị nếu so sánh một dòng này với các lời giải code golf trong nhiều ngôn ngữ lập trình
    https://codegolf.stackexchange.com/questions/tagged/sudoku?tab=Votes

    • Thú vị là lời giải đứng đầu cho một bài cụ thể, tức bộ giải Sudoku bằng vét cạn, lại chính là một đoạn K. Vị trí thứ hai là lời giải J được làm theo lời giải K
      https://codegolf.stackexchange.com/a/5030