3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【for文撲滅】数独ソルバのfor文を、関数型プログラミングで撲滅してみた【F#】

3
Last updated at Posted at 2026-06-28

目次

背景

  • 前回の記事にて, F#を用いて数独のソルバを実装してみた
  • 制約式の生成でfor文を使っていた部分を関数型で書き直したくなった
  • 複数ファイルの並列処理を行いたくなった
  • ついでに処理時間を計測したくなった

以上の経緯で記事を書きます. ディレクトリ構成を以下に示しておきます. 必要に応じてご参照ください.

※前回の記事と同じです.

sudoku-solver/
├── Core/
│   ├── Optimizer/
│   │   └── SudokuSolver.fs # ソルバ
│   ├── Utils/
│   │   ├── CsvHandler.fs   # 問題のCSV読み込み処理
│   │   ├── Models.fs       # 問題の型や解のタイプ等
│   │   ├── PathHandler.fs  # パスのハンドリングを行う
│   │   └── Visualizer.fs   # ソルバの結果を可視化する
│   ├── Core.fsproj
│   └── Program.fs          # エントリポイント
├── Data/
│   └── base/               # 問題集
│       ├── L7_1_01.csv
│       ...
│       └── L7_1_10.csv
└── sudoku-solver.sln

制約式からfor文を撲滅する

for文がもたらす副作用

まず, 副作用の代表格(主観)である, for文です.

using System;

namespace Core
{
    static class Program
    {
        static void Main(string[] args)
        {
            for (int i = 0; i < 10; i++)
            {
                Console.WriteLine(i);
            }
        }
    }
}

上記コードにおいて, for文内部の変数iが毎回書き換えられています.
ループ1回ごとに内部の振る舞いが変わるため, for文は典型的な副作用であると言えます.

それでは改めて, 数独ソルバの制約式を見てみましょう.

ヒント数字に関する制約

ヒント数字に関する制約の実装を提示します.

module SudokuSolver =
    open Google.OrTools.Sat
    /// 制約を追加するプライベートモジュール
    module private ConstraintAdder =
        /// 初期の制約を追加する
        let private addConstraintsInitial (model: CpModel) (vars: BoolVar[,,]) (puzzle: SudokuPuzzle): unit =
            let idxs = [0..8]
            // 初期の制約を追加する
            for r in idxs do                        // <- ココと,
                for c in idxs do                    // <- ココ.
                    let value = puzzle.Cells[r, c]
                    if value <> 0 then
                        [vars.[r,c,value-1] :> ILiteral]
                        |> List.toSeq
                        |> model.AddBoolAnd
                        |> ignore

上記のコードにおいて, for文が連続しています.
行と列の数だけ, 0~8までのインデックスを回す処理になっています.
これを, 次のように変更しました.

module SudokuSolver =
    open Google.OrTools.Sat
    /// 制約を追加するプライベートモジュール
    module private ConstraintAdder =
        /// 初期の制約を追加する
        let private addConstraintsInitial (model: CpModel) (vars: BoolVar[,,]) (puzzle: SudokuPuzzle): unit =
            puzzle.Cells
            |> Array2D.iteri (fun r c value ->
                if value <> 0 then
                    [ vars.[r, c, value-1] :> ILiteral]
                    |> model.AddBoolAnd
                    |> ignore
            )

Array2D.iteriについて. F#の二次元配列が持つ機能で,

  • 行方向のインデックス
  • 列方向のインデックス
  • 上記行, 列インデックスが指し示す値

