- Dyalog APL の
sudoku は、空欄を 0 としたパズル行列から 可能なすべての解答行列 を返し、同じ問題を APL/K スタイルで複数の方法で実装している
- 基本対象は 9×9 の Sudoku で、各 3×3 ボックス、行、列には 1 から 9 までの数字が重複なく入らなければならない
- 入力
prob は埋まっているマスに 1-9、空欄に 0 を入れ、任意の左引数 shape によって 2×3、3×4 のような 非正方形ボックス も指定できる
- Veli-Matti Jantunen の解法アルゴリズムは、行列をベクトル化して行・列・ボックスのインデックスを作成し、候補を絞り込みながら 最も制約の強いグループ から展開していく
- 例の
s33 と s22 はそれぞれ 3 個の解答を持ち、3 4 sudoku s34 は 2 個の解答を持つ。Arthur Whitney の K 5 ワンライナーと複数の APL 再実装もあわせて紹介されている
Sudoku の入力と sudoku 関数の結果
- Sudoku パズルは、3×3 ボックスが 3×3 に配置されたグリッドで、各マスは空であるか 1 から 9 までの数字を持つ
- 解答は 3 つの重複禁止条件をすべて満たさなければならない
- 各 3×3 ボックス に 1 から 9 までの数字が重複なく入る
- 各 9 マスの行 に 1 から 9 までの数字が重複なく入る
- 各 9 マスの列 に 1 から 9 までの数字が重複なく入る
prob 行列は、埋まっているマスに 1-9、空欄に 0 を使う
- 任意の左引数
shape は、標準の正方形ではないパズルの ボックス形状 を指定する
- 6×6 行列で部分領域が 2×3 なら、
2 3 sudoku mat の形で呼び出す
- 結果は、すべての解答行列を含むベクトル
- 解答がなければ
⍬ を返す
- エラー状況は
'' で示されることがあり、文書では「発生すべきではないが、結果が非常に多いとき」とされている
Veli-Matti Jantunen の解法の流れ
- アルゴリズムは Sudoku 行列を ベクトル として扱い、行・列・Sudoku 領域をそれぞれ インデックスベクトル で表す
- 基本検査を通過した後、候補リストから代替案を 1 つずつ調べる
- 各段階では、すべてのマスの可能な要素を絞り込む
- 可能な値がないマスが 1 つでもあれば、その解答候補は除外される
- 1 マスに候補数字が 2 つ以上ある場合は、最も制約の強いグループからマスを選び、そのマスの候補の組み合わせをリストに追加する
- すべてのマスに数字が 1 つずつしか残らなければ、解答として処理して次の候補へ進む
- 同じセクションには、既存の Sudoku 表を別の表にシャッフルする
Shuffle 関数も含まれている
Arthur Whitney のワンライナーと代替実装群
- David Crossley の代替
sudoku 実装は N×N 設定を入力として受け取り、ボックスサイズ N*÷2 が整数になる場合を対象としている
- 入力は、一部のマスに 1 から
N までの数字が入り、残りが 0 の有効な配置でなければならない
- 各行、列、ボックスは、結果において 1 から
N までのすべての数字を含まなければならない
- 実装内部には
valid, search, rules, sole, singles, uniques, matches, NinN, setup などの補助関数がある
- Arthur Whitney の K 5 による解法は 1 行のコード として示されている
x(,/{@[x;y;]'(!10)^x*|/p[;y]=p,:,3/:-3!p:!9 9}')/&~* x
- Phil Last は、Whitney のコードを D-function に移した
sudoku 実装を提供している
- Morten Kromberg の書き直しは、K の一部の構成要素を明示的に定義して、原文により近い形を取っている
- K 版と同様に、行列ではなく 81 要素のベクトル を入力・出力する
- Roger Hui の
Sudoku 実装はより一般化された形で、非正方形パズルも扱う
svec は解答ベクトルを作り、pvex と pvec は可能な配置を展開する
avl は可能な数字の一覧を作り、emt は空欄の行・列インデックスを見つける
rcb, box, cmap, CMAP は行・列・ボックスの衝突関係を構成する
サンプルパズルと解答数
s33 は 9×9 のサンプル問題で、sudoku s33 の結果は 3 個の解答 を持つ
sbox 関数は内部ボックスを分割して Sudoku グリッドを読みやすく表示する
- 0 は中点 (
·) として表示される
- ボックス境界が描かれた文字行列の形で出力される
s22 は 4×4 のサンプル問題で、sbox¨ sudoku s22 の結果は 3 個の解答 を持つ
s34 は 3×4 ボックスを使うサンプル問題
3 4 sbox s34 で問題をボックス区切りの形で表示する
3 4 sudoku s34 の結果は 2 個の解答 を持つ
参考リンクと関連項目
1件のコメント
Hacker Newsのコメント
この行は K で書かれている。K は Arthur Whitney が APL と Scheme をもとに作った言語だ。
x(,/{@[x;y;]'(!10)^x*|/p[;y]=p,:,3/:-3!p:!9 9}')/&~*xときどき、コードの複雑さをコード行数と次の出力結果を比べて見積もることがある。
tar -cf - . | gzip | base64 | wc -lつまり「どれだけうまく圧縮できるか?」を見ているわけだ。APL を見ると、誤って gzip の出力をターミナルに流してしまったときのことを思い出す。
p←{(↑⍵)∘{(⍺∨.=⍵)/⍳n×n∘}¨,⍵},(n*÷2){⍵,⍺⊥⌊⍵÷⍺}'⍳n n←⍴⍵こんなコードを追いながら「バグを見つけられるか?」までやる人がいるのには感心する。まるで、みんなが同じ辞書を持っている圧縮済みバイナリデータのように感じる。
∘といった構文エラーがあるし、n n←⍴⍵はnを二度設定して⍵が 2 次元だと期待している合図のように見えるが、意図次第では_ n←⍴⍵やn←⊃⌽⍴⍵のほうが自然だ。また
⊥は⍴⍵が単一の整数か空ベクトルでなければエラーになるので、結局n←⍴⍵と変わらず、なおさら紛らわしい。重複した,や↑⍵もいくつか削れるし、式全体は実際のところp←(n+1)⍴⊂⍳n×n←⍴⍵とほぼ同じになり、1..n²ベクトルをn+1個出す構造だ。見た目は奇妙でも、記号と基本演算を覚えれば APL は意外と素直だ。ただし熟達するには時間がかかり、その域に達すると超能力のように感じられる。
言語の支持者が速度、配列処理のしやすさ、表現力の高い文法を強調するというのはその通りだ。
https://en.m.wikipedia.org/wiki/K_(programming_language)
コード行数は、言語ごとに行の使い方が違うので良い指標ではない。
もっと良い尺度は、「定数」や「関数呼び出し」のような意味のある非終端記号を基準に 構文木ノード数 を数えることかもしれない。さらに、その木の深さや分岐係数まで考慮できればもっとよい。
ワンライナー解法は画面スペースをほとんど取らないので、複雑な問題を扱うときに大きな利点になる。視線を画面内で動かすほうが、ファイルをまたいでスクロールするよりずっと負担が小さく、認知負荷は重要だ。
K を知らなくても、定数が並んで見えれば問題の直接的なデータ表現を使っているように見える。K の文化がこうしたコードを奨励し、直接性と単純さの方向へ思考を傾けるのなら、チームにそういう秘伝のタレを持ち込みたくなる。
https://cliffle.com/esoterica/hq9plus/
https://en.wikipedia.org/wiki/Algorithmic_information_theory
APL/K のような言語を使うと、プログラマーは実際に問題をより効率的に考えられるのか、前からよく気になっていた。
avg a+bとして考え、書けるのは確かに楽だ。配列中心でない言語だと、境界チェックや大きな
forループ、合計や件数を入れる一時変数などが必要になる可能性が高い。C のような言語ならだいたい 6 行くらいになる処理が、Q では 6 文字で済むような違いだ。ただし、どの言語にも特定の問題をよりうまく推論できるようにする機能がある。代数的データ型とパターンマッチングを持つ関数型言語、たとえば OCaml や F# は大きな
switchやif-else-ifより優れているし、async/awaitのようなシンタックスシュガーを持つ言語は並行処理に向いている。クオンツとして働いていたころ、kdb+/q を 5 年以上にわたって中頻度戦略で多用していたが、板情報の計算のように簡単にも効率的にもベクトル化できない高頻度取引の領域へ移ると、配列指向言語を使い続けることはかえって問題の推論を複雑にした。
https://youtu.be/PlM9BXfu7UY?si=ORtwI1qmfmzhJGZX&t=3598
その箇所はコンパイラ文脈の話だったが、発表全体では Dyalog と APL を数学的記法体系として扱っている。主な流れとしては、一般的なコードより数学的な式を最適化するほうが簡単な場合がある、ということだ。
ここで最も重要な点のひとつは、上部の問題生成器が非常に明快だということ。これが、JやKを含むIverson式の記号言語と他の言語との違いだ
一行解法の優雅さや力強さはないが、厳密なコメントがなくてもとてもすっきりしていて理解しやすい。ただし、
lampは良いコメント記号ではないと思う一行解法は驚異的で、暗黙的プログラミングは頭がねじ曲がるほどすごい。グリフベース言語の独特な圧縮性を関数型プログラミングの説明と実行に使い、それをさらに配列全体に適用した発想は天才的だ
https://www.jsoftware.com/papers/fork.htm
もちろん、そうした能力を取り除けばより冗長なコードを書くよう強制はできるが、そうすると対話的ツールとしての強みが大きく損なわれる。Iverson式言語は非常に短いコードを書けるので、対話的作業に向いている。そのときのコードは保存すらされないので、本当に write-only コードだ
ファイルに入れるコードを書くときは好きなスタイルを選べばよく、その場合はあまり圧縮しすぎない書き方を勧める。それでもIverson式言語は、冗長なスタイルで書いても大半の言語よりはるかに短いコードを提供する
ほとんどの人は記号を理由に敬遠するが、私の問題はそこではなかった
APLや配列言語は好きで、そこで学んだことは他の言語を書くうえでも大いに役立った。だが日常的な道具にはならなかった。記号のせいではなく、3〜4年ほど断続的に触れているうちに越えられない壁にぶつかったからだ
他の言語にはたいてい、問題をとりあえずでも解いていく一般的なアプローチがあり、後でその問題の「秘訣」を見つければ、もっと優雅で効率的に直せる。APLにはそうした一時的な回避策がなく、秘訣を知っているか知らないかのどちらかしかないように感じられた
実際にそうなのか、秘訣を十分に学べば問題解決の直感が身につくのか、最後まで秘訣頼みなのか、それとも自分が中核となる戦略文書を読んでいないだけなのかは、よく分からない
⍸⍣¯1を使えばいいのに?」と言いがちだが、そもそも誰かが⍸に逆演算があって、それをどう使うか教えてくれたとは限らない今でも何年もこうした言語を使っているが、一部の配列プログラマが作るコードの壁には少し気後れする。なぜそう書くのかは理解できるが、個人的にはコードに多少空白がある方が好みだ
APLベースの配列言語を作っているのだが、初期目標のひとつは、
if文のようなものを書く初心者を罰せず、命令型スタイルを第一級市民にすることだった。このスタイルは、純粋なAPL風と一般的な命令型言語の中間あたりだと考えているhttps://codeberg.org/loke/array/src/branch/master/array/standard-lib/output3.kap
とはいえ、それは言語自体の限界でもない。私の経験では、その壁を突破する過程こそがパラダイムが腑に落ちる過程だった。1年間YAMLパーサのプロトタイプを作りながら500時間ほどハックして、ようやく点と点がつながり始めた
核心は、データ駆動設計の原則、良い記法のIverson的特性をソフトウェアアーキテクチャで具体的に活用する方法、そしてイディオムとそれがドメイン概念をどう表現するかに慣れること、その組み合わせのように感じる
https://dyalog.tv/Dyalog23/?v=J4cg6SV92C4
https://www.jsoftware.com/papers/tot.htm
この話題に関する動画がある
https://www.youtube.com/watch?v=DmT80OseAGs
解法は https://tryapl.org/ で自分で試せる
この一行をさまざまなプログラミング言語のコードゴルフ解法と比べてみると面白いかもしれない
https://codegolf.stackexchange.com/questions/tagged/sudoku?tab=Votes
https://codegolf.stackexchange.com/a/5030