2
0

「Nテトリス問題」 - ブロックがN個の場合のミノの種類の総数を求めたい(C#)

Posted at

Nテトリス問題とは?

さっき俺が名付けた問題です。Nクイーン問題をパクりました。
(↓Nクイーン問題って何?って人はこれ見たら分かると思う)
N-queens - とうきょうこうぎょう

いわゆる、あの落ちものゲーのテトリスで使われる正方形が集まってできた物体、ミノっていう名前なんですけど、あれは全部で7種類あるんすよね。
こういうやつ↓
qiita_2_1.png

で、このミノを構成しているブロックって4つですよね。
qiita_2_2.png

このブロックの数が5とか6の時はどんな形があって、何通りあるんやろって思ったんで、考えてみました。

qiita_2_3.png

やりたいこと

Nに自然数を代入して、実行するとブロック数がN個のミノの形を全て求めて、その形と個数を出力できればOK。
※縦長に出力するものとする。
※回転させて一致する図形はいずれか1つのみ出力するものとする。
表にまとめるとこんな感じ。

引数 ミノの形状(省略) ミノの種類の個数
1 $1$
2 $1$
3 $2$
4 $7$
5 $18$
6 $60$
7 $196$
8 $704$
9 $2,500$
10 $9,189$

というわけで、こんな感じで実装してみました。

実装

Program.cs
using System.Diagnostics;

namespace CreatingMino {
    internal class Program {
        
        //ブロック数1のミノ。
        static Mino firstMino = new Mino(new List<int[]> { new int[] { 0, 0 } });

        //全ての結果を保持する。
        static List<Mino> minoList = new List<Mino> { firstMino };

        //ブロックが(i + 1)個のミノを保持する。
        static List<Mino> nextMinoList = new List<Mino> { };

        //出力するミノを保持する。
        static List<Mino> printMinoList = new List<Mino> { };

        //今回の使用ブロック数。
        const int N = 5;

        static void Main(string[] args) {

            var sw = Stopwatch.StartNew();

            CreateMino(minoList);

            //ブロック数がNのミノのみ出力する。
            foreach(var mino in minoList) {
                if(mino.Blocks.Count() == N) {
                    printMinoList.Add(mino);
                }
            }

            Mino.PrintMino(printMinoList);

            sw.Stop();

            Console.WriteLine("経過時間: " + sw.ElapsedMilliseconds + " ms");

        }

        //ブロック数がi個の全てのミノの形状と、その個数を求める。
        private static void CreateMino(List<Mino> oMinoList) {

            nextMinoList = new List<Mino>();

            foreach(var oMino in oMinoList) {
                var testBlocks = new List<int[]>();

                //ブロックの外側に1マス分の空のブロックを追加し、枠を作る。
                foreach(var block in oMino.Blocks) {
                    testBlocks.Add(new int[] { block[0] + 1, block[1] + 1 });
                }

                //AddMino()をtestBlocksに対して行うと反復処理が行えなくなるので、
                //"testBlocks"の中身を"tmpBlocks"にコピーしておく。
                var tmpBlocks = testBlocks.Select(b => b.ToArray()).ToList();
                var newBlock = new int[2];
                var settable = true;

                //上→右→下→左の順に、ブロックの隣に新たなブロックを置けるか確認する。
                foreach(var tBlock in testBlocks) {
                    foreach(var direction in Mino.directionMap.Keys) {
                        //tBlockの1つ上、右、下、左隣の座標を求める。
                        newBlock = Mino.GetNewBlock(tBlock, direction);

                        //tmpBlockに、既にブロックが置かれていなければ、その形状をminoListに追加する。
                        foreach(var block in testBlocks) {
                            if(block[0] == newBlock[0] && block[1] == newBlock[1]) {
                                settable = false;
                            }
                        }
                        if(settable == true) {
                            AddMino(tmpBlocks, newBlock);
                            tmpBlocks.Remove(newBlock);
                        }
                        settable = true;
                    }
                }
            }

            minoList.AddRange(nextMinoList);

            //i個のブロックのミノを使用して(i + 1)個のミノを生成して、
            //それをN個まで繰り返した後、処理が終了する。
            if(nextMinoList.First().Blocks.Count() < N) { CreateMino(nextMinoList); }
        }

        //適切な形状であれば、ミノを生成しリストに追加する。
        private static void AddMino(List<int[]> blocks, int[] nBlock) {
            blocks.Add(nBlock);

            var blocksWithNoSpace = RemoveSpaces(blocks);

            var height = blocksWithNoSpace.Max(y => y[0]) - blocksWithNoSpace.Min(y => y[0]) + 1;
            var width = blocksWithNoSpace.Max(x => x[1]) - blocksWithNoSpace.Min(x => x[1]) + 1;

            //縦より横の長さの方が長い場合
            if(height < width) {
                //形状を時計回りに90°回転させて、長さを(縦)≥(横)にする。
                var tmpBlocks = new List<int[]>();
                foreach(var block in blocksWithNoSpace) {
                    tmpBlocks.Add(new int[] { block[1], height - block[0] });
                }
                blocksWithNoSpace = RemoveSpaces(tmpBlocks);
                (height, width) = (width, height);
            }

            //既に生成されているミノと比較して、重複がない場合のみミノを生成し、リストに追加する。
            if(CheckNoDuplication(blocksWithNoSpace, height, width)) {
                nextMinoList.Add(new Mino(blocksWithNoSpace));
            };
        }

        //CreateMino()で加えられた外枠を取り除き、隙間を取り除いた形状にして返す。
        private static List<int[]> RemoveSpaces(List<int[]> blocks) {
            blocks = blocks.OrderBy(y => y[0]).ThenBy(x => x[1]).ToList();

            //xMin(各座標の中で最小のx座標の値)を算出し、左端に何マスの隙間があるか計算する。 yMin(最小のy座標の値)も同様。
            var xMin = blocks.Min(x => x[1]);
            var yMin = blocks.Min(y => y[0]);

            var blocksWithNoSpace = new List<int[]>();

            foreach(var block in blocks) {
                blocksWithNoSpace.Add(new int[] { block[0] - yMin, block[1] - xMin });
            }

            return blocksWithNoSpace;
        }

        //重複しているミノの生成を防ぐ。
        //与えられた形状に重複が無かった場合、trueを返す。
        private static bool CheckNoDuplication(List<int[]> blocks, int height, int width) {
            bool sameFlg;
            foreach(var mino in nextMinoList) {
                //縦と横の長さが同じ場合のみ重複をチェックする。
                if(height == mino.Height && width == mino.Width) {
                    //同じかどうか比較
                    var rBlocks = blocks.Select(b => b.ToArray()).ToList();
                    var tmpBlocks = new List<int[]>();
                    for(var i = 0; i < 4; i++) {
                        var differ = i % 2 == 0 ? height : width;
                        //tmpBlocksに、rBlocksを90°回転させた座標を代入して、チェックを行う。
                        foreach(var rBlock in rBlocks) {
                            tmpBlocks.Add(new int[] { rBlock[1], differ - 1 - rBlock[0] });
                        }
                        tmpBlocks = tmpBlocks.OrderBy(y => y[0]).ThenBy(x => x[1]).ToList();
                        rBlocks = tmpBlocks.Select(b => b.ToArray()).ToList();
                        sameFlg = true;
                        //全ての座標が一致していた場合、重複していると判定される。
                        for(var j = 0; j < mino.Blocks.Count; j++) {
                            if(!Enumerable.SequenceEqual(tmpBlocks[j], mino.Blocks[j])) { sameFlg = false; }
                        }
                        if(sameFlg) { return false; }
                        tmpBlocks.Clear();
                    }
                }
            }
            return true;
        }
    }
}
mino.cs
using System.Text;

namespace CreatingMino {
    public class Mino {
        //ミノの形状を表現するクラス。Blocksには各ブロックの位置をxy平面上の格子点に対応付け、その座標(y座標, x座標)が入る。
        //原点の座標は(0, 0)とし、xは正の方向に、yは負の方向に進む。
        //Heightにはその形状の縦の長さ、Widthには横の長さが入る。
        //
        //  0  1  … x
        //0 ■  ■
        //1 ■  
        //2 ■
        //:
        //y
        //このような形状のデータは、[ {"Blocks" : "{0, 0}, {0, 1}, {1, 0}, {2, 0}", "Height" : "3", "Width" : "2"} ]となる。

        //ブロックの座標
        public List<int[]> Blocks { get; }
        //ミノの高さ
        public int Height => CalcHeight();
        //ミノの長さ
        public int Width => CalcWidth();

        public Mino(List<int[]> blocks) {
            Blocks = blocks;
        }

        //ミノの高さを求める。
        private int CalcHeight() {
            return Blocks.Max(y => y[0]) - Blocks.Min(y => y[0]) + 1;
        }

        //ミノの長さを求める。
        private int CalcWidth() {
            return Blocks.Max(x => x[1]) - Blocks.Min(x => x[1]) + 1;
        }

        //ブロックを追加する方向
        public static Dictionary<String, int[]> directionMap = new Dictionary<String, int[]>() {
            {"above", new int[]{ -1,  0 } },
            {"right", new int[]{  0,  1 } },
            {"below", new int[]{  1,  0 } },
            {"left" , new int[]{  0, -1 } }
        };

        //ブロックを1つ追加する。
        public static int[] GetNewBlock(int[] blocks, string direction) {
            return new int[] { blocks[0] + directionMap[direction][0], blocks[1] + directionMap[direction][1] };
        }

        //ミノを出力する。(N ≥ 9ではスクロールしきれず全パターンを表示できない。)
        public static void PrintMino(List<Mino> minoList) {

            var sbAllMinos = new StringBuilder();

            foreach(var mino in minoList) {

                //各ミノの縦×横の長さの、空文字が連続した文字列を生成する。
                var sbMino = new StringBuilder(new string(' ', mino.Height * mino.Width));

                //blockの座標から、何文字目を「■」に変換するかを求める。
                var target = 0;
                foreach(var block in mino.Blocks) {
                    target = block[0] * mino.Width + block[1];
                    sbMino.Remove(target, 1).Insert(target, "■");
                }
                //各行末ごとに改行する。
                for(var i = mino.Height; i > 0; i--) {
                    target = i * mino.Width;
                    sbMino.Insert(target, "\n");
                }
                sbAllMinos.Append(sbMino.Append("\n"));
            }
            sbAllMinos.Append("ブロック数 " + minoList.Last().Blocks.Count() + " の時 : " + minoList.Count() + "個\n");
            Console.WriteLine(sbAllMinos.ToString());
        }
    }
}

流れはこんな感じです。
N=iのときのミノがM種類あったとして、まず1種類目のミノの1つ目のブロックを選びます。そのブロックの上、右、下、左にブロックが無ければブロックを置きます。その形状を4回時計回りに90度回転させて、既に生成された形状と被らなければ、新たなミノとして扱います。
それを、i個目のブロックまで、M種類目のミノまで行って、N=i+1のときのミノを求める、みたいなことをしています。

↓N=3のミノを元に、N=4のミノを求めるときにやっていることです。
qiita_2_4.png

アルゴリズムに関してはこれ以上いいのが思いつかなかったです。もっと無駄なく調べ切れる方法があるんやろうな…
ちなみに、かかった時間はこんな感じでした。

N 計算時間(ms)
1 22
2 19
3 20
4 23
5 26
6 80
7 396
8 2,745
9 31,725
10 479,806
これ作った後、座標の管理をPoint型にするように作り直しました(ListではなくList)が、時間が余計に掛かったので捨てました。

ブロック数がNのときミノは何種類ある?

ところで、コードを完成させた後、ブロック数がNのときミノの種類は何通りあるんやろ、という所が気になりました。
いわゆる、ブロック数がNの時のミノの種類の個数をM(N)としたとき、それがどんな式になるんやろうという疑問です。想像もつかんけど。

N M(N)(ミノの種類の個数)
1 $1$
2 $1$
3 $2$
4 $7$
5 $18$
6 $60$
7 $196$
8 $704$
9 $2,500$
10 $9,189$

これ見て、思いました。諦めよう。
とはいうものの、流石にちょっと考えました。

qiita_2_5.JPG
qiita_2_6.JPG

…ちょっと用事思い出したのでこの辺にしときます。
というわけで、分かった方は俺にこっそり耳元で教えてください。
一応、ブロック数がNの時、長辺、短辺が最短のミノの長さが、長辺が$\lceil \sqrt{n} \rceil$、短辺が$\lceil\frac{n}{\lceil\sqrt{n}\rceil}\rceil$ってことは分かりました。

おまけ

この記事を書いてて知ったんですけど、この形状にはテトロミノっていう名称があったらしい。これ書かんと一生知らんかったと思う。
参照
[テトロミノ - Wikipedia](https://ja.wikipedia.org/wiki/%E3%83%9D%E3%83%AA%E3%82%AA%E3%83%9F%E3%83%8E

2
0
2

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
2
0