Day 021-Q3 — マトロイド交差(Matroid Intersection)

2026-05-04 赤色 Master / Phase 8+ ★★★★★★★★★ マトロイド交差・組み合わせ最適化

問題

$N$ 個の要素からなる集合 $U = \{0, 1, \ldots, N-1\}$ がある。2つのマトロイド $M_1, M_2$ が与えられ、それぞれ独立集合族を持つ。

$M_1$: グラフ的マトロイド(Graphic Matroid)。$N$ 要素は辺に対応し、グラフ $G$ の辺集合 $E$ から森(閉路を含まない部分グラフ)を選ぶ。

$M_2$: 分割マトロイド(Partition Matroid)。$N$ 要素は $K$ グループ $G_1, G_2, \ldots, G_K$ に分割されており、各グループ $G_i$ から高々 $k_i$ 個選べる。

両マトロイドの独立集合の共通集合(Matroid Intersection)の最大サイズを求めよ。

入力形式

V E
u_1 v_1
u_2 v_2
...
u_E v_E
K
g_1 k_1
g_2 k_2
...
g_E k_E

制約

$2 \leq V \leq 100$
$1 \leq E \leq 500$
$1 \leq K \leq E$
$1 \leq k_i \leq E$

入出力例

入力例 1

4 5
0 1
0 2
1 2
1 3
2 3
3
0 2
0 2
1 1
2 1
3 1

出力例 1

3

ヒント (段階的開示)

ヒント1: 方向性
マトロイド交差アルゴリズムは「交換グラフ上の最短路」を繰り返し見つけることで最大独立集合を拡大する。
ヒント2: アプローチ
現在の共通独立集合 $I$ から: (1) $M_1$ で $I$ に追加できる要素 $X_1 = \{e \notin I : I + e \in \mathcal{I}_1\}$、(2) $M_2$ で追加できる要素 $X_2 = \{e \notin I : I + e \in \mathcal{I}_2\}$、(3) 交換グラフ $D_{M_1,M_2}(I)$ を構築して $X_1 \to X_2$ の最短路を探す。
ヒント3: 誘導
# 交換グラフの辺:
# e ∈ I → f ∉ I: I - e + f ∈ M1 のとき e→f
# f ∉ I → e ∈ I: I - e + f ∈ M2 のとき f→e
# BFS で X1 の要素から X2 の要素への最短路を見つけ、
# その路上で I の要素とそれ以外を交互にswapする

模範解答 (Python)

import sys
from collections import deque

def solve():
    data = sys.stdin.read().split()
    pos = 0

    V = int(data[pos]); pos += 1
    E = int(data[pos]); pos += 1

    edges = []
    for _ in range(E):
        u = int(data[pos]); pos += 1
        v = int(data[pos]); pos += 1
        edges.append((u, v))

    K = int(data[pos]); pos += 1

    group = [0] * E
    k_limit = {}
    for i in range(E):
        g = int(data[pos]); pos += 1
        ki = int(data[pos]); pos += 1
        group[i] = g
        k_limit[g] = ki

    def make_uf(n):
        return list(range(n))

    def find(uf, x):
        while uf[x] != x:
            uf[x] = uf[uf[x]]
            x = uf[x]
        return x

    def union(uf, x, y):
        x, y = find(uf, x), find(uf, y)
        if x == y:
            return False
        uf[x] = y
        return True

    def is_independent_M1(S):
        uf = make_uf(V)
        for e in S:
            u, v = edges[e]
            if not union(uf, u, v):
                return False
        return True

    def can_add_M1(S_set, e):
        uf = make_uf(V)
        for f in S_set:
            union(uf, edges[f][0], edges[f][1])
        u, v = edges[e]
        return find(uf, u) != find(uf, v)

    def can_exchange_M1(S_set, e, f):
        new_S = (S_set - {e}) | {f}
        return is_independent_M1(new_S)

    def can_add_M2(S_set, e):
        g = group[e]
        cnt = sum(1 for f in S_set if group[f] == g)
        return cnt < k_limit[g]

    def can_exchange_M2(S_set, e, f):
        new_S = (S_set - {e}) | {f}
        for g, lim in k_limit.items():
            if sum(1 for x in new_S if group[x] == g) > lim:
                return False
        return True

    I = set()

    while True:
        X1 = set(e for e in range(E) if e not in I and can_add_M1(I, e))
        X2 = set(e for e in range(E) if e not in I and can_add_M2(I, e))

        common = X1 & X2
        if common:
            I.add(next(iter(common)))
            continue

        dist = {e: -1 for e in range(E)}
        prev = {}
        queue = deque()

        for s in X1:
            dist[s] = 0
            queue.append(s)

        found = None
        while queue and found is None:
            cur = queue.popleft()
            if cur not in I:
                for e in list(I):
                    if can_exchange_M2(I, e, cur) and dist[e] == -1:
                        dist[e] = dist[cur] + 1
                        prev[e] = cur
                        queue.append(e)
            else:
                for f in range(E):
                    if f not in I and dist[f] == -1 and can_exchange_M1(I, cur, f):
                        dist[f] = dist[cur] + 1
                        prev[f] = cur
                        if f in X2:
                            found = f
                            break
                        queue.append(f)

        if found is None:
            break

        path = []
        node = found
        while node in prev:
            path.append(node)
            node = prev[node]
        path.append(node)

        for node in path:
            if node in I:
                I.remove(node)
            else:
                I.add(node)

    print(len(I))

solve()

Step-by-Step 解説

1マトロイドとは
独立集合族 $\mathcal{I}$ が以下を満たす組み合わせ構造: (1) $\emptyset \in \mathcal{I}$、(2) $A \in \mathcal{I}, B \subseteq A \Rightarrow B \in \mathcal{I}$(遺伝性)、(3) $A, B \in \mathcal{I}, |A| < |B| \Rightarrow \exists e \in B \setminus A: A+e \in \mathcal{I}$(拡張性)。
2交換グラフの構築
現在の共通独立集合 $I$ に対し、有向グラフ $D$ を構築: $e \in I, f \notin I$ で $I - e + f \in \mathcal{I}_1$ なら弧 $e \to f$。$I - e + f \in \mathcal{I}_2$ なら弧 $f \to e$。
3BFS で拡大路を探す
「$M_1$ で追加可能な要素 $X_1$」から「$M_2$ で追加可能な要素 $X_2$」への最短路を BFS で探す。路が存在すれば $I$ を拡大できる。
4計算量
各反復で $|I|$ が 1 増える。最大 $r = \min(r_1, r_2)$ 回の反復。各反復で BFS: $O(E^2 \cdot \text{oracle})$ → 全体 $O(r \cdot E^2 \cdot \text{oracle})$。

よくあるミス

ミス原因正しい書き方
X1∩X2 のチェックを省く直接追加できる場合を見逃すBFS前にcommonをチェック
交換グラフの弧方向を逆にするM1とM2の役割を混同e→f (M1), f→e (M2)
路の更新でI⊕pathを間違える対称差を取り忘れ路上のノードをXOR的に切り替え

次のステップ

  • 発展問題: 重み付きマトロイド交差(最大重みの共通独立集合)
  • 応用: 有向全域木(Directed Spanning Tree / Matroid Union)

自己評価

自分の回答

気づき・メモ