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

Cマガ電脳クラブ(第049回) ツルみオセロ

問題

今回は皆さんご存じのゲーム「オセロ」のルールを使ったパズルだ。
Fig.1の状態から始めて、黒先手で黒白交互にコマを置く。

そして自分と同じ色のコマで相手のコマをはさむのだが、縦・横・斜めどのようにはさんでもよい。
はさまれたコマはすべて自分と同じ色のコマに変わる。ただし、相手のコマをはさむことができない場所には置けない。

「オセロ」なら最終的に数の多い色のほうが勝ちとなるが、このパズルの目的は違う。
盤上のコマを、白か黒のどちらか1色にしてしまおうというものだ。
Fig.1の状態から、すべてのコマを白か黒にするための最少手数を求めてほしい。
勝ち負けは関係ないので、たとえば白が自分に不利になるような位置にコマを置く、
つまり黒に協力するようなゲーム進行になってもかまわない。Fig.2の例では11手ですべて黒になっている。

          


ソース

using System;
using System.Collections.Generic;

class Program
{
    const int UB = 8 - 1;

    struct JyoutaiDef
    {
        internal int Level;
        internal char[,] BanArr;
        internal List<char[,]> BanArrLogList;
    }

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

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

            List<Char[,]> AnswerBanArrLog = ExecDFS(Depth);

            if (AnswerBanArrLog.Count == 0) continue;

            for (int I = 0; I <= AnswerBanArrLog.Count - 1; I++) {
                Console.WriteLine("{0}手目", I);
                PrintBan(AnswerBanArrLog[I]);
            }
            break;
        }
    }

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

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

        WillPush.Level = 0;
        WillPush.BanArr = new char[UB + 1, UB + 1];
        for (int Y = 0; Y <= UB; Y++) {
            for (int X = 0; X <= UB; X++) {
                WillPush.BanArr[X, Y] = '□';
            }
        }
        WillPush.BanArr[3, 3] = WillPush.BanArr[4, 4] = '○';
        WillPush.BanArr[3, 4] = WillPush.BanArr[4, 3] = '●';
        WillPush.BanArrLogList = new List<char[,]>() { WillPush.BanArr };
        stk.Push(WillPush);

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

            //クリア判定
            if (IsClear(Popped.BanArr))
                return Popped.BanArrLogList;

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

            //石を配置した盤面を返す
            List<Char[,]> BanList = SetStone(Popped.Level, Popped.BanArr);

            WillPush.Level = Popped.Level + 1;

            //オセロの初手は回転させると同じなので、1手に限定する
            if (WillPush.Level == 1) BanList.RemoveRange(0, BanList.Count - 1);

            //パスの場合
            if (BanList.Count == 0) {
                WillPush.BanArr = Popped.BanArr;
                WillPush.BanArrLogList = new List<char[,]>(Popped.BanArrLogList);
                WillPush.BanArrLogList.Add(WillPush.BanArr);
                stk.Push(WillPush);
            }
            else {
                BanList.ForEach(X =>
                {
                    WillPush.BanArr = X;
                    WillPush.BanArrLogList = new List<char[,]>(Popped.BanArrLogList);
                    WillPush.BanArrLogList.Add(X);
                    stk.Push(WillPush);
                });
            }
        }
        return WillReturnCharArrList;
    }

    //クリア判定
    static bool IsClear(char[,] pBanArr)
    {
        bool HasBlack = false;
        bool HasWhite = false;

        for (int Y = 0; Y <= UB; Y++) {
            for (int X = 0; X <= UB; X++) {
                if (pBanArr[X, Y] == '●') HasBlack = true;
                if (pBanArr[X, Y] == '○') HasWhite = true;
            }
        }
        return (HasBlack && HasWhite) == false;
    }

    //石を配置した盤面を返す
    static List<Char[,]> SetStone(int pLevel, char[,] pBanArr)
    {
        var WillReturnCharArrList = new List<char[,]>();

        //手番を求める
        bool IsKuroban = pLevel % 2 == 0;

        //配置候補の座標を求める
        for (int Y = 0; Y <= UB; Y++) {
            for (int X = 0; X <= UB; X++) {
                if (pBanArr[X, Y] != '□') continue;

                char[,] wkBanArr = (char[,])pBanArr.Clone();
                wkBanArr[X, Y] = IsKuroban ? '●' : '○';
                bool CanSet = false;
                CanSet |= ReverseStone(wkBanArr, X, Y, 0, -1);
                CanSet |= ReverseStone(wkBanArr, X, Y, 1, -1);
                CanSet |= ReverseStone(wkBanArr, X, Y, 1, 0);
                CanSet |= ReverseStone(wkBanArr, X, Y, 1, 1);
                CanSet |= ReverseStone(wkBanArr, X, Y, 0, 1);
                CanSet |= ReverseStone(wkBanArr, X, Y, -1, 1);
                CanSet |= ReverseStone(wkBanArr, X, Y, -1, 0);
                CanSet |= ReverseStone(wkBanArr, X, Y, -1, -1);
                if (CanSet == false) continue;

                WillReturnCharArrList.Add(wkBanArr);
            }
        }
        return WillReturnCharArrList;
    }

    //盤面と石を置いた座標とチェック方向を引数とし、
    //裏返せる石があれば裏返し、1枚でも裏返したらTrueを返す
    static bool ReverseStone(char[,] pBanArr, int pSetX, int pSetY, int pHoukouX, int pHoukouY)
    {
        char SetStone = pBanArr[pSetX, pSetY];
        char ReverseStone = SetStone == '●' ? '○' : '●';

        bool CanReverse = false;

        //裏返し可能かを判定
        for (int I = 1; I <= 7; I++) {
            int X = pSetX + pHoukouX * I;
            int Y = pSetY + pHoukouY * I;

            if (X < 0 || UB < X) return false;
            if (Y < 0 || UB < Y) return false;

            if (I == 1 && pBanArr[X, Y] != ReverseStone) return false;
            if (I >= 2 && pBanArr[X, Y] == '□') return false;

            if (I >= 2 && pBanArr[X, Y] == SetStone) {
                CanReverse = true;
                break;
            }
        }

        //裏返し処理
        if (CanReverse) {
            for (int I = 1; I <= 7; I++) {
                int X = pSetX + pHoukouX * I;
                int Y = pSetY + pHoukouY * I;

                if (pBanArr[X, Y] == SetStone) break;
                pBanArr[X, Y] = SetStone;
            }
            return true;
        }
        return false;
    }

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


