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

Cマガ電脳クラブ(第133回) 見た目は月並みですが

問題

英語の1月から12月までの12単語を,Fig.1のように正方形のマス目に配置する。
配置する際の条件は以下の4つ。
 (1) 1マスに1文字を書く
 (2) 各単語は左から右,または上から下に向かって読めるように配置する。ナナメは考えない
 (3) 各単語は,必ず1つ以上のほかの単語とつながっている。
      ここでいう「つながっている」とは,
      2つの単語が同じ文字の部分でオーバーラップして配置されている状態のことで,
      単に隣接して配置しただけではつながったことにならない
 (4) 全体が1つのつながりでできている。一部が環になっていてもかまわない

Fig.1は上記の条件を満たしており,この外接長方形の面積は120 (=8×15) となっている。
上記条件を満たしつつもっと上手に12単語を配置して,
外接長方形の面積をできるだけ小さくしたい。最小面積はいくつか。

そのときの配置は1つ示せばよいことにする。
なお,英語名はFig.1に登場している単語を使い,省略形は不可である。

Fig.1 パズルの解の例
■■■■■F■■■■■D■■■
■■■■SEPTEMBER■■
■MAY■B■■■■■C■■■
JANUARY■■■■E■■■
UR■■PU■NOVEMBER
NC■■RAUGUSTB■■■
EH■■IROCTOBER■■
■■JULY■■■■■R■■■


ソース

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

class Program
{
    static Dictionary<int, string> mMonthNameDict = new Dictionary<int, string>();

    //月名のDictを初期化
    static void DeriveMonthNameDict()
    {
        mMonthNameDict.Add(1, "JANUARY");
        mMonthNameDict.Add(2, "FEBRUARY");
        mMonthNameDict.Add(3, "MARCH");
        mMonthNameDict.Add(4, "APRIL");
        mMonthNameDict.Add(5, "MAY");
        mMonthNameDict.Add(6, "JUNE");
        mMonthNameDict.Add(7, "JULY");
        mMonthNameDict.Add(8, "AUGUST");
        mMonthNameDict.Add(9, "SEPTEMBER");
        mMonthNameDict.Add(10, "OCTOBER");
        mMonthNameDict.Add(11, "NOVEMBER");
        mMonthNameDict.Add(12, "DECEMBER");
    }

    static int UB_X;
    static int UB_Y;

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

    static void Main()
    {
        //月名のDictを初期化
        DeriveMonthNameDict();

        int StaMenseki =
            mMonthNameDict.Values.SelectMany(A => A.ToCharArray()).Distinct().Count();

        //面積でループ
        for (int Menseki = StaMenseki; ; Menseki++) {
            //最低2つは、横置きするので横幅の最低は4
            for (int X = 4; X <= Menseki; X++) {
                for (int Y = X; Y <= Menseki; Y++) {
                    if (X * Y != Menseki) continue;

                    //SEPTEMBERが9文字なので、縦が9未満なら不適
                    if (Y < 9) continue;

                    UB_X = X - 1;
                    UB_Y = Y - 1;

                    Console.WriteLine("面積={0,2}(横{1,2}、縦{2,2})の長方形を判定中。経過時間={3}"
                        , Menseki, X, Y, sw.Elapsed);
                    if (CanShikitume()) return;
                }
            }
        }
    }

    struct JyoutaiDef
    {
        internal char[,] BanArr;
        internal HashSet<int> UsedMonthSet; //使用済の月のSet
        internal int CanSetMinY; //次の配置での最小のY座標
    }

