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

23-11 15ゲーム

問題

ハナヤマのWOODY STYLE 15ゲームを解きます。



ソース

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

class Program
{
    const int UB = 4 - 1;
    static int[,] QuestionArr;

    static int DepthLimit;
    static int FirstDepthLimit;

    static string PathToCorrectTo2 = "";
    static string PathToCorrectTo4 = "";
    static string PathToCorrectTo8 = "";

    static void Main()
    {
        var sw = System.Diagnostics.Stopwatch.StartNew();

        int[,] Q0 = new int[,] {{ 1, 2, 3, 4,},  //1
                                { 0, 5, 6, 7,},  //2
                                { 9,10,11, 8,},  //3
                                {13,14,15,12,}}; //4;

        //1〜15 縦配列
        int[,] Q1 = new int[,]{{ 1, 5, 9,13},
                               { 2, 6,10,14},
                               { 3, 7,11,15},
                               { 4, 8,12, 0}};

        //大小逆転配列
        int[,] Q3 = new int[,]{{15,14,13,12},
                               {11,10, 9, 8},
                               { 7, 6, 5, 4},
                               { 3, 2, 1, 0}};

        //上り下り配列
        int[,] Q4 = new int[,]{{ 4, 5,12,13},
                               { 3, 6,11,14},
                               { 2, 7,10,15},
                               { 1, 8, 9, 0}};

        //対角線配列
        int[,] Q5 = new int[,]{{ 7,11,14, 0},
                               { 4, 8,12,15},
                               { 2, 5, 9,13},
                               { 1, 3, 6,10}};

        //奇数偶数交互線配列
        int[,] Q6 = new int[,]{{ 1, 3, 5, 7},
                               { 2, 4, 6, 8},
                               { 9,11,13,15},
                               {10,12,14, 0}};

        //ラセン1配列
        int[,] Q7 = new int[,]{{ 7, 8, 9,10},
                               { 6, 1, 2,11},
                               { 5, 4, 3,12},
                               { 0,15,14,13}};

        //ラセン2配列
        int[,] Q8 = new int[,]{{ 1, 2, 3, 4},
                               {12,13,14, 5},
                               {11, 0,15, 6},
                               {10, 9, 8, 7}};

        int[,] wkArr = Q3;

        QuestionArr = new int[UB + 1, UB + 1];

        for (int Y = 0; Y <= UB; Y++) {
            for (int X = 0; X <= UB; X++) {
                QuestionArr[X, Y] = wkArr[Y, X];
            }
        }

        bool HasEvenKai = true; //解の手数が偶数か?
        for (int Y = 0; Y <= UB; Y++) {
            for (int X = 0; X <= UB; X++) {
                if (QuestionArr[X, Y] == 0) {
                    HasEvenKai = ((X + Y) % 2 == 0);
                }
            }
        }

        FirstDepthLimit = (HasEvenKai ? 2 : 1);
        for (DepthLimit = FirstDepthLimit; DepthLimit <= 80; DepthLimit += 2) {
            bool FoundTo2 = (PathToCorrectTo2 != "");
            bool FoundTo4 = (PathToCorrectTo4 != "");
            bool FoundTo8 = (PathToCorrectTo8 != "");

            Console.WriteLine("手数上限={0}で探索中。FoundTo2={1},FoundTo4={2},FoundTo8={3}",
                DepthLimit, FoundTo2, FoundTo4, FoundTo8);

            bool FoundSamLoyd;
            string LastMoveLog = ExecDFS(out FoundSamLoyd);

            //文字を5文字ずつ出力
            Action<string> OutString5MojiZutu = pStr =>
            {
                for (int I = 0; I <= pStr.Length - 1; I++) {
                    Console.Write(pStr[I]);
                    if (I < pStr.Length - 1 && I % 5 == 4) Console.WriteLine();
                };
                Console.WriteLine();
            };

            if (LastMoveLog != "") {
                int SumTesuu = PathToCorrectTo2.Length + PathToCorrectTo4.Length
                             + PathToCorrectTo8.Length + LastMoveLog.Length;

                if (FoundSamLoyd) {
                    Console.WriteLine("この問題の解はありません。経過時間{0}", sw.Elapsed);
                    Console.WriteLine("サムロイドの懸賞問題の形までの{0}手を表示します。", SumTesuu);
                }
                else {
                    Console.WriteLine("{0}手の解を発見。経過時間{1}", SumTesuu, sw.Elapsed);
                }
                if (PathToCorrectTo2 != "") {
                    Console.WriteLine("12までの手順は");
                    OutString5MojiZutu(PathToCorrectTo2);
                }
                if (PathToCorrectTo4 != "") {
                    Console.WriteLine("1234までの手順は");
                    OutString5MojiZutu(PathToCorrectTo4);
                }
                if (PathToCorrectTo8 != "") {
                    Console.WriteLine("12345678までの手順は");
                    OutString5MojiZutu(PathToCorrectTo8);
                }
                Console.WriteLine("以降の手順は");
                OutString5MojiZutu(LastMoveLog);
                break;
            }
        }
    }