実行結果

深さ制限=1で解を検証中。経過時間=00:00:00.0033683
深さ制限=2で解を検証中。経過時間=00:00:00.0241209
深さ制限=3で解を検証中。経過時間=00:00:00.0248771
深さ制限=4で解を検証中。経過時間=00:00:00.0273559
深さ制限=5で解を検証中。経過時間=00:00:00.0351100
深さ制限=6で解を検証中。経過時間=00:00:00.0755900
深さ制限=7で解を検証中。経過時間=00:00:00.3471321
深さ制限=8で解を検証中。経過時間=00:00:01.9249806
0手目
□□□□□□□□
□□□□□□□□
□□□□□□□□
□□□○●□□□
□□□●○□□□
□□□□□□□□
□□□□□□□□
□□□□□□□□

1手目
□□□□□□□□
□□□□□□□□
□□□□□□□□
□□□○●□□□
□□□●●□□□
□□□□●□□□
□□□□□□□□
□□□□□□□□

2手目
□□□□□□□□
□□□□□□□□
□□□□□□□□
□□□○●□□□
□□□●○□□□
□□□□●○□□
□□□□□□□□
□□□□□□□□

3手目
□□□□□□□□
□□□□□□□□
□□□□□□□□
□□□○●□□□
□□□●○□□□
□□□□●●●□
□□□□□□□□
□□□□□□□□

4手目
□□□□□□□□
□□□□□□□□
□□□□□□□□
□□□○●□□□
□□□●○□□□
□□□□○●●□
□□□□○□□□
□□□□□□□□

5手目
□□□□□□□□
□□□□□□□□
□□□□□□□□
□□□○●□□□
□□□●○□□□
□□□□○●●□
□□□□●□□□
□□□●□□□□

6手目
□□□□□□□□
□□□□□□□□
□□□□○□□□
□□□○○□□□
□□□●○□□□
□□□□○●●□
□□□□●□□□
□□□●□□□□

7手目
□□□□□□□□
□□□□●□□□
□□□□●□□□
□□□○●□□□
□□□●●□□□
□□□□●●●□
□□□□●□□□
□□□●□□□□

8手目
□□□□□□□□
□□□□●□□□
□□□□●□□□
□□□○●□□□
□□□○●□□□
□□□○●●●□
□□□□●□□□
□□□●□□□□

9手目
□□□□□□□□
□□□□●□□□
□□□□●□□□
□□□●●□□□
□□●●●□□□
□□□●●●●□
□□□□●□□□
□□□●□□□□


解説

反復深化深さ優先探索で解いてます。