    //敷き詰め可能かを返す
    static bool CanShikitume()
    {
        var stk = new Stack<JyoutaiDef>();
        JyoutaiDef WillPush;
        WillPush.BanArr = new char[UB_X + 1, UB_Y + 1];

        for (int X = 0; X <= UB_X; X++) {
            for (int Y = 0; Y <= UB_Y; Y++) {
                WillPush.BanArr[X, Y] = '*';
            }
        }
        WillPush.UsedMonthSet = new HashSet<int>();
        WillPush.CanSetMinY = 0;
        stk.Push(WillPush);

        var VisitedSet = new HashSet<string>();
        VisitedSet.Add(BanToStr(WillPush.BanArr));

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

            //クリア判定
            if (Popped.UsedMonthSet.Count == 12) {
                Console.WriteLine("解を発見。経過時間={0}", sw.Elapsed);
                PrintBan(Popped.BanArr);
                return true;
            }

            for (int Y = 0; Y <= Popped.CanSetMinY; Y++) {
                for (int X = 0; X <= UB_X; X++) {
                    foreach (var EachPair in mMonthNameDict) {
                        if (Popped.UsedMonthSet.Contains(EachPair.Key)) continue;
                        PushSyori(Popped, X, Y, EachPair, VisitedSet, stk);
                    }
                }
            }
        }
        return false;
    }

    //Push処理
    static void PushSyori(JyoutaiDef pPopped, int pX, int pY, KeyValuePair<int, string> pEachPair,
                          HashSet<string> pVisitedSet, Stack<JyoutaiDef> pStk)
    {
        bool NeedConn = (pPopped.UsedMonthSet.Count > 0);

        //縦に置く場合
        if (CanSetTate(pPopped.BanArr, pX, pY, pEachPair.Value, NeedConn)) {
            //端に置けない月かを判定
            bool IsOK = true;
            if (pX == 0) {
                if (pEachPair.Value == "APRIL") IsOK = false;
                if (pEachPair.Value == "OCTOBER") IsOK = false;
            }
            if (pX == UB_X) {
                if (pEachPair.Value == "MAY") IsOK = false;
                if (pEachPair.Value == "JUNE") IsOK = false;
                if (pEachPair.Value == "AUGUST") IsOK = false;
            }
            if (IsOK) {
                JyoutaiDef WillPush;
                WillPush.BanArr = (char[,])pPopped.BanArr.Clone();
                WillPush.UsedMonthSet = new HashSet<int>(pPopped.UsedMonthSet) { pEachPair.Key };
                WillPush.CanSetMinY = pPopped.CanSetMinY;
                for (int I = 0; I <= pEachPair.Value.Length - 1; I++) {
                    WillPush.BanArr[pX, pY + I] = pEachPair.Value[I];
                    if (WillPush.CanSetMinY < pY + I)
                        WillPush.CanSetMinY = pY + I;
                }
                if (pVisitedSet.Add(BanToStr(WillPush.BanArr)))
                    pStk.Push(WillPush);
            }
        }

        //横に置く場合
        if (CanSetYoko(pPopped.BanArr, pX, pY, pEachPair.Value, NeedConn)) {
            //端に置けない月かを判定
            bool IsOK = true;
            if (pY == 0) {
                if (pEachPair.Value == "APRIL") IsOK = false;
                if (pEachPair.Value == "OCTOBER") IsOK = false;
            }
            if (pY == UB_Y) {
                if (pEachPair.Value == "MAY") IsOK = false;
                if (pEachPair.Value == "JUNE") IsOK = false;
                if (pEachPair.Value == "AUGUST") IsOK = false;
            }
            if (IsOK) {
                JyoutaiDef WillPush;
                WillPush.BanArr = (char[,])pPopped.BanArr.Clone();
                WillPush.UsedMonthSet = new HashSet<int>(pPopped.UsedMonthSet) { pEachPair.Key };
                WillPush.CanSetMinY = pPopped.CanSetMinY;
                for (int I = 0; I <= pEachPair.Value.Length - 1; I++) {
                    WillPush.BanArr[pX + I, pY] = pEachPair.Value[I];
                }
                if (pVisitedSet.Add(BanToStr(WillPush.BanArr)))
                    pStk.Push(WillPush);
            }
        }
    }

    //縦に置けるかのチェック
    static bool CanSetTate(char[,] pBanArr, int pCurrX, int pCurrY, string pMonthName, bool pNeedConn)
    {
        if (pCurrY + pMonthName.Length - 1 > UB_Y) return false;

        bool HasConn = false;
        for (int I = 0; I <= pMonthName.Length - 1; I++) {
            char CurrChar = pBanArr[pCurrX, pCurrY + I];
            if (CurrChar == '*') continue;
            else if (CurrChar == pMonthName[I]) HasConn = true;
            else return false;
        }
        return pNeedConn ? HasConn : true;
    }

    //横に置けるかのチェック
    static bool CanSetYoko(char[,] pBanArr, int pCurrX, int pCurrY, string pMonthName, bool pNeedConn)
    {
        if (pCurrX + pMonthName.Length - 1 > UB_X) return false;

        bool HasConn = false;
        for (int I = 0; I <= pMonthName.Length - 1; I++) {
            char CurrChar = pBanArr[pCurrX + I, pCurrY];
            if (CurrChar == '*') continue;
            else if (CurrChar == pMonthName[I]) HasConn = true;
            else return false;
        }
        return pNeedConn ? HasConn : true;
    }

    //盤面をString型に変換
    static string BanToStr(char[,] pBanArr)
    {
        var sb = new System.Text.StringBuilder();
        for (int X = 0; X <= UB_X; X++) {
            for (int Y = 0; Y <= UB_Y; Y++) {
                sb.Append(pBanArr[X, Y]);
            }
        }
        return sb.ToString();
    }

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


実行結果

省略
面積=77(横 7、縦11)の長方形を判定中。経過時間=00:01:35.2565027
面積=78(横 6、縦13)の長方形を判定中。経過時間=00:02:05.8691836
面積=80(横 4、縦20)の長方形を判定中。経過時間=00:02:15.1825623
面積=80(横 5、縦16)の長方形を判定中。経過時間=00:02:15.3794688
面積=80(横 8、縦10)の長方形を判定中。経過時間=00:02:19.0068503
解を発見。経過時間=00:02:19.1290261
****A*SD
F**JUNEE
E**JGNPC
B**UUOTE
R*MLSVEM
UMAYTEMB
APRILMBE
ROCTOBER
Y*H**ER*
JANUARY*


解説

面積をループさせつつ、
再訪防止のメモ化探索で解いてます。