LoginSignup
3
4

数独を全探索で解く(C#版)

Last updated at Posted at 2024-04-28

はじめに

先人に触発されて、C#で数独を解いてみた。

変更点

  • C#で書く
  • 盤面を文字列でなくbyte配列で保持する
  • 行・列・ブロックの取り得る状態をbit列で保持する
  • イテレータを使用して、取り得る解を全て返す

実装

※簡潔に表すために、エラー処理等は省いています

環境.
Windows 11
Visual Studio 2022
.NET 8.0

使い方

main.cs
var q = "800000000003600000070090200050007000000045700000100030001000068008500010090000400";
var a = NumPle.Solve(q).Take(2).ToArray();

Console.WriteLine(a.Length switch {
    1 => a,
    0 => "Error:解がありません",
    _ => "Error:解が複数あります"
});

解法クラス

問題を読み込み、取り得る解を全て返す
問題及び解は先人に倣い、文字列(81文字)としている
クラスを直接参照せず、静的関数(Solve)のみ外部からアクセス可能とした

NumPle.cs
public class NumPle
{
    public static IEnumerable<byte> Parse(string s) => s.Select(c => (byte)(c - '0'));

    public static IEnumerable<string> Solve(string question)
    {
        var max = (int)Math.Sqrt(question.Length);
        return new NumPle([.. Parse(question)], max).Solve();
    }

    readonly byte[] cells;
    readonly int max;

    NumPle(byte[] cells, int max)
    {
        this.cells = cells;
        this.max = max;
    }

    IEnumerable<string> Solve()
    {
        var (i, possibles) = new Possible(max, cells).GetMinimum();
        if (i < 0)
        {
            if (IsComplete)
                yield return Answer;
            yield break;
        }

        foreach (var n in possibles)
        {
            cells[i] = n;
            foreach (var answer in Solve())
                yield return answer;
        }

        cells[i] = 0;
    }

    bool IsComplete => cells.All(v => v != 0);

    string Answer => string.Join("", cells.Select(v => $"{v}"));
}

探索クラス

盤面の状態から、次に変更すべきセルの位置と取り得る解を返す
次に変更すべきセルは、一番取り得る解が少ないセルとした
また、取り得る解が無いセルが見つかった場合は、解無しとなるためエラー(セル位置=-1)を返す

Possible.cs
internal class Possible(int max)
{
    readonly byte[] indexes = [.. Enumerable.Range(1, max).Select(i => (byte)i)];

    readonly Rows rows = new(max);
    readonly Columns columns = new(max);
    readonly Blocks blocks = new(max);
    readonly List<int> sets = [];
    readonly int length = max * max;

    public Possible(int max, byte[] cells) : this(max)
    {
        for (var i = 0; i < cells.Length; i++)
        {
            var n = cells[i] - 1;
            if (n < 0) continue;
            sets.Add(i);
            this[i, n] = true;
        }
    }

    public (int Cell, byte[] Possibles) GetMinimum()
    {
        if (IsError) return (-1, []);

        var (m, p) = (-1, new byte[max]);

        for (var i = 0; i < length; i++)
        {
            if (sets.Contains(i)) continue;
            var numbers = indexes.Where(n => !this[i, n - 1]).ToArray();
            // 取り得る解が無い場合は、この盤面はエラー
            if (numbers.Length <= 0) return (-1, []);
            if (p.Length <= numbers.Length) continue;
            (m, p) = (i, numbers);
        }

        return (m, p);
    }

    bool this[int cell, int bit]
    {
        get => rows[cell, bit] | columns[cell, bit] | blocks[cell, bit];
        set
        {
            rows[cell, bit] = value;
            columns[cell, bit] = value;
            blocks[cell, bit] = value;
        }
    }

    bool IsError => rows.IsError || columns.IsError || blocks.IsError;
}

行・列・ブロックの状態クラス

行・列・ブロックの状態を保持する
セル位置から各行・列・ブロックの位置を計算して、入力された数値を保持する
同じ数値が2回以上入力された場合は、エラー(IsError = true)とする
位置計算以外は、共通化(Countsクラス)した

Counts.cs
internal abstract class Counts(int count)
{
    static bool GetBit(int bits, int i) => (bits & 1 << i) != 0;
    static void SetBit(ref int bits, int i, bool value) => bits = value ? bits | 1 << i : bits & ~(1 << i);

    readonly int[] val = new int[count];
    protected readonly int length = count;

    protected abstract int GetIndex(int index);

    public bool this[int cell, int bit]
    {
        get => GetBit(val[GetIndex(cell)], bit);
        set
        {
            if (this[cell, bit] == value) IsError = true;
            SetBit(ref val[GetIndex(cell)], bit, value);
        }
    }

    public bool IsError { get; private set; }
}

internal class Rows(int count) : Counts(count)
{
    protected override int GetIndex(int index) => index / length;
}

internal class Columns(int count) : Counts(count)
{
    protected override int GetIndex(int index) => index % length;
}

internal class Blocks(int count) : Counts(count)
{
    readonly int SPLIT = count / 3;

    protected override int GetIndex(int index) =>
        (index % length) / SPLIT + (index / length / SPLIT) * SPLIT;
}

まとめ

先人(Python)に比べて行数が爆発した

3
4
1

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
4