の3つを同時に回すことが出来ます.
個人的には衝撃的なツールで, forの2重ループを撲滅出来るのが素敵です.
(PythonでもC#でも, ネストが深くなってちょっとイヤでした...)

そこから, もしヒント数字が入っていたら, それをインデックスとして書き換え(マイナス1),
ILiteralに変換し, model.AddBoolAndtrueを確定させる, という流れです.

書いてあるそのまんまで良いですね.

各セルにはちょうど1つの数字が入る制約

各セルにはちょうど1つの数字が入る制約の実装を提示します.

module SudokuSolver =
    open Google.OrTools.Sat
    /// 制約を追加するプライベートモジュール
    module private ConstraintAdder =
        /// 各セルにはちょうど1つの数字が入る
        let private addConstraintsOne (model: CpModel) (vars: BoolVar[,,]): unit =
            let idxs = [0..8]
            // 各セルにはちょうど1つの数字が入る
            for r in idxs do
                for c in idxs do
                    [ for v in idxs -> vars.[r, c, v] :> ILiteral ]
                    |> model.AddExactlyOne
                    |> ignore

先ほどと同様, 上記のコードにおいて, for文が連続しています.
これを, 次のように変更しました.

module SudokuSolver =
    open Google.OrTools.Sat
    /// 制約を追加するプライベートモジュール
    module private ConstraintAdder =
        /// 各セルにはちょうど1つの数字が入る
        let private addConstraintsOne (model: CpModel) (vars: BoolVar[,,]): unit =
            Array2D.init 9 9 (fun r c -> vars.[r, c, *])
            |> Seq.cast<BoolVar[]>
            |> Seq.iter (fun slice ->
                slice
                |> Seq.map (fun v -> v :> ILiteral)
                |> model.AddExactlyOne
                |> ignore
            )

ちょっと独特なことをしています.

前提として, varsは三次元のキューブ状に変数が並んだ箱だと考えてください.

以下のような操作をしています

  • Array2D.init 9 9で, 9x9の二次元配列
  • 上記配列の中に, varsの三次元キューブから, r, cに対応する特定のマスに対して, 奥行き(vの方向)の変数9個を, 1本ずつ串としてごそっと取ってくる
  • Seq.cast<BoolVar[]>で, 上記の串1本をBoolVar型の配列のSeqに変換
  • 上記SeqILiteralに変換し, 串1本につきtrueになる変数は一つという制約条件を与える

特筆すべきは, Pythonでいう所のスライスを使用しているところですね.

Array2D.init 9 9 (fun r c -> vars.[r, c, *])

コードインデックスr cで走査しますが,
高さ分のインデックスv*でスライスされ,
その分はSeq.iterの部分で使われます.

numpyでいう所のvars[:, c, v]の書き方に共通した考え方です.

1~9の数値が出てくるのは3×3の領域それぞれで1回

1~9の数値が出てくるのは3×3の領域それぞれで1回の制約の実装を提示します.

module SudokuSolver =
    open Google.OrTools.Sat
    /// 制約を追加するプライベートモジュール
    module private ConstraintAdder =
        /// 1~9の数値が出てくるのは3×3の領域それぞれで1回
        let private addConstraintsBlock (model: CpModel) (vars: BoolVar[,,]): unit =
            let idxs = [0..8]
            // 1~9の数値が出てくるのは3×3の領域それぞれで1回
            let blockIdx = [0..2]
            for v in idxs do
                for rBlock in blockIdx do
                    for cBlock in blockIdx do
                        [
                            for r in blockIdx do
                            for c in blockIdx
                            -> vars.[r+3*rBlock, c+3*cBlock, v]
                            :> ILiteral
                        ]
                        |> List.toSeq
                        |> model.AddExactlyOne
                        |> ignore

for文撲滅します.

module SudokuSolver =
    open Google.OrTools.Sat
    /// 制約を追加するプライベートモジュール
    module private ConstraintAdder =
        /// 1~9の数値が出てくるのは3×3の領域それぞれで1回
        let private addConstraintsBlock (model: CpModel) (vars: BoolVar[,,]): unit =
            // 9つの各ブロック(k)において、3x3の範囲スライスを切り出して1次元配列に潰す
            Array2D.init 9 9 (fun v k ->
                let rBlock = (k / 3) * 3
                let cBlock = (k % 3) * 3
                vars.[rBlock .. rBlock + 2, cBlock .. cBlock + 2, v]
                |> Seq.cast<BoolVar>
                |> Seq.toArray
            )
            |> Seq.cast<BoolVar[]>
            |> Seq.iter (fun slice ->
                slice
                |> Seq.map (fun v -> v :> ILiteral)
                |> model.AddExactlyOne
                |> ignore
            )

この制約は若干骨が折れますね. 前半と後半に分けましょう.

まずは前半から.

/// 1~9の数値が出てくるのは3×3の領域それぞれで1回
let private addConstraintsBlock (model: CpModel) (vars: BoolVar[,,]): unit =
    // 9つの各ブロック(k)において、3x3の範囲スライスを切り出して1次元配列に潰す
    Array2D.init 9 9 (fun v k ->
        let rBlock = (k / 3) * 3
        let cBlock = (k % 3) * 3
        vars.[rBlock .. rBlock + 2, cBlock .. cBlock + 2, v]
        |> Seq.cast<BoolVar>
        |> Seq.toArray
    )
    |> Seq.cast<BoolVar[]>
    // 後半略

ちょっと複雑な書き方してますが...

  • rBlockcBlockで, 3で割った商と剰余に対して3をかけて, 3x3ブロックの"左上"のインデックスを取得
  • 上記3x3ブロックの"左上"のインデックスに対し, varsから各3x3ブロックの変数群を取得
  • 上記3x3ブロックの変数を, Seq.cast<BoolVar>で1次元の配列にする

続いて後半です.

/// 1~9の数値が出てくるのは3×3の領域それぞれで1回
let private addConstraintsBlock (model: CpModel) (vars: BoolVar[,,]): unit =
    // 前半略
    |> Seq.iter (fun slice ->
        slice
        |> Seq.map (fun v -> v :> ILiteral)
        |> model.AddExactlyOne
        |> ignore
    )

前半で作成した1次元の配列を受け取り, 各配列に対してtrueは一つだけという制約式を与えます.

並列処理を行う

先ほどまでは数独ソルバの中身について論じましたが, ここからは並列処理について記載します.

まずは全体のコードです. 通常の処理も一緒に書きます

// Core/Program.fs

namespace Core

module Program =
    open Core.Utils.CsvHandler
    open Core.Utils.PathHandler
    open Core.Utils.Visualizer
    open Core.Optimizer.SudokuSolver
    /// 順番に処理する
    let syncRun (root: string) (targetFiles: string list): unit =
        // ターゲットのファイルリストに対し,
        // 絶対パス取得->問題取得->求解->可視化
        targetFiles
        |> List.map (combinePaths root)
        |> List.iter (
            fun csvPath ->
                printfn "Loading: %s" csvPath
                csvPath
                |> loadPuzzle
                |> solvePuzzle
                |> visualizeResult
            )

    /// 並列で処理する
    let asyncRun (root: string) (targetFiles: string list): unit =
        targetFiles
        |> List.map (fun path ->
                async {
                    let result =
                        path
                        |> combinePaths root
                        |> loadPuzzle
                        |> solvePuzzle
                    return (path, result)
                }
            )
        |> Async.Parallel
        |> Async.RunSynchronously
        |> Array.iter (fun (path, result) ->
                printfn "Result for %s:" path
                visualizeResult result
            )

順番に処理するところは前回と一緒ですね. 並列処理の部分を解説します.

まずはこの部分からです.

targetFiles |> List.map (fun path -> async { ... })

ここで, 「パス結合->問題をローディング->解を求める->パスと結果を返す」の一連の流れを,
ファイル数分だけ作成します. この時点では実行されません.

ここからの流れは全体を見た方が良いので, 上記の部分を省略して再掲します.

    /// 並列で処理する
    let asyncRun (root: string) (targetFiles: string list): unit =
        targetFiles
        |> List.map (fun path -> async { ... })
        |> Async.Parallel
        |> Async.RunSynchronously
        |> Array.iter (fun (path, result) ->
                printfn "Result for %s:" path
                visualizeResult result
            )

上記「パス結合->問題をローディング->解を求める->パスと結果を返す」のパックを以下のように処理します

  • Asyc.Parallel
    • PCのCPUにタスクアロケーションする
  • Async.RunSynchronously
    • 各CPUコアに「よーいドン」で解かせる
    • 全部が解き終わるまで待ち, 配列に格納する
  • Array.iter
    • 上記で解き終わった結果の配列を画面に出力する

こんな感じで, シンプルな並列処理を実装しました.

処理時間を計測する

並列処理を実装したら, どのくらい早くなったのか, 計測したくなりますよね?当然ですよね?

実装

と, いうわけで, 実装してみました. まずは計測する関数です.

// Core/Program.fs

namespace Core

module Program =
    open System.Diagnostics

    // 任意の処理(f)を実行し, 計測時間と結果を返す
    let measureTime (f: unit -> 'T): int64 * 'T =
        let sw = Stopwatch.StartNew()
        let result = f()
        sw.Stop()
        (sw.ElapsedMilliseconds, result)

シンプルですね. C#ラブな皆様なら割とメジャーかな?と思いますが...
System.Diagnosticsを使用して, ミリ秒単位で返します.

任意の関数の返り値はresultでラッピングしてあげて, 一緒に返してあげます.

使い方は以下の通りです.

// Core/Program.fs

namespace Core

module Program =
    // 省略

    // 任意の処理(f)を実行し, 計測時間と結果を返す
    let measureTime (f: unit -> 'T): int64 * 'T =
        // 省略

    /// 順番に処理する
    let syncRun (root: string) (targetFiles: string list): unit =
        // 省略

    /// 並列で処理する
    let asyncRun (root: string) (targetFiles: string list): unit =
        // 省略

    // エントリポイント
    [<EntryPoint>]
    let main args =
        // ルートパスを取得する
        let root    = resolveProjectRoot args
        printfn "ProjectRoot : %s" root

        // ターゲットのファイルをリストでベタ書きする
        let targetFiles = [
            "Data/base/L7_1_01.csv";
            // 省略
            "Data/base/L7_1_10.csv";
        ]

        // 順番に処理する
        let time1, _ = measureTime (fun () -> syncRun root targetFiles)
        // 並列で処理する
        let time2, _ = measureTime (fun () -> asyncRun root targetFiles)

        // 比較用に書き出す
        printfn "Sync  elapsed: %d ms" time1
        printfn "Async elapsed: %d ms" time2

        0

重要なポイントがあるので, 解説しておきます.

順番に処理する部分で, 以下の記載があります.

// 順番に処理する
let time1, _ = measureTime (fun () -> syncRun root targetFiles)

ここ, 理解出来たら何てことないのですが, 私はちょっとミスしたので共有します.

私は最初, 以下のように書いていました.

// 順番に処理する
let time1, _ = measureTime (syncRun root targetFiles)

高階関数に慣れ始めて, 上記のような書き方をしていました. これの書き方がなぜダメかと言うと,

  • measureTimeを実行する前に, syncRun root targetFilesを実行する
  • 数独を全て解き終わった後の結果(unit)を, measureTimeに渡す

という処理になってしまいます.

再掲しますが, 意図通りに動く書き方は以下の通りです.

// 順番に処理する
let time1, _ = measureTime (fun () -> syncRun root targetFiles)

匿名関数であるfun () -> ...を使用します.
この匿名関数でまずはsyncRun root targetFilesを実行せずにmeasureTimeに渡し,
measureTime内で実行する, という動きになります.

計測結果

10回ずつ実行して時間を計測してみました. まずは箱ひげ図をどうぞ.

measured_time.png

細かい集計とデータは以下をご覧ください.
ざっくりですが, 並列処理により4倍程度の高速化になっていますね.

集計データです.

Term Sync[ms] Async[ms] Ratio (Sync/Async)
average 212.5 51.1 4.16
median 209.5 50.5 4.15

各試行データです

Sync[ms] Async[ms]
1 202 52
2 224 55
3 213 51
4 213 50
5 210 54
6 207 54
7 206 48
8 209 49
9 206 49
10 235 49

最後に余談

F#の数独ソルバを良い感じに出来たかな?と思います.
普段だったらこれで終わりますが, ちょっとだけ余談です.

次の画像は, qiitaでC#タグ検索した結果です(2026年6月29日時点)

csharp.png

一方, F#でタグ検索すると...

fsharp.png

C# F#
articles 20031 443
follower 56752 364

私もC#は好きですよ?オブジェクト指向で, 型は硬いし, 大規模開発するなら私はC#を一番に選びます. しかし, F#の良いところも見逃せないと思います.

実際, F#だと数式っぽく書けるし, パイプラインで美しく書けるし,
何より手続き型やオブジェクト指向っぽい書き方も出来て, 初心者に優しい.
良いところを挙げ出したらキリがないくらい, 良い言語だと思います.

もう少し盛り上がってくれないかなぁ...

私はチマチマと記事を書きます. 最後まで読んでもらえて嬉しいです. ありがとうございました.

Appendix

ひたすら実装したコードの全文を張ります.

CsvHandler

// Core/Utils/CsvHandler.fs

namespace Core.Utils

open System
open System.IO
open Core.Utils.Models

// CSVパーサー
module CsvHandler =
    /// 文字をパースする. 空白はゼロ, それ以外はintに変換
    let private parseCell (s: string): int =
        match s.Trim() with
        | "" -> 0
        | s -> int s

    /// 1行をカンマで区切り, 各文字をパースして配列で返す
    let private parseLine (line: string) : int[] =
        line.Split(',') |> Array.map parseCell

    /// CSVパスを受け取り, 2次元配列に整形, 問題の型にして返す
    let loadPuzzle (path: string) : SudokuPuzzle =
        let cells =
            File.ReadLines(path)
            |> Seq.filter (fun line -> not (String.IsNullOrWhiteSpace(line)))
            |> Seq.map parseLine
            |> Seq.toArray
            |> fun rows -> Array2D.init 9 9 (fun r c -> rows.[r].[c])
        { Cells = cells }

Models

// Core/Utils/Models.fs

namespace Core.Utils

/// 型定義
module Models =
    /// 問題の型
    type SudokuPuzzle =
        { Cells: int[,] }

    /// ソルバの結果型
    type SolveResult =
        /// 解が見つかった
        | Solved of SudokuPuzzle
        /// 解無し
        | Infeasible
        /// タイムアウト等
        | Unknown

PathHandler

namespace Core.Utils

open System
open System.IO

/// パスのハンドリング
module PathHandler =
    /// ルートパスを何とかして返す関数
    let resolveProjectRoot (args: string[]) =
        // ステップその1. --root引数を探し出す. 見つからなければNone
        let fromArgs =
            args
            |> Array.pairwise
            |> Array.tryFind (fun (k, _) -> k = "--root")
            |> Option.map snd

        // ステップその1でルートパスが見つかっていればそのまま
        match fromArgs with
        | Some root -> root
        | None ->
            // cwdでカレントディレクトリを取得
            let cwd = Directory.GetCurrentDirectory()
            if Directory.Exists(Path.Combine(cwd, "Data", "base")) then
                // カレントディレクトリにデータのディレクトリが存在が見つかったので,
                // ルートパスとして適切. そのまま返す
                cwd
            else
                // カレントディレクトリにデータのディレクトリが存在しない
                // カレントディレクトリがnetX.0のディレクトリになっている
                // 親(Debug or Release)の親(bin)の親(Core)のフルパスを取得して返す
                AppDomain.CurrentDomain.BaseDirectory
                |> Directory.GetParent
                |> fun d -> d.Parent
                |> fun d -> d.Parent
                |> fun d -> d.FullName

    /// 2つの文字列をパスとして結合する
    let combinePaths (root: string) (relativePath: string) =
        Path.Combine(root, relativePath)

Visualizer

// Core/Utils/Visualizer.fs

namespace Core.Utils

open Core.Utils.Models

/// 可視化用モジュール
module Visualizer =

    let private getRow (arr: int[,]) (r: int) : int[] =
        [| for c in 0..8 -> arr.[r, c] |]

    let private getVisRow (row: int[]): string =
        // 分かりにくいが, 要するに以下のようにする
        // [1; 2; 3; 4; 5; 6; 7; 8; 9]
        // -> "|1 2 3|4 5 6|7 8 9|"
        $"|{row.[0]} {row.[1]} {row.[2]}|{row.[3]} {row.[4]} {row.[5]}|{row.[6]} {row.[7]} {row.[8]}|"

    let private getVisList (sep: string) (arr: int[,]) =
        let row r = getRow arr r |> getVisRow
        [0..8]
        |> List.collect (
            fun r ->
                if r % 3 = 0 then
                    [ sep; row r ]
                else
                    [ row r ]
            )
        |> fun lines -> lines @ [ sep ]

    let private visualizeCells (arr: int[,]) : unit =
        let sep   = "+-----+-----+-----+"
        getVisList sep arr |> List.iter (printfn "%s")

    /// ソルバの結果を受け取り, 可視化を行う
    let visualizeResult (result: SolveResult): unit =
        match result with
        | Solved p -> visualizeCells p.Cells
        | Infeasible -> eprintfn "Infeasible"
        | Unknown -> eprintfn "Interrupted"

SudokuSolver

// Core/Optimizer/SudokuSolver.fs

namespace Core.Optimizer

open Core.Utils.Models

/// ソルバ
module SudokuSolver =
    open Google.OrTools.Sat

    /// モデル, 変数の生成を行うプライベートモジュール
    module private ModelBuilder =
        /// モデル, 変数を生成する
        let buildModel (): CpModel*BoolVar[,,] =
            // モデルを生成する
            let model = CpModel()
            // ブーリアン変数を9*9*9個作成する
            let vars = Array3D.init 9 9 9 (fun r c v -> model.NewBoolVar($"x_{r}_{c}_{v}"))

            model, vars

    /// 制約を追加するプライベートモジュール
    module private ConstraintAdder =
        /// 初期の制約を追加する
        let private addConstraintsInitial (model: CpModel) (vars: BoolVar[,,]) (puzzle: SudokuPuzzle): unit =
            puzzle.Cells
            |> Array2D.iteri (fun r c value ->
                if value <> 0 then
                    [ vars.[r, c, value-1] :> ILiteral]
                    |> model.AddBoolAnd
                    |> ignore
            )

        /// 各セルにはちょうど1つの数字が入る
        let private addConstraintsOne (model: CpModel) (vars: BoolVar[,,]): unit =
            Array2D.init 9 9 (fun r c -> vars.[r, c, *])
            |> Seq.cast<BoolVar[]>
            |> Seq.iter (fun slice ->
                slice
                |> Seq.map (fun v -> v :> ILiteral)
                |> model.AddExactlyOne
                |> ignore
            )

        /// 1~9の数値が出てくるのは1列に1回
        let private addConstraintsColumn (model: CpModel) (vars: BoolVar[,,]): unit =
            Array2D.init 9 9 (fun c v -> vars.[*, c, v])
            |> Seq.cast<BoolVar[]>
            |> Seq.iter (fun slice ->
                slice
                |> Seq.map (fun x -> x :> ILiteral)
                |> model.AddExactlyOne
                |> ignore
            )

        /// 1~9の数値が出てくるのは1行に1回
        let private addConstraintsRow (model: CpModel) (vars: BoolVar[,,]): unit =
            Array2D.init 9 9 (fun r v -> vars.[r, *, v])
            |> Seq.cast<BoolVar[]>
            |> Seq.iter (fun slice ->
                slice
                |> Seq.map (fun x -> x:> ILiteral)
                |> model.AddExactlyOne
                |> ignore
            )

        /// 1~9の数値が出てくるのは3×3の領域それぞれで1回
        let private addConstraintsBlock (model: CpModel) (vars: BoolVar[,,]): unit =
            // 9つの各ブロック(k)において、3x3の範囲スライスを切り出して1次元配列に潰す
            Array2D.init 9 9 (fun v k ->
                let rBlock = (k / 3) * 3
                let cBlock = (k % 3) * 3
                vars.[rBlock .. rBlock + 2, cBlock .. cBlock + 2, v]
                |> Seq.cast<BoolVar>
                |> Seq.toArray
            )
            |> Seq.cast<BoolVar[]>
            |> Seq.iter (fun slice ->
                slice
                |> Seq.map (fun v -> v :> ILiteral)
                |> model.AddExactlyOne
                |> ignore
            )

        /// 制約を追加する
        let addConstraints (model: CpModel) (vars: BoolVar[,,]) (puzzle: SudokuPuzzle): unit =
            // 初期の制約を追加する
            addConstraintsInitial model vars puzzle
            // 各セルにはちょうど1つの数字が入る
            addConstraintsOne model vars
            // 1~9の数値が出てくるのは1列に1回
            addConstraintsColumn model vars
            // 1~9の数値が出てくるのは1行に1回
            addConstraintsRow model vars
            // 1~9の数値が出てくるのは3×3の領域それぞれで1回
            addConstraintsBlock model vars

    /// ソルバのプライベートモジュール
    module private Solver =
        /// 変数の配列を受け取り, Trueになっている変数のインデックスに1を足して返す
        let private getIdx (solver: CpSolver) (vars: BoolVar[]): int =
            vars
            |> Array.findIndex (fun v -> solver.BooleanValue v)
            |> (+) 1

        /// 求解する
        let solve (model: CpModel) (vars: BoolVar[,,]): SolveResult =
            // ソルバを生成して求解する
            let solver = new CpSolver()
            let status = solver.Solve(model)

            if status = CpSolverStatus.Optimal || status = CpSolverStatus.Feasible then
                // 2次元配列に解のインデックスを入れ込む
                let cells = Array2D.init 9 9 (fun r c ->
                    vars.[r, c, *]
                    |> getIdx solver
                )
                // 解けたので判別共用体にして返す
                Solved {Cells = cells}
            else
                // 何らかの理由で解が見つからなかった
                match status with
                | CpSolverStatus.Infeasible -> Infeasible
                | _                         -> Unknown

    /// 問題を受け取り, 結果を返す
    let solvePuzzle (puzzle: SudokuPuzzle): SolveResult =
        // モデル, 変数を生成する
        let model, vars = ModelBuilder.buildModel()
        // 制約を作成して追加する
        ConstraintAdder.addConstraints model vars puzzle
        // 求解する
        Solver.solve model vars

Program

// Core/Program.fs

namespace Core

module Program =
    open System.Diagnostics

    open Core.Utils.CsvHandler
    open Core.Utils.PathHandler
    open Core.Utils.Visualizer
    open Core.Optimizer.SudokuSolver

    // 任意の処理(f)を実行し, 計測時間と結果を返す
    let measureTime (f: unit -> 'T): int64 * 'T =
        let sw = Stopwatch.StartNew()
        let result = f()
        sw.Stop()
        (sw.ElapsedMilliseconds, result)

    /// 順番に処理する
    let syncRun (root: string) (targetFiles: string list): unit =
        // ターゲットのファイルリストに対し,
        // 絶対パス取得->問題取得->求解->可視化
        targetFiles
        |> List.map (combinePaths root)
        |> List.iter (
            fun csvPath ->
                printfn "Loading: %s" csvPath
                csvPath
                |> loadPuzzle
                |> solvePuzzle
                |> visualizeResult
            )

    /// 並列で処理する
    let asyncRun (root: string) (targetFiles: string list): unit =
        targetFiles
        |> List.map (fun path ->
                async {
                    let result =
                        path
                        |> combinePaths root
                        |> loadPuzzle
                        |> solvePuzzle
                    return (path, result)
                }
            )
        |> Async.Parallel
        |> Async.RunSynchronously
        |> Array.iter (fun (path, result) ->
                printfn "Result for %s:" path
                visualizeResult result
            )

    // エントリポイント
    [<EntryPoint>]
    let main args =
        // ルートパスを取得する
        let root    = resolveProjectRoot args
        printfn "ProjectRoot : %s" root

        // ターゲットのファイルをリストでベタ書きする
        let targetFiles = [
            "Data/base/L7_1_01.csv";
            "Data/base/L7_1_02.csv";
            "Data/base/L7_1_03.csv";
            "Data/base/L7_1_04.csv";
            "Data/base/L7_1_05.csv";
            "Data/base/L7_1_06.csv";
            "Data/base/L7_1_07.csv";
            "Data/base/L7_1_08.csv";
            "Data/base/L7_1_09.csv";
            "Data/base/L7_1_10.csv";
        ]

        // 順番に処理する
        let time1, _ = measureTime (fun () -> syncRun root targetFiles)
        // 並列で処理する
        let time2, _ = measureTime (fun () -> asyncRun root targetFiles)

        // 比較用に書き出す
        printfn "Sync  elapsed: %d ms" time1
        printfn "Async elapsed: %d ms" time2

        0

fsproj

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net9.0</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <Compile Include="Utils/Models.fs" />
    <Compile Include="Utils/CsvHandler.fs" />
    <Compile Include="Utils/PathHandler.fs" />
    <Compile Include="Utils/Visualizer.fs" />
    <Compile Include="Optimizer/SudokuSolver.fs" />
    <Compile Include="Program.fs" />
  </ItemGroup>

  <ItemGroup>
    <PackageReference Include="Google.OrTools" Version="9.15.6755" />
  </ItemGroup>

</Project>

3
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?