トップページに戻る    次のC#のサンプルへ    前のC#のサンプルへ

Cマガ電脳クラブ(第068回) N龍

問題

将棋の龍の駒は、飛車と王を合わせた動きをする。
つまり、将棋盤のなかで上下左右にどこまでも進めるのに加えて、すぐ斜め隣の4か所にも進むことができる。
将棋を知らない人もFig.1を見ればわかるだろう。

9x9の大きさの将棋盤にいくつかの龍を配置して、
どの空いているマスもいずれかの龍のきき筋 (そこに龍が進めるということ) になるようにしたい。
さて、最小何個の龍を配置すればよいだろうか。
また、最小個数の配置は、回転・裏返しで一致するものを除いて何通りあるだろうか。

Fig.1 龍のきき筋


ソース

using System;
using System.Collections.Generic;
using System.Linq;

class Program
{
    const int UB = 9 - 1;

    struct JyoutaiDef
    {
        internal int Level;
        internal bool[,] KikiArr;
        internal bool[,] HaitiArr;
    }

    static System.Diagnostics.Stopwatch sw = System.Diagnostics.Stopwatch.StartNew();

    static void Main()
    {
        //反復深化深さ優先探索で解く
        for (int Depth = 1; Depth <= 9 * 9; Depth++) {
            Console.WriteLine("深さ制限={0}で解を検証中。経過時間={1}", Depth, sw.Elapsed);

            List<bool[,]> AnswerHaitiArrList = ExecDFS(Depth);

            if (AnswerHaitiArrList.Count == 0) continue;

            //回転を除外
            RemoveKaiten(AnswerHaitiArrList);
            for (int I = 0; I <= AnswerHaitiArrList.Count - 1; I++) {
                Console.WriteLine("(回転除外後の)解{0}", I + 1);
                PrintBan(AnswerHaitiArrList[I]);
            }

            Console.WriteLine("解は{0}通り。経過時間={1}", AnswerHaitiArrList.Count, sw.Elapsed);
            break;
        }
    }

    //深さ制限を引数として深さ優先探索を行う
    static List<bool[,]> ExecDFS(int pDepthMax)
    {
        var WillReturnBoolArrList = new List<bool[,]>();

        var stk = new Stack<JyoutaiDef>();
        JyoutaiDef WillPush;

        WillPush.Level = 0;
        WillPush.KikiArr = WillPush.HaitiArr = new bool[UB + 1, UB + 1];
        stk.Push(WillPush);

        while (stk.Count > 0) {
            JyoutaiDef Popped = stk.Pop();

            //レベル制限
            if (pDepthMax < Popped.Level) continue;

            //クリア判定
            if (Popped.KikiArr.Cast<bool>().All(X => X)) {
                WillReturnBoolArrList.Add(Popped.HaitiArr);
                Console.WriteLine("(回転除外前の)解候補{0}を発見。経過時間={1}",
                    WillReturnBoolArrList.Count, sw.Elapsed);

                continue;
            }

            //最後に龍を配置した座標を求める
            int LastRyuX = 0, LastRyuY = 0;
            for (int Y = 0; Y <= UB; Y++)
                for (int X = 0; X <= UB; X++)
                    if (Popped.HaitiArr[X, Y]) {
                        LastRyuX = X;
                        LastRyuY = Y;
                    }

            WillPush.Level = Popped.Level + 1;
            var KagentiEdakiriXSet = new HashSet<int>();

            for (int Y = LastRyuY; Y <= UB; Y++) {
                //回転解の除外で1枚目の龍は左上に配置
                if (WillPush.Level == 1 && 3 < Y) break;

                for (int X = 0; X <= UB; X++) {
                    //回転解の除外で1枚目の龍は左上に配置
                    if (WillPush.Level == 1 && 3 < X) break;

                    //最後に龍を配置した座標までは、Continue
                    if (Y == LastRyuY && X <= LastRyuX) continue;

                    //下限値枝切りされたX座標ならContinue
                    if (KagentiEdakiriXSet.Contains(X)) continue;

                    WillPush.KikiArr = (bool[,])Popped.KikiArr.Clone();
                    SetRyuKikiTrue(WillPush.KikiArr, X, Y);

                    //下限値枝切り
                    if (WillPush.Level + CalcNeedMinTesuu(WillPush.KikiArr, Y) > pDepthMax) {
                        KagentiEdakiriXSet.Add(X);
                        continue;
                    }

                    WillPush.HaitiArr = (bool[,])Popped.HaitiArr.Clone();
                    WillPush.HaitiArr[X, Y] = true;

                    stk.Push(WillPush);
                }
            }
        }
        return WillReturnBoolArrList;
    }

    //龍の配置座標を引数として効きの座標をTrueにする
    static void SetRyuKikiTrue(bool[,] pHaitiArr, int pX, int pY)
    {
        pHaitiArr[pX, pY] = true;

        for (int X = 0; X <= UB; X++) pHaitiArr[X, pY] = true;
        for (int Y = 0; Y <= UB; Y++) pHaitiArr[pX, Y] = true;

        Action<int, int> SetNaname = (pNanameX, pNanameY) =>
        {
            if (pNanameX < 0 || UB < pNanameX) return;
            if (pNanameY < 0 || UB < pNanameY) return;
            pHaitiArr[pNanameX, pNanameY] = true;
        };
        SetNaname(pX - 1, pY - 1);
        SetNaname(pX - 1, pY + 1);
        SetNaname(pX + 1, pY - 1);
        SetNaname(pX + 1, pY + 1);
    }