    struct JyoutaiDef
    {
        internal int Level;
        internal int[,] Ban;
        internal string MoveLog;
        internal char PreMove;
        internal bool CorrectTo2;
        internal bool CorrectTo4;
        internal bool CorrectTo8;
        internal int[] NeedTesuuArr;
    }

    //反復深化深さ優先探索を行う
    static string ExecDFS(out bool pFoundSamLoyd)
    {
        pFoundSamLoyd = false;

        var stk = new Stack<JyoutaiDef>();
        JyoutaiDef WillPush;
        WillPush.Level = 0;
        WillPush.Ban = QuestionArr;
        WillPush.MoveLog = "";
        WillPush.PreMove = '\0';
        WillPush.CorrectTo2 = IsCorrectPlace(QuestionArr, 2);
        WillPush.CorrectTo4 = IsCorrectPlace(QuestionArr, 4);
        WillPush.CorrectTo8 = IsCorrectPlace(QuestionArr, 8);
        WillPush.NeedTesuuArr = new int[15];
        for (int I = 1; I <= 15; I++) {
            WillPush.NeedTesuuArr[I - 1] = NeedMinTesuuForNum(QuestionArr, I);
        }

        stk.Push(WillPush);

        //盤面に対する最少手数のDict
        var SortedMinTesuuDict = new SortedDictionary<ulong, int>();

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

            if (IsCorrectPlace(Popped.Ban, 15)) return Popped.MoveLog;

            //サムロイドの懸賞問題の形の判定
            if (Popped.CorrectTo8 && IsSamLoydQuestion(Popped.Ban)) {
                pFoundSamLoyd = true;
                return Popped.MoveLog;
            }

            int X = 0, Y = 0;
            while (Popped.Ban[X, Y] != 0) {
                if (++X > UB) {
                    Y++; X = 0;
                }
            }

            WillPush.Level = Popped.Level + 1;

            bool WillQuestionReWrite = false;

            Action<int, int, char> PushSyori = (pFromX, pFromY, pMoveLog) =>
            {
                WillQuestionReWrite = false;
                int MoveNum = Popped.Ban[pFromX, pFromY];

                if (Popped.CorrectTo2 && MoveNum <= 2) return;
                if (Popped.CorrectTo4 && MoveNum <= 4) return;
                if (Popped.CorrectTo8 && MoveNum <= 8) return;

                WillPush.Ban = (int[,])Popped.Ban.Clone();
                WillPush.Ban[X, Y] = MoveNum;
                WillPush.Ban[pFromX, pFromY] = 0;

                WillPush.NeedTesuuArr = (int[])Popped.NeedTesuuArr.Clone();
                WillPush.NeedTesuuArr[MoveNum - 1] = NeedMinTesuuForNum(WillPush.Ban, MoveNum);

                if (WillPush.Level + WillPush.NeedTesuuArr.Sum() > DepthLimit) return;

                ulong BanULong = BanToULong(WillPush.Ban);

                //当該盤面に、少ない手数か等しい手数で、到達済なら枝切り
                int MinTesuu;
                if (SortedMinTesuuDict.TryGetValue(BanULong, out MinTesuu)) {
                    if (MinTesuu <= WillPush.Level) return;
                }
                SortedMinTesuuDict[BanULong] = WillPush.Level;

                WillPush.MoveLog = Popped.MoveLog + pMoveLog;
                WillPush.PreMove = pMoveLog;

                //12まで正しい位置
                //1234まで正しい位置
                //12345678まで正しい位置
                //のいずれかだったら問題を差し替え
                WillPush.CorrectTo2 = Popped.CorrectTo2;
                WillPush.CorrectTo4 = Popped.CorrectTo4;
                WillPush.CorrectTo8 = Popped.CorrectTo8;

                if (WillPush.CorrectTo8 == false) {
                    if (WillPush.CorrectTo8 = IsCorrectPlace(WillPush.Ban, 8)) {
                        PathToCorrectTo8 = WillPush.MoveLog;
                        WillPush.CorrectTo4 = WillPush.CorrectTo2 = true;
                        WillQuestionReWrite = true;
                    }
                }
                if (WillPush.CorrectTo4 == false) {
                    if (WillPush.CorrectTo4 = IsCorrectPlace(WillPush.Ban, 4)) {
                        PathToCorrectTo4 = WillPush.MoveLog;
                        WillPush.CorrectTo2 = true;
                        WillQuestionReWrite = true;
                    }
                }
                if (WillPush.CorrectTo2 == false) {
                    if (WillPush.CorrectTo2 = IsCorrectPlace(WillPush.Ban, 2)) {
                        PathToCorrectTo2 = WillPush.MoveLog;
                        WillQuestionReWrite = true;
                    }
                }

                if (WillQuestionReWrite) { //問題の差し替え
                    QuestionArr = WillPush.Ban;
                    DepthLimit = FirstDepthLimit;
                    return;
                }

                stk.Push(WillPush);
            };

            //左の数字を、右に移動
            if (X > 0 && Popped.PreMove != '←') PushSyori(X - 1, Y, '→');
            if (WillQuestionReWrite) return "";

            //右の数字を、左に移動
            if (X < UB && Popped.PreMove != '→') PushSyori(X + 1, Y, '←');
            if (WillQuestionReWrite) return "";

            //上の数字を、下に移動
            if (Y > 0 && Popped.PreMove != '↑') PushSyori(X, Y - 1, '↓');
            if (WillQuestionReWrite) return "";

            //下の数字を、上に移動
            if (Y < UB && Popped.PreMove != '↓') PushSyori(X, Y + 1, '↑');
            if (WillQuestionReWrite) return "";
        }
        return "";
    }

    //盤面を符号なしLong型で表現
    static ulong BanToULong(int[,] pBan)
    {
        var sb = new System.Text.StringBuilder();
        for (int Y = 0; Y <= UB; Y++) {
            for (int X = 0; X <= UB; X++) {
                if (pBan[X, Y] == 10) sb.Append('A');
                else if (pBan[X, Y] == 11) sb.Append('B');
                else if (pBan[X, Y] == 12) sb.Append('C');
                else if (pBan[X, Y] == 13) sb.Append('D');
                else if (pBan[X, Y] == 14) sb.Append('E');
                else if (pBan[X, Y] == 15) sb.Append('F');
                else sb.Append(pBan[X, Y]);
            }
        }
        return Convert.ToUInt64(sb.ToString(), 16);
    }

    //1から指定した値までの位置が正しい位置かを返す
    static bool IsCorrectPlace(int[,] pBan, int pToNum)
    {
        int MustNumber = 0;
        for (int Y = 0; Y <= UB; Y++) {
            for (int X = 0; X <= UB; X++) {
                ++MustNumber;
                if (pBan[X, Y] != MustNumber) return false;
                if (MustNumber == pToNum) return true;
            }
        }
        return true;
    }

    //サムロイドの懸賞問題の形かを返す
    static bool IsSamLoydQuestion(int[,] pBan)
    {
        if (IsCorrectPlace(pBan, 13) == false) return false;
        if (pBan[1, 3] != 15) return false;
        if (pBan[2, 3] != 14) return false;
        return true;
    }

    //指定した数字が正しい位置に到達するのに必要な、最少手数を求める
    static int NeedMinTesuuForNum(int[,] pBan, int pTargetNum)
    {
        int X1 = 0, Y1 = 0;
        if (pTargetNum == 1) { X1 = 0; Y1 = 0; }
        if (pTargetNum == 2) { X1 = 1; Y1 = 0; }
        if (pTargetNum == 3) { X1 = 2; Y1 = 0; }
        if (pTargetNum == 4) { X1 = 3; Y1 = 0; }
        if (pTargetNum == 5) { X1 = 0; Y1 = 1; }
        if (pTargetNum == 6) { X1 = 1; Y1 = 1; }
        if (pTargetNum == 7) { X1 = 2; Y1 = 1; }
        if (pTargetNum == 8) { X1 = 3; Y1 = 1; }
        if (pTargetNum == 9) { X1 = 0; Y1 = 2; }
        if (pTargetNum == 10) { X1 = 1; Y1 = 2; }
        if (pTargetNum == 11) { X1 = 2; Y1 = 2; }
        if (pTargetNum == 12) { X1 = 3; Y1 = 2; }
        if (pTargetNum == 13) { X1 = 0; Y1 = 3; }
        if (pTargetNum == 14) { X1 = 1; Y1 = 3; }
        if (pTargetNum == 15) { X1 = 2; Y1 = 3; }

        int X2, Y2;
        SearchNumXY(pBan, pTargetNum, out X2, out Y2);
        return Math.Abs(X1 - X2) + Math.Abs(Y1 - Y2);
    }

    //指定した数字の存在する座標を返す
    static void SearchNumXY(int[,] pBan, int pTargetNum, out int pX, out int pY)
    {
        pX = pY = -1;
        for (int Y = 0; Y <= UB; Y++) {
            for (int X = 0; X <= UB; X++) {
                if (pBan[X, Y] == pTargetNum) {
                    pX = X;
                    pY = Y;
                    return;
                }
            }
        }
    }
}


