カーニバルに行ったとき、各会場で特別な穴あけパンチでプログラムに印をつけました。穴あけパンチは 3x3 のグリッドです。各スペースには、紙に穴を開けるピンが 1 本あるか、または 1 本ありません。このツールで何種類のパターンを作れるか、考えました。最初に思いついたのは 2^9 = 512 でしたが、9 つのスペースすべてにピンが 1 本もなければ、実際には穴あけパンチとは言えないので、実際には 511 です。
すると、複雑さに気が付きました。特に、作業員が紙に穴を開けるときにそれほど注意を払わないので、これらはすべて同じに見えます。
x.. .x. ... etc.
.x. x.. .x.
... ... ..x
質問:回転とシフトを考慮してテストを作成するにはどうすればよいでしょうか?
これまでの努力と考察:
- バイナリはこの方程式の明らかな部分のように感じられる
- ユニークなパターンが見つかったら、それをメモリに保存して、将来のパターンをテストできるようにします。
- 回転の選択肢は4つあります。
編集:「回転」とは、どんな形であっても 90 度回転できることを意味します。左上隅の点のパターンを考えてみましょう。これを 90 度回転すると、右上隅の点になります。これをもう一度行うと、右下になります。もう一度行うと、左下になります。純粋な 2^9 計算を使用すると、これらは 4 つの異なる組み合わせになります。ただし、この問題では、これらはまさに私が排除しようとしている重複の種類です。 - 回転ごとに、3x3 グリッドを重ねる方法は 25 通りあります。
重複:
/ = the spaces in the new one to test
\ = the spaces in a verified unique one
1 2 25
/ / / . . . . . / / / . . . . . . . . . .
/ / / . . . . . / / / . . . . . . . . . .
/ / X \ \ . . . / X X \ . . . . \ \ \ . .
. . \ \ \ . . . . \ \ \ . . . . \ \ \ . .
. . \ \ \ . . . . \ \ \ . . . . \ \ X / /
. . . . . . . . . . . . . . . . . . / / /
. . . . . . . . . . . . . . . . . . / / /
- いずれかのパターンに重複領域にないピンが含まれている場合、重複をテストする必要はありません。ここではビット単位の AND が役立ちます。
- 2つのパターンのそれぞれの位置を文字列にすると、等しいかどうかをチェックするだけで済みます。
- 前述の 2 つのアイデアを組み合わせて効率を高めることはできますか?
ベストアンサー1
最初の行と列にパンチがあるパターンのみを考慮する必要があります。最初の行が空の場合、パターンを上にシフトできます。最初の列が空の場合、パターンを左にシフトできます。どちらの場合でも、考慮する類似のパターンを導き出すことができます。
これらのパターンについては、回転したバージョンが同一であるかどうかを確認する必要があります。これを行うには、最大 3 回の 90 度回転を適用し、先頭の空の列を削除するために左にシフトし (最初の行が空になることはありません)、数値が最も小さいパターンを見つけます。
次に、この値をハッシュ セットに追加して、一意の値のみを保持することができます。
空のパターンは、すべての行が空であるため含まれません。
これを実装するには、パターンを連続するビットとしてエンコードします。
012
345
678
必要な操作はほとんどが非常に簡単です。
Test for an empty row: (n & 7) == 0 // bits 0,1,2 not set
Test for an empty column: (n & 73) == 0 // bits 0,3,6 not set
Shift pattern up: n -> (n >> 3)
Shift pattern left: n -> (n >> 1)
最も難しい部分は回転です。これは実際にはすべてのビットを並べ替えるだけです。
n -> ((n & 1) << 2) + ((n & 2) << 4) + ((n & 4) << 6)
+ ((n & 8) >> 2) + (n & 16) + ((n & 32) << 2)
+ ((n & 64) >> 6) + ((n & 128) >> 4) + ((n & 256) >> 2);
C# の場合:
public static int Count3x3() {
HashSet<int> patterns = new HashSet<int>();
for (int i = 0; i < 512; i++) {
if ((i & 7) == 0 || (i & 73) == 0)
continue;
int nLowest = i;
int n = i;
do {
nLowest = Math.Min(nLowest, n);
n = ((n & 1) << 2) + ((n & 2) << 4) + ((n & 4) << 6)
+ ((n & 8) >> 2) + (n & 16) + ((n & 32) << 2)
+ ((n & 64) >> 6) + ((n & 128) >> 4) + ((n & 256) >> 2);
while ((n & 73) == 0)
n >>= 1;
} while (n != i);
patterns.Add(nLowest);
}
return patterns.Count;
}
この関数は 116 を返します。私のマシンでかかった時間は 0.023 ミリ秒でした。
編集: 4 つの観察結果を使用することで、さらに 7 倍の改善が得られます。
- ハッシュ セットの代わりに、単純なアクセス済み配列を使用できます。パターンが以前に見つかった場合は、それをカウントしません。これにより、内部ループで「最低」のパターンを追跡する必要もなくなります。パターンがアクセスされた場合、その最低の回転パターンもアクセスされます。
- 180 度の回転対称性がない場合、3 回目の回転では元のパターンは生成されません。4 回目の回転では必ず元のパターンが生成されるため、不要です。
- 回転式は少し簡略化できます。
したがって、これらの観察を適用して内部の do ループを展開すると、次のようになります。
static int Rotate(int n) {
n = ((n & (1+32)) << 2) + ((n & 2) << 4) + ((n & 4) << 6)
+ ((n & (8+256)) >> 2) + (n & 16)
+ ((n & 64) >> 6) + ((n & 128) >> 4);
while ((n & 73) == 0)
n >>= 1;
return n;
}
public static int Count3x3_3() {
bool[] visited = new bool[512];
int count = 0, r;
for (int i = 0; i < 512; i++) {
if (visited[i])
continue;
if ((i & 7) == 0 || (i & 73) == 0)
continue;
count++;
if ((r = Rotate(i)) == i) continue;
visited[r] = true;
if ((r = Rotate(r)) == i) continue;
visited[r] = true;
visited[Rotate(r)] = true;
}
return count;
}
これは同じマシン上で約 3μs で実行されます。