    //必要な最低手数を求める
    static int CalcNeedMinTesuu(bool[,] pKikiArr, int pY)
    {
        int NeedMinTesuu = 0;

        //カレント座標より2つ以上上のマスで効きがなかったら、
        //そのマスの下に龍を配置する必要あり
        for (int X = 0; X <= UB; X++) {
            for (int Y = 0; Y <= pY - 2; Y++) {
                if (pKikiArr[X, Y] == false) {
                    NeedMinTesuu++;
                    break;
                }
            }
        }
        return NeedMinTesuu;
    }

    //回転を除外
    static void RemoveKaiten(List<bool[,]> pTargetList)
    {
        Predicate<int> IsExist = (pCurrInd) =>
        {
            for (int I = 0; I <= pCurrInd - 1; I++) {
                bool IsOK1 = false, IsOK2 = false, IsOK3 = false, IsOK4 = false;
                bool IsOK5 = false, IsOK6 = false, IsOK7 = false; //回転1から7

                for (int X = 0; X <= UB; X++) {
                    for (int Y = 0; Y <= UB; Y++) {
                        bool CurrVal = pTargetList[pCurrInd][X, Y];
                        if (pTargetList[I][UB - X, Y] != CurrVal) IsOK1 = true;
                        if (pTargetList[I][UB - X, UB - Y] != CurrVal) IsOK2 = true;
                        if (pTargetList[I][X, UB - Y] != CurrVal) IsOK3 = true;
                        if (pTargetList[I][Y, X] != CurrVal) IsOK4 = true;
                        if (pTargetList[I][UB - Y, X] != CurrVal) IsOK5 = true;
                        if (pTargetList[I][UB - Y, UB - X] != CurrVal) IsOK6 = true;
                        if (pTargetList[I][Y, UB - X] != CurrVal) IsOK7 = true;
                    }
                }
                if (IsOK1 == false || IsOK2 == false || IsOK3 == false || IsOK4 == false
                 || IsOK5 == false || IsOK6 == false || IsOK7 == false)
                    return true;
            }
            return false;
        };

        for (int I = pTargetList.Count - 1; I >= 0; I--) {
            if (IsExist(I)) pTargetList.RemoveAt(I);
        }
    }

    //盤面を表示
    static void PrintBan(bool[,] pHaitiArr)
    {
        var sb = new System.Text.StringBuilder();
        for (int Y = 0; Y <= UB; Y++) {
            for (int X = 0; X <= UB; X++) {
                sb.Append(pHaitiArr[X, Y] ? '龍' : '□');
            }
            sb.AppendLine();
        }
        Console.WriteLine(sb.ToString());
    }
}


実行結果

深さ制限=1で解を検証中。経過時間=00:00:00.0072525
深さ制限=2で解を検証中。経過時間=00:00:00.0459323
深さ制限=3で解を検証中。経過時間=00:00:00.0541700
深さ制限=4で解を検証中。経過時間=00:00:00.1112230
深さ制限=5で解を検証中。経過時間=00:00:00.7604159
深さ制限=6で解を検証中。経過時間=00:00:09.4686282
(回転除外前の)解候補1を発見。経過時間=00:00:51.1547917
(回転除外前の)解候補2を発見。経過時間=00:00:51.1550065
(回転除外前の)解候補3を発見。経過時間=00:00:51.1595565
(回転除外前の)解候補4を発見。経過時間=00:01:31.0344426
(回転除外前の)解候補5を発見。経過時間=00:01:31.0346644
(回転除外前の)解候補6を発見。経過時間=00:01:32.3232329
(回転除外前の)解候補7を発見。経過時間=00:01:46.6074948
(回転除外前の)解候補8を発見。経過時間=00:01:53.1499738
(回転除外前の)解候補9を発見。経過時間=00:01:54.6507570
(回転除外後の)解1
□□龍□□□□□□
□□□□□□□□□
□□□□□□龍□□
□□□□□□□□□
□□□□龍□□□□
□□□□□□□□□
□□□□□□□□龍
□龍□□□□□□□
龍□□□□□□□□

(回転除外後の)解2
□□龍□□□□□□
□□□□□□□□□
□□□□□□龍□□
□□□□□□□□□
□□□□龍□□□□
□□□□□□□□□
□□□□□□□□龍
龍□□□□□□□□
□龍□□□□□□□

(回転除外後の)解3
□□龍□□□□□□
□□□□□□□□□
□□□□□□龍□□
□□□□□□□□□
□□龍□□□□□□
□□□□□□□□□
□□□□龍□□□龍
□□□□□□□□□
龍□□□□□□□□

(回転除外後の)解4
□龍□□□□□□□
□□□□□□□□□
□□□□□龍□□□
□□□□□□□□□
□□□龍□□□□□
□□□□□□□□□
□□□□□□□龍□
□□□□□□□□龍
龍□□□□□□□□

(回転除外後の)解5
□龍□□□□□□□
□□□□□□□□□
□□□□□龍□□□
□□□□□□□□□
□□□龍□□□□□
□□□□□□□□□
□□□□□□□龍□
龍□□□□□□□□
□□□□□□□□龍

(回転除外後の)解6
□龍□□□□□□□
龍□□□□□□□□
□□龍□□□□□□
□□□□□□□□□
□□□□□□龍□□
□□□□□□□□□
□□□□龍□□□□
□□□□□□□□□
□□□□□□□□龍

解は6通り。経過時間=00:02:17.4108075


解説

下限値枝切り付きの反復深化深さ優先探索で解いてます。