実行結果

省略
手数上限=18で探索中。FoundTo2=True,FoundTo4=True,FoundTo8=True
手数上限=20で探索中。FoundTo2=True,FoundTo4=True,FoundTo8=True
手数上限=22で探索中。FoundTo2=True,FoundTo4=True,FoundTo8=True
手数上限=24で探索中。FoundTo2=True,FoundTo4=True,FoundTo8=True
96手の解を発見。経過時間00:00:11.7891095
12までの手順は
↓→↓↓→
↑→↑←←
↓↓→↑
1234までの手順は
→↑↑←↓
→↓←↑←
↓→→↑←
←←↓↓→
↑→↑↑←
←↓↓↓→
↑
12345678までの手順は
↑↑→→↓
↓←↑↑←
↓→→↑←
←←↓→↓
←↑→→↓
←↑
以降の手順は
↑→→↓←
←↑←↓→
→↑←↓→
→↑←←←
↓→↑←


解説

幅優先探索ではなく、反復深化深さ優先探索を使い、
下限値枝切りと組み合わせてます。

分割統治法として、
12までが正しい位置、1234までが正しい位置、12345678までが正しい位置
のいずれかの手順を求めたら問題の差し替えを行ってます。

この分割統治法により探索木は4本になりますが、
1つの探索木で探索するよりも、探索木の高さを低く抑えることができます。

なお、大小逆順配列は、サムロイドの懸賞問題の形になりますので解くことができません。

8-9 15パズル