問題
$H$ 行 $W$ 列の盤面に、的が $N$ 個ある。$i$ 個目の的はマス $(r_i,c_i)$ にある(同じマスに 2 個以上は無い)。
1 回のレーザーで、ある 1 行の的を全部、またはある 1 列の的を全部消せる。すべての的を消すのに必要なレーザーの最小回数 $K$ と、そのときに撃つ行・列を 1 組出力せよ。
入力形式
H W N
r_1 c_1
...
r_N c_N
出力形式: 1 行目に $K$。続く $K$ 行に、行を撃つなら R r、列を撃つなら C c。最小回数を達成する組なら、どれを出力してもよい(順序も任意)。
制約
| # | 条件 |
|---|---|
| 1 | $1\le H,W\le10^5$ |
| 2 | $1\le N\le2\times10^5$ |
| 3 | $1\le r_i\le H$、$1\le c_i\le W$、$(r_i,c_i)$ はすべて異なる |
| 4 | 入力はすべて整数 |
入出力例
入力例1
3 3 4
1 1
1 3
2 2
3 2出力例1
2
R 1
C 2(行 1 で $(1,1),(1,3)$、列 2 で $(2,2),(3,2)$。1 回では足りない)
入力例2
2 4 5
1 1
1 2
1 3
1 4
2 1出力例2
2
R 1
R 2(R 1 と C 1 でも正解。どちらも 2 回)
背景と出典
行を左側、列を右側の頂点にし、的 $(r,c)$ を辺 $r-c$ にすると、「すべての辺の端点のどちらかを選ぶ」最小頂点被覆になる。二部グラフでは最小頂点被覆の大きさが最大マッチングの大きさに等しい(König の定理、1931 年。Egerváry も同時期に独立に示した、と私は理解している。原典は本セッションでは未確認)。
大きさだけなら最大マッチングを出せば終わる。この問題の中核は被覆そのものの復元で、手順は König の定理の構成的な証明そのもの。
- 最大マッチング $M$ を作る(Hopcroft–Karp、$O(E\sqrt V)$。Hopcroft・Karp 1973、原典未確認)
- 未マッチの行から、「行→(マッチしていない辺)→列→(マッチ辺)→行」と交互にたどって届く頂点の集合 $Z$ を取る
- 被覆 $=(\text{行}\setminus Z)\cup(\text{列}\cap Z)$
正しさは「$|$被覆$|=|M|$ かつ全辺を覆う」の 2 点で、弱双対性(被覆はどのマッチングより小さくならない)から最小になる。検証ではこの 2 点を大入力でも機械的に確かめた。
概念図
ヒント(クリックで段階的に開示)
ヒント1(方向性)
「各的について、その行か列の少なくとも一方を撃つ」。行と列を頂点、的を辺にした二部グラフで、すべての辺に触れる頂点の最小集合を探している。貪欲(的の多い行・列から撃つ)は最小にならない。
ヒント2(アプローチ)
最小頂点被覆の大きさ = 最大マッチングの大きさ。マッチングの各辺は少なくとも 1 頂点を被覆に含むので、被覆 $\ge$ マッチングは明らか。等号を実現する被覆を、最大マッチングから作る。
ヒント3(誘導)
最大マッチングを作った後、未マッチの行すべてを始点に BFS する。行からはどの辺でも列へ進み(マッチ辺は行の相手の列なので、未マッチ行からは出ない)、列からはマッチ辺だけで行へ戻る。届いた行の集合 $Z_L$、届いた列の集合 $Z_R$ について、答えは「$Z_L$ に入らない行(辺を持つもの)」と「$Z_R$ の列」。
模範解答 (Python)
模範解答を表示する(まず自力で解いてから)
import sys
from collections import deque
def main():
data = sys.stdin.buffer.read().split()
h, w, n = int(data[0]), int(data[1]), int(data[2])
adj = [[] for _ in range(h + 1)] # 行 r -> 点のある列
for i in range(n):
r = int(data[3 + 2 * i]); c = int(data[4 + 2 * i])
adj[r].append(c)
rows = [r for r in range(1, h + 1) if adj[r]]
INF = 1 << 30
match_r = [0] * (h + 1) # 行 -> 列(0 は未マッチ)
match_c = [0] * (w + 1) # 列 -> 行
dist = [0] * (h + 1)
# Hopcroft–Karp: 最短の増加路だけを層ごとにまとめて流す
while True:
q = deque()
for r in rows:
if match_r[r] == 0:
dist[r] = 0
q.append(r)
else:
dist[r] = INF
found = False
while q:
r = q.popleft()
for c in adj[r]:
r2 = match_c[c]
if r2 == 0:
found = True
elif dist[r2] == INF:
dist[r2] = dist[r] + 1
q.append(r2)
if not found:
break
it = [0] * (h + 1)
for s in rows:
if match_r[s]:
continue
# 反復版 DFS で層を 1 つずつ下る増加路を探す
stack = [s]
while stack:
r = stack[-1]
advanced = False
while it[r] < len(adj[r]):
c = adj[r][it[r]]
r2 = match_c[c]
if r2 == 0:
# 増加路が見つかった: スタック上の行を順に付け替える
for k in range(len(stack) - 1, -1, -1):
rr = stack[k]
cc = adj[rr][it[rr]]
match_r[rr] = cc
match_c[cc] = rr
stack = []
advanced = True
break
if dist[r2] == dist[r] + 1:
stack.append(r2)
advanced = True
break
it[r] += 1
if not advanced:
dist[r] = INF # この行からは増加路が無い
stack.pop()
if stack:
it[stack[-1]] += 1
# König: 未マッチの行から交互路(行→非マッチ辺→列→マッチ辺→行)で届く集合 Z
vis_r = [False] * (h + 1)
vis_c = [False] * (w + 1)
q = deque()
for r in rows:
if match_r[r] == 0:
vis_r[r] = True
q.append(r)
while q:
r = q.popleft()
for c in adj[r]:
if not vis_c[c]:
vis_c[c] = True
r2 = match_c[c] # Z の列は必ずマッチ済み(でなければ増加路がある)
if not vis_r[r2]:
vis_r[r2] = True
q.append(r2)
# 最小頂点被覆 = (Z に入らない行) ∪ (Z に入った列)
ans = [f"R {r}" for r in rows if not vis_r[r]] + [f"C {c}" for c in range(1, w + 1) if vis_c[c]]
sys.stdout.write(str(len(ans)) + "\n" + "\n".join(ans) + "\n")
main()
計算量: Hopcroft–Karp が $O(N\sqrt{H+W})$、$Z$ の BFS が $O(N+H+W)$。実測(Python 3.14.7、M1 Mac)で $H=W=10^5,N=2\times10^5$ の一様乱数が 1.00 s(別の回は 2.85 s。機械の負荷で揺れた)、行 $r$ に列 $r,r+1$ を置いた鎖状の入力が 0.23 s、450×450 に密集させた入力が 0.15 s。
Step-by-Step 解説
行は左、列は右。的は辺。撃つ行・列が頂点被覆。的のない行・列は被覆に入れる必要が無いので、辺を持つ行だけを rows に持つ。
$Z$ の列 $c$ が未マッチなら、未マッチの行からそこまでの交互路が増加路になり、マッチングが最大であることに反する。だからコード中の r2 = match_c[c] は必ず 0 でない。
辺 $(r,c)$ が覆われないのは「$r\in Z$ かつ $c\notin Z$」のときだけ。$r\in Z$ なら BFS で $r$ から全部の辺を見るので $c\in Z$ になる。よって覆われない辺は無い。
マッチ辺 $(r,c)$ について、$c\in Z$ なら $r\in Z$(マッチ辺で戻る)なので $r$ は被覆に入らず $c$ だけが入る。$c\notin Z$ なら $r\notin Z$($r\in Z$ なら $c\in Z$ になるから)で $r$ だけが入る。未マッチの行は $Z$ に入るので被覆に入らず、$Z$ の列は全部マッチ済み。したがって被覆の各頂点がちょうど 1 本のマッチ辺に対応し、$|$被覆$|=|M|$。
「$Z$ に入った行」と「$Z$ に入らない列」を答えると、乱数 500 件中 411 件で被覆にならないか最小でない(実測)。式を覚えるのでなく、Step 3 の「覆われない辺は $r\in Z,c\notin Z$ だけ」から毎回組み立てると間違えない。
迷った他案と採らなかった理由
| 案 | 計算量 | 採否 | 理由 |
|---|---|---|---|
| 行の部分集合の全探索 | $O(2^HN)$ | 検算用のみ | $H\le10$ の乱数 500 件で最小回数を照合 |
| 貪欲(的の多い行・列から撃つ) | $O(N\log N)$ | 不採用 | 500 件中 37 件で最小にならない(実測) |
| Kuhn 法(増加路を 1 本ずつ DFS) | $O(NH)$ | 不採用 | 最悪 $2\times10^{10}$。乱数入力では速いことが多いが、保証が無い |
| Hopcroft–Karp + König の復元(本解) | $O(N\sqrt V)$ | 採用 | 500 件全部で被覆が正しく最小。大入力 3 種で「被覆が全的を覆う」「被覆の大きさ = マッチングの大きさ」を確認 |
| 最大流(Dinic)で最小カットを取る | $O(N\sqrt V)$ | 不採用 | 単位容量の二部グラフでは Hopcroft–Karp と同じ計算量。残余グラフの $S$ 側到達集合が本解の $Z$ に当たる。実装量が増えるだけ |
前提が変わったら:
- 行・列ごとに撃つ費用が違う(重み付き)なら König は使えない。$S\to$行(費用)、行$\to$列($\infty$)、列$\to T$(費用)の最小カットにする
- 斜め方向のレーザーも撃てるなら三部以上になり、頂点被覆は NP 困難(一般グラフ)。制約を見直す
- 最大マッチングの大きさだけを問うなら、復元の BFS は不要
よくあるミス
| ミス | 原因 | 正しい書き方 |
|---|---|---|
| 被覆を「$Z$ の行」と「$Z$ でない列」にする | 左右の取り違え | 「$Z$ でない行」と「$Z$ の列」(411/500 誤答) |
| マッチ済みの行を全部撃つ | マッチングの各辺の片方なら何でもよいと思う | 辺ごとに $Z$ で左右を選ぶ(212/500 で覆われない的が残る) |
| 交互路で列から非マッチ辺でも戻る | BFS を普通のグラフ探索にする | 列からはマッチ辺だけで行へ |
| 貪欲に多い順で撃つ | 局所最適 | 37/500 で最小にならない |
| 再帰 DFS の Kuhn/HK | 深さ $10^5$ で再帰上限 | 反復版の DFS |
次のステップ
- 発展1: 最大独立集合(撃たれずに残せる「行・列」の最大集合)を同じ $Z$ から出す(補集合)
- 発展2: Dilworth の定理: DAG の最小鎖被覆を二部マッチングに帰着し、鎖そのものを復元する
- 発展3: 重み付き版を最小カットで解き、残余グラフの到達集合から撃つ行・列を復元する
検証ログ(本セッションで実行した数値)
- 全探索との照合(実測): $H,W\le10$、的の数 $\le30$ の乱数 500 件で、出力が「全的を覆う」かつ「回数が全探索の最小に等しい」を全件満たした
- 大入力(実測): 一様乱数($K=78302$)、鎖状($K=100000$)、450×450 密集($K=450$)の 3 種で、出力が全的を覆い、かつ $K$ が Hopcroft–Karp のマッチングの大きさと一致(弱双対性より最小)
- 誤った方法(実測): 左右を取り違えた被覆は 500 件中 411 件で不正解、マッチ済みの行を全部撃つと 212 件で的が残る、貪欲は 37 件で最小にならない
- 入力例(実測): 例 1
2 / R 1 / C 2、例 22 / R 1 / R 2 - 実行時間(実測): 一様乱数 1.00 s(負荷の高い回は 2.85 s)、鎖状 0.23 s、密集 0.15 s
- 扱っていない範囲: Hopcroft–Karp の最悪ケースを狙った入力での測定、重み付き版、König・Egerváry・Hopcroft–Karp の原典