問題
$1$から$M$までの要素からなる全体集合に対し、$N$個の部分集合$S_1,\dots,S_N$が与えられる。この中からちょうど$K$個の集合を選び、和集合の要素数をできるだけ大きくしたい(最大被覆問題)。この問題は一般にNP困難なので、次の貪欲法で近似的に解け。
現在の被覆集合を$C$(初期状態は空集合)とする。$K$回、次を繰り返す:まだ選んでいない集合の中から、$C$に追加したときに新たにカバーされる要素数が最大になる$S_i$を選ぶ。複数候補が同点の場合は添字が最小のものを選ぶ。
選ばれる集合の添字を選んだ順に出力し、最後に得られる被覆の要素数を出力せよ。
入力形式
N M K
c_1 e_{1,1} ... e_{1,c_1}
...
c_N e_{N,1} ... e_{N,c_N}
制約
$1 \le K \le N \le 3000$
$1 \le M \le 60$
$0 \le c_i \le M$、$\sum c_i \le 2\times10^5$
入出力例
入力例1
5 8 3
3 1 2 3
4 3 4 5 6
4 1 5 6 7
2 2 8
2 7 8
出力例1
2 1 5
8
概念図
ヒント(段階的開示)
ヒント1(方向性)
「厳密な最大被覆」を全探索すると$\binom{N}{K}$通りあり非現実的。本問は厳密解を求めよとは言っていない。指定された貪欲手続きを一字一句そのままシミュレートすればよい。
ヒント2(アプローチ)
$M\le60$という制約がヒント。各集合を64ビット整数のビットマスクとして表現すれば、和集合は|(OR)、要素数はint.bit_count()(Python 3.10+)で高速に計算できる。$K$回×$N$候補の$O(NK)$シミュレーションで十分。
ヒント3(誘導)
masks = [...] # 各集合をビットマスク化
used = [False]*N
cur = 0
chosen = []
for _ in range(K):
best_gain = -1
best_i = -1
for i in range(N):
if used[i]:
continue
gain = (cur | masks[i]).bit_count() - cur.bit_count()
if gain > best_gain:
best_gain = gain
best_i = i
chosen.append(best_i)
used[best_i] = True
cur |= masks[best_i]
gain > best_gain(>=ではない)が「同点なら添字が小さい方」を自然に実現する。
模範解答 (Python)
import sys
def main():
data = sys.stdin.buffer.read().split()
idx = 0
N = int(data[idx]); idx += 1
M = int(data[idx]); idx += 1
K = int(data[idx]); idx += 1
masks = [0] * N
for i in range(N):
c = int(data[idx]); idx += 1
m = 0
for _ in range(c):
e = int(data[idx]); idx += 1
m |= (1 << (e - 1))
masks[i] = m
used = [False] * N
cur = 0
chosen = []
cur_pop = 0
for _ in range(K):
best_gain = -1
best_i = -1
for i in range(N):
if used[i]:
continue
gain = (cur | masks[i]).bit_count() - cur_pop
if gain > best_gain:
best_gain = gain
best_i = i
chosen.append(best_i + 1)
used[best_i] = True
cur |= masks[best_i]
cur_pop = cur.bit_count()
print(*chosen)
print(cur_pop)
main()
Step-by-Step 解説
1被覆関数の劣モジュラ性
$f(C)=|\bigcup_{i\in C}S_i|$は「限界効用逓減」の性質(劣モジュラ性)を満たす。
$f(C)=|\bigcup_{i\in C}S_i|$は「限界効用逓減」の性質(劣モジュラ性)を満たす。
2貪欲法の近似保証
Nemhauser–Wolsey–Fisherの定理により、単調劣モジュラ関数への貪欲法は最適解の$(1-1/e)\approx63.2\%$以上を保証する。
Nemhauser–Wolsey–Fisherの定理により、単調劣モジュラ関数への貪欲法は最適解の$(1-1/e)\approx63.2\%$以上を保証する。
3ビットマスクによる高速化
$M\le60$ならPythonの整数1個に全要素を詰め込め、ORと
$M\le60$ならPythonの整数1個に全要素を詰め込め、ORと
bit_count()で高速計算できる。4タイブレーク
ループを添字昇順で回し
ループを添字昇順で回し
gain > best_gainで更新することで「同点なら添字最小」が自然に実現される。5計算量
$O(NK)$に整数演算の定数倍。$N,K\le3000$なら数秒程度で収まる。
$O(NK)$に整数演算の定数倍。$N,K\le3000$なら数秒程度で収まる。
よくあるミス
| ミス | 原因 | 正しい書き方 |
|---|---|---|
タイブレークの不等号を>=にしてしまう | 「最大値更新」で何となく>=を使う癖 | 添字が小さい方を優先したいので厳密な>を使う |
| 厳密最適解を実装しようとする | 「最大化」という言葉から厳密解法を連想 | 問題文は貪欲手続きそのものを問うている |
増分計算cur_popの更新を忘れる | ループ内で毎回全体を数え直すのは非効率かつミスの元 | 選んだ後にまとめて1回cur_popを更新する |
| 要素番号が1-indexedなのを忘れる | ビット位置は0始まり | 1 << (e - 1)のように必ずずらす |
| $K=N$で順序を無視してよいと誤解 | 最終的な被覆自体は順不同で同じになると早合点 | 出力仕様は「選んだ順」も求めているため、貪欲の実行順通りに出力する |
次のステップ
- 発展: $M$が大きい($10^5$程度)場合、多倍長整数のビット演算がどこまで実用的か実験的に確認せよ。
- 発展2: 「全要素をカバーするのに必要な最小集合数」を求める古典的な集合被覆問題に対する貪欲の$O(\log M)$近似との違いを整理せよ。
- 次回予告: 次回のテーマは今後のセッションで選定