0. どんな問題?(問題の概要)
$N$ 枚の $H \times W$ マスの白黒模様のハンカチ(# と .)の情報が与えられます。回転・反転させて同じになる模様は同一視するとき、所持しているハンカチが「実質何種類あるか」を求める問題です。
※特定の題材を抽象化し、「2次元グリッドの同型判定(幾何学的な重複排除)」として整理しています。
1. 最初のアプローチとつまずいたポイント
#まず、まったく回答になってなくて申し訳ないです
N = int(input())
grid = []
for _ in range(N):
H, W = map(int, input().split())
grid.append([list(input()) for _ in range(H)])
#模様のパターンをN種類すべて配列に入れようとした
print(grid)
number = 0
def requirement1(grid):
for k in range(W):
for l in range(H):
if (grid[k][l] == "#"):
number += 1
return number
#回転して重なるのなら、前提として#という模様の数は等しいのは当然だろうと思い、それを確かめるための
関数を作りました
coordinate = []
def requirement2(grid):
for m in range(W):
for n in range(H):
coordinate.append([(m, n)])
for o in range(N):
for p in range(N):
if((grid[o] == grid[p]) and (o != p)):
set_o = sorted(set(grid[o]))
set_p = sorted(set(grid[p]))
symmetrical_difference = set_o ^ set_p
for s in range(2):
for t in range(2):
if (())
#回転して重なるということは、それぞれの模様において座標が等しいものと、回転したら等しくなる座標があると考えて、まず同じ座標を対象差で取り除き、ひっくり返して等しいペアが作れるかみたいなことを検証しようと思いましたが、できませんでした
つまずきポイント
-
座標計算が複雑すぎる: 90度・180度・270度回転や左右反転のたびに $H$ と $W$ が入れ替わったりインデックス計算がズレたりして、for ループがカオスになってしまいました
-
判定基準の崩壊: 行ごとに set() に入れて比較しようとしましたが、上下左右の結合関係(位置関係)が壊れてしまい、正しく柄を判定できませんでした
2. 発想の転換:「標準形(代表値)」を作って set に突っ込む
1対1で座標を変換して比較するのではなく、「各ハンカチから作れる回転・反転の全パターンから、1つの代表(標準形)を決める」というアプローチに切り替えます。
-
1枚のハンカチにつき、回転(4通り) $\times$ 反転(2通り) $=$ 最大8通りの変形パターンを作る
-
8通りの中で 「辞書順で一番小さい文字列(タプル)」 をそのハンカチの “標準形” として選ぶ
-
すべてのハンカチを標準形に変換し、Pythonの set に突っ込む
この方法なら、回転・反転して同じになるハンカチは必ず同じ「標準形」に集約されるため、最後に len(set) を取るだけで一発で答えが出ます!
3. 解答コード(素直な実装版)
まずは回転・反転の処理を愚直に書いて全体像を掴むコードです。
def flip(grid):
"""
【関数1】グリッド(ハンカチ)を左右反転させる関数
各行の文字列をスライス [::-1] で逆順にひっくり返します。
"""
# row[::-1] は「文字列 row を後ろから1文字ずつ読む(=反転)」という意味
return tuple(row[::-1] for row in grid)
def rotate90(grid):
"""
【関数2】グリッドを90度時計回りに回転させる関数
縦横のサイズ(H, W)が変わる場合にも対応しています。
"""
H = len(grid) # 元のグリッドの「縦のマス数(行数)」
W = len(grid[0]) # 元のグリッドの「横のマス数(列数)」
rotated = []
# 90度回転後の各行を作るため、元の列(w)ごとに処理する
for w in range(W):
# 回転後の新しい行の文字を、元のグリッドの「下から上へ」順番に集める
# H - 1 - h で一番下の行から順にアクセスしています
new_row = "".join(grid[H - 1 - h][w] for h in range(H))
rotated.append(new_row)
# 重複排除の set に入れるため、変更できない tuple 型にして返す
return tuple(rotated)
def get_canonical_form(grid):
"""
【関数3】回転・反転で作れる全8パターンを生成し、
その中で一番文字として小さい「標準形(代表パターン)」を1つ選んで返す関数
"""
patterns = []
curr = grid
# 4回回転させる(0度、90度、180度、270度)
for _ in range(4):
patterns.append(curr) # そのままの形を登録
patterns.append(flip(curr)) # 左右反転した形を登録
curr = rotate90(curr) # 次のループのために90度回転させる
# 8通りのパターン(文字列のタプル)の中で「辞書順で一番小さいもの」を代表値(標準形)とする
# これにより、回転や反転で同じ模様になるものは、すべて全く同じ「標準形」に揃います
return min(patterns)
def solve():
"""
【メイン処理】標準入力からデータを受け取り、ハンカチの種類数を計算して出力する
"""
# 1. 持っているハンカチの枚数 N を取得
N = int(input())
# 2. 登場したハンカチの「標準形」を記憶するための集合(set)を作る
# set は自動的に重複する要素を排除してくれます
unique_handkerchiefs = set()
# 3. N 枚分のハンカチ情報を順番に処理する
for _ in range(N):
# 縦 H マス、横 W マスの値を取得
H, W = map(int, input().split())
# H 行分の文字列を読み込む
# set の要素にするため、変更不可能な tuple 型で保持する
grid = tuple(input() for _ in range(H))
# どの向き・反転状態でも共通の「標準形(代表)」に変換する
canonical = get_canonical_form(grid)
# set に追加(既に同じ標準形が存在していれば自動で無視される)
unique_handkerchiefs.add(canonical)
# 4. 集まった標準形の種類数(=実質的なハンカチの種類数)を出力する
print(len(unique_handkerchiefs))
# プログラムを実行する
solve()
4. さらに Python らしさを極めるなら(zip(*grid[::-1]) の活用)
Pythonでは zip(*grid[::-1]) を使うと、たった1行で「2次元配列の90度時計回り回転」が書けます。
元のグリッド:
1 2
3 4
上下反転 (grid[::-1]):
3 4
1 2
転置 zip(*...):
(3, 1) <- 1列目を抽出
(4, 2) <- 2列目を抽出
文字列結合 "".join:
"31"
"42" => 見事に90度時計回りに回転!
これをコピペ用にまとめると以下のようになる。
# 長方形(H × W)のサイズ変更にも自動で対応!
def rotate90(grid):
return tuple("".join(col) for col in zip(*grid[::-1]))
以上の関数を使うと、このようになる
def rotate90(grid):
"""
【関数1】90度時計回りに回転する関数(Pythonワンライナー版)
『上下反転』してから『転置(行と列の入れ替え)』をすることで、
たった1行で縦横のサイズが変わる回転にも対応できます。
"""
# 1. grid[::-1] : グリッドの上下(行の順番)をひっくり返す
# 2. zip(*...) : * で展開して各行の同じ位置の文字を縦(列)にまとめる(転置)
# 3. "".join(col): タプルになっている列の文字を1つの文字列に結合する
return tuple("".join(col) for col in zip(*grid[::-1]))
def flip(grid):
"""
【関数2】左右反転する関数
スライス [::-1] を使って、各行の文字列を後ろから逆に読みます。
"""
return tuple(row[::-1] for row in grid)
def get_canonical_form(grid):
"""
【関数3】全8パターンを生成し、辞書順最小の『標準形(代表値)』を返す関数
"""
patterns = []
curr = grid
# 90度回転を4回繰り返す(0度, 90度, 180度, 270度)
for _ in range(4):
patterns.append(curr) # 回転のみの形を追加
patterns.append(flip(curr)) # それを左右反転した形を追加
curr = rotate90(curr) # 次のループのために90度回転させる
# 生成された8つのパターンの中から、辞書順で一番小さいものを標準形として選ぶ
return min(patterns)
# ==========================================
# メイン処理
# ==========================================
# 1. ハンカチの枚数 N を取得
N = int(input())
# 2. 登場した標準形を記憶する集合(重複を自動で除外してくれる)
unique = set()
# 3. N枚のハンカチを1枚ずつ処理する
for _ in range(N):
# 縦 H、横 W の大きさを取得(※計算自体は rotate90 内で自動処理されます)
H, W = map(int, input().split())
# H行分の文字列を読み込み、set に入れられるよう不可変な tuple 型にする
grid = tuple(input() for _ in range(H))
# 回転・反転に対応した標準形(代表パターン)に変換して set に追加する
unique.add(get_canonical_form(grid))
# 4. 集まった標準形の種類数(重複を除いた実質的な種類数)を出力する
print(len(unique))
5. 学んだこと・まとめ
-
回転・反転の同型判定は「標準形(代表値)」を作るのが鉄則
2つのオブジェクトを比較して「同じか?」を判定するより、全パターン生成して min() で一意の代表値に落とし込む方が実装が100倍楽になる。 -
zip(*grid[::-1]) は2次元配列回転の最強テクニック
縦横のサイズ $H, W$ が異なるケースでも自動的に転置してくれるため、バグが混入しにくい。 -
set に2次元配列を入れる時は tuple 化する
Pythonの list は可変(mutable)のため set の要素にできないが、tuple や文字列に変換すれば集合(set)で一括重複排除ができる。