問題
$N$ 頂点 $M$ 辺の単純無向グラフ(辺重みなし、多重辺・自己ループなし)と整数 $K$ が与えられる。Nagamochi-Ibaraki法による $K$-辺連結性証明書(sparse certificate)を次の手順で構築せよ。
- 証明書に含める辺集合 $F = \emptyset$ から開始する。
- $i = 1, \dots, K$ について以下を繰り返す:
- 現在の証明書 $F$ を用いて Maximum Adjacency Scan(頂点1から開始)を行う。未訪問頂点のうち、証明書 $F$ における既訪問頂点への接続本数が最大の頂点を次々に選ぶ(同点は頂点番号が小さい方を優先)。
- 訪問順で2番目以降の各頂点 $v$ について、$v$より前に訪問された頂点の中から元のグラフ上で$v$と隣接し、まだ$F$に無い辺のうち、直前に訪問された頂点との辺を最優先し、無ければ最後に訪問された隣接頂点との辺を1本選び$F$に追加する。
- 最終的な $F$ の辺数を出力せよ。
この証明書$F$は、元のグラフが持つサイズ$K$以下の全てのカットを正確に保持することが理論的に保証されており、$|F|\le K(N-1)$本に収まる。
入力形式
N M K
u_1 v_1
:
u_M v_M
制約
$2 \le N \le 10$
$1 \le M \le 45$
$1 \le K \le 5$
グラフは連結
入出力例
入力例1
4 5 2
1 2
2 3
3 4
4 1
1 3出力例1
5$K=2$回のMAスキャンで、元の5辺のグラフの辺連結性を保つのに5辺全てが証明書に含まれる例。
概念図:Maximum Adjacency Scan
ヒント(段階的開示)
ヒント1(方向性)
「$K$-辺連結性を判定するのに全ての辺は必要ない」という直感がある。疎な部分グラフでも元のグラフの「カットの大きさが$K$以下かどうか」という情報を完全に保存できることがある。
ヒント2(アプローチ)
MA ordering(Day095のStoer-Wagner法で既出)を$K$回繰り返す。$i$回目のスキャンではそれまでに集めた証明書$F_{i-1}$を使って走査順序を決め、各頂点について「スキャン順で自分より前にいる隣接頂点との辺を1本」新たに証明書に加える。これを$K$回繰り返すことで、各頂点は証明書側で高々$K$本の「保証された」接続を持つことになる。
ヒント3(誘導)
visited = [False] * (N + 1)
weight = [0] * (N + 1) # F_adjにおける既訪問頂点への接続本数
order = [start]
visited[start] = True
for w in F_adj[start]:
weight[w] += 1
for _ in range(N - 1):
u = max((v for v in range(1, N+1) if not visited[v]),
key=lambda v: (weight[v], -v))
visited[u] = True
order.append(u)
# order[-2](直前訪問)を最優先して元グラフの隣接辺を1本選びFに追加
for w in F_adj[u]:
weight[w] += 1
「直前に訪問された頂点を最優先」というルールはMAスキャンの理論的性質に基づく実装上の単純化。
模範解答 (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
orig_adj = [set() for _ in range(N + 1)]
for _ in range(M):
u = int(data[idx]); idx += 1
v = int(data[idx]); idx += 1
orig_adj[u].add(v)
orig_adj[v].add(u)
F = set()
F_adj = [set() for _ in range(N + 1)]
def add_edge(u, v):
key = (min(u, v), max(u, v))
if key not in F:
F.add(key)
F_adj[u].add(v)
F_adj[v].add(u)
for _ in range(K):
visited = [False] * (N + 1)
weight = [0] * (N + 1)
order = []
start = 1
visited[start] = True
order.append(start)
for w in F_adj[start]:
weight[w] += 1
for _ in range(N - 1):
u = -1
for v in range(1, N + 1):
if not visited[v]:
if u == -1 or weight[v] > weight[u]:
u = v
visited[u] = True
order.append(u)
prev_visited_neighbors = [w for w in order[:-1] if w in orig_adj[u]]
if prev_visited_neighbors:
chosen = None
if len(order) >= 2 and order[-2] in orig_adj[u]:
key = (min(u, order[-2]), max(u, order[-2]))
if key not in F:
chosen = order[-2]
if chosen is None:
for w in reversed(prev_visited_neighbors):
key = (min(u, w), max(u, w))
if key not in F:
chosen = w
break
if chosen is not None:
add_edge(u, chosen)
for w in F_adj[u]:
weight[w] += 1
print(len(F))
main()
Step-by-Step 解説
1Maximum Adjacency Search(MAスキャン)とは
Stoer-Wagner法(Day095, Day115)でも使われる走査法。既訪問集合への接続が最も強い未訪問頂点を選んで訪問する。本問では重みを「証明書$F$における既訪問頂点への辺数」とする点が特徴。
Stoer-Wagner法(Day095, Day115)でも使われる走査法。既訪問集合への接続が最も強い未訪問頂点を選んで訪問する。本問では重みを「証明書$F$における既訪問頂点への辺数」とする点が特徴。
2各スキャンで辺を1本ずつ追加する意味
$i$回目のMAスキャンでは各頂点(始点を除く)につき自分より前に訪問された頂点との元グラフ上の辺を1本証明書に追加する。$K$回繰り返すことで各頂点は証明書内で理想的には$K$本の強い接続を確保する。
$i$回目のMAスキャンでは各頂点(始点を除く)につき自分より前に訪問された頂点との元グラフ上の辺を1本証明書に追加する。$K$回繰り返すことで各頂点は証明書内で理想的には$K$本の強い接続を確保する。
3なぜ情報が保存されるのか(直感)
MAスキャンで選ばれる頂点順序は連結性が強い順に近く、$K$回繰り返すことで証明書はサイズ$K$以下の任意のカットについて元のグラフと同じカット値を再現できることがNagamochi-Ibaraki(1992)により証明されている。
MAスキャンで選ばれる頂点順序は連結性が強い順に近く、$K$回繰り返すことで証明書はサイズ$K$以下の任意のカットについて元のグラフと同じカット値を再現できることがNagamochi-Ibaraki(1992)により証明されている。
4辺数の上界
各スキャンで新たに追加される辺は高々$N-1$本なので、$K$回のスキャン全体で$|F|\le K(N-1)$に収まる。密なグラフでも証明書は疎になる。
各スキャンで新たに追加される辺は高々$N-1$本なので、$K$回のスキャン全体で$|F|\le K(N-1)$に収まる。密なグラフでも証明書は疎になる。
5計算量
$K$回のスキャンそれぞれが$O(N^2)$(本問の素朴な実装)。理論的には優先度付きキューで$O(M+N\log N)$程度まで高速化できる。
$K$回のスキャンそれぞれが$O(N^2)$(本問の素朴な実装)。理論的には優先度付きキューで$O(M+N\log N)$程度まで高速化できる。
よくあるミス
| ミス | 原因 | 正しい書き方 |
|---|---|---|
| MAスキャンの重みを元グラフの隣接で計算してしまう | Stoer-Wagner法とNagamochi-Ibaraki法の重み定義の違いを混同する | 常に「現在の証明書$F$における既訪問頂点への接続」を重みとして使う |
| 同じ辺を複数回証明書に追加してカウントしてしまう | 辺の重複チェックを怠る | `(min,max)`タプルで正規化した辺集合を管理し追加前に存在確認する |
| 各スキャンで頂点ごとに複数本の辺を追加してしまう | 「1頂点につき1本まで」の制約を見落とす | 訪問順で2番目以降の各頂点について証明書に追加する辺は高々1本に限定する |
| K回のスキャンを独立に行ってしまう | 各スキャンが独立だと誤解する | 2回目以降のスキャンはそれまでに蓄積した証明書$F$を使って重みを計算する |
| 非連結グラフに対してそのまま使ってしまう | 制約確認を怠る | 一般には非連結グラフでは各連結成分ごとに独立して扱う必要がある |
次のステップ
- 発展: 構築した証明書$F$上で実際に大域最小カット(Stoer-Wagner法)を計算し、元のグラフの最小カットが$K$以下であれば証明書上でも同じ値になることを実験的に確認せよ。
- 次回予告: メトリック閉包上のSteiner Tree 2-近似アルゴリズム