Day 160-Q2 — 2026-09-25 — MinHash + LSH(帯分割)で近似重複の候補を絞り、厳密な Jaccard で検証する

2026-09-25 赤色 Master (Phase 8+) ★★★★★★★★★ 類似検索。全ペア $O(N^2)$ を避け、「見逃し(偽陰性)」と「無駄な検証(偽陽性)」を帯の形 $(b,r)$ で調整する

問題

$N$ 個の整数集合 $S_1,\dots,S_N$ と、整数 $b,r$、しきい値 $\theta=p/q$ が与えられる。$k=b\cdot r$ とし、$P=2^{61}-1$ として $i=0,\dots,k-1$ のハッシュ関数を

$$h_i(x)=(a_i x+c_i)\bmod P,\quad a_i=1+(\mathrm{sm}(2i+1)\bmod (P-1)),\quad c_i=\mathrm{sm}(2i+2)\bmod P$$

で定める($\mathrm{sm}$ は splitmix64 の finalizer。Day159-Q4 と同じ関数)。

1. 署名: $\mathrm{sig}_j[i]=\min_{x\in S_j}h_i(x)$(長さ $k$)
2. LSH: 署名を長さ $r$ の $b$ 本の帯に切る。ある帯 $t$ で $\mathrm{sig}_u[tr..tr+r-1]=\mathrm{sig}_v[tr..tr+r-1]$ となる組 $u<v$ を 候補 とする
3. 検証: 候補のうち厳密な Jaccard 係数 $J(S_u,S_v)=\frac{|S_u\cap S_v|}{|S_u\cup S_v|}\ge\theta$ を満たすものを数える

候補の組数 $C$ と、検証を通った組数 $D$ を出力せよ。

入力形式

N b r p q
m_1 e_{1,1} ... e_{1,m_1}
...
m_N e_{N,1} ... e_{N,m_N}

制約

#条件
1$2\le N\le 10^4$、$1\le b\cdot r\le 100$、$0\le p\le q\le 100$、$q\ge1$
2$1\le m_j$、$\sum m_j\le 2\times10^5$、$0\le e<10^9$、各行の要素は相異なる
3候補の組数 $C\le 2\times10^5$ が保証される

入出力例

入力例1

5 4 2 1 2
6 1 2 3 4 5 6
6 1 2 3 4 5 7
4 1 2 3 8
3 100 200 300
3 100 200 301

出力例1

3 1

(厳密な Jaccard は $(1,2)$: 5/7=0.714、$(1,3),(2,3)$: 3/7=0.429、$(4,5)$: 2/4=0.5。$k=8$ 本の最小値の一致は $(1,2)$ が 7/8、$(1,3)$ が 4/8、$(4,5)$ が 2/8。帯(2 本ずつ 4 帯)が丸ごと一致したのは $(1,2)$ の 3 帯と $(1,3),(2,3)$ の 1 帯 → 候補 3 組。検証で $(1,3),(2,3)$ は落ち $D=1$。**$(4,5)$ は $J=0.5\ge\theta$ なのに候補にならなかった(偽陰性)**——これが LSH の代償)

MinHash は Broder「On the resemblance and containment of documents」(SEQUENCES 1997)で AltaVista の近似重複検出に使われた手法、帯分割は Leskovec・Rajaraman・Ullman『Mining of Massive Datasets』3 章(http://www.mmds.org/ 、2026-09-25 参照)の定式化に従う。

概念図:LSH の S 字曲線(理論と実測)

1 組が候補になる確率 1 − (1 − J^r)^b(線 = 理論、点 = 実測 b=20, r=5) 0.0 0.0 0.2 0.2 0.4 0.4 0.6 0.6 0.8 0.8 1.0 1.0 Jaccard 係数 J 候補になる確率 θ = 0.5 (b,r)=(10,10) 閾値 0.79 再現率 20.7% (b,r)=(20,5) 閾値 0.55 再現率 95.3% (b,r)=(25,4) 閾値 0.45 再現率 99.7% 再現率は 1000 クラスタ×10 件 (真の組 45,000)での実測 θ より左に曲線を置き、 偽陽性は厳密計算で落とす

ヒント(クリックで段階的に開示)

ヒント1(方向性)

一様ランダムな置換 $\pi$ で $\min\pi(A)=\min\pi(B)$ となる確率は、ちょうど $J(A,B)$($A\cup B$ の中で $\pi$ が最小の要素が $A\cap B$ に入る確率)。$k$ 本の最小値の一致率は $J$ の不偏推定で、標準誤差 $\sqrt{J(1-J)/k}$。

ヒント2(アプローチ)

署名を全ペアで比べると $O(N^2k)$。帯 $t$ ごとに「$r$ 個の値の組」をキーに辞書へ入れれば、同じバケツの組だけが候補になる。1 組が候補になる確率は $1-(1-J^r)^b$ の S 字曲線で、しきい値はおよそ $(1/b)^{1/r}$。

ヒント3(誘導)
sig = [[min((a * x + c) % P for x in S) for a, c in zip(A, C)] for S in sets]
cand = set()
for t in range(b):
    buckets = {}
    for j in range(n):
        buckets.setdefault(tuple(sig[j][t*r:(t+1)*r]), []).append(j)
    for g in buckets.values():
        ...  # g の中の全組を cand に(帯をまたいだ重複は set で消える)
# 検証: q * |A∩B| >= p * |A∪B|(浮動小数を使わない)

模範解答 (Python)

模範解答を表示する(まず自力で解いてから)
import sys

P = (1 << 61) - 1
MASK = (1 << 64) - 1


def splitmix64(x):
    x = (x + 0x9E3779B97F4A7C15) & MASK
    x = ((x ^ (x >> 30)) * 0xBF58476D1CE4E5B9) & MASK
    x = ((x ^ (x >> 27)) * 0x94D049BB133111EB) & MASK
    return x ^ (x >> 31)


def main():
    data = sys.stdin.buffer.read().split()
    n, b, r, p, q = map(int, data[:5])
    k = b * r
    A = [1 + splitmix64(2 * i + 1) % (P - 1) for i in range(k)]
    C = [splitmix64(2 * i + 2) % P for i in range(k)]
    sets = []
    pos = 5
    for _ in range(n):
        m = int(data[pos]); pos += 1
        sets.append(set(map(int, data[pos:pos + m]))); pos += m

    # 1) MinHash 署名: sig[i] = min_x (A[i]*x + C[i]) mod P
    sigs = []
    for S in sets:
        sigs.append([min((a * x + c) % P for x in S) for a, c in zip(A, C)])

    # 2) LSH: 帯 t の r 個をキーにバケツへ。同じバケツの組が候補
    cand = set()
    for t in range(b):
        buckets = {}
        for j in range(n):
            key = tuple(sigs[j][t * r:(t + 1) * r])
            buckets.setdefault(key, []).append(j)
        for g in buckets.values():
            for x in range(len(g)):
                for y in range(x + 1, len(g)):
                    cand.add((g[x], g[y]))

    # 3) 候補だけ厳密な Jaccard で検証: |A∩B|/|A∪B| >= p/q  <=>  q*|A∩B| >= p*|A∪B|
    ok = 0
    for u, v in cand:
        inter = len(sets[u] & sets[v])
        union = len(sets[u]) + len(sets[v]) - inter
        if q * inter >= p * union:
            ok += 1
    print(len(cand), ok)


main()

計算量: 署名 $O(k\sum m_j)$、LSH $O(bN + C)$、検証 $O(\sum_{\text{候補}}\min(|S_u|,|S_v|))$。全ペア厳密計算の $O(N^2\cdot\bar m)$ と違い、**$N$ について線形**(候補数が小さい限り)。

Step-by-Step 解説

1なぜ最小値の一致確率が Jaccard になるか

$A\cup B$ の各要素が置換で「最小」になる確率は等しい。最小の要素が $A\cap B$ にあれば両方の最小値が一致し、$A\setminus B$ か $B\setminus A$ にあれば一致しない。よって $\Pr=|A\cap B|/|A\cup B|$。本問は真の置換の代わりに $(ax+c)\bmod P$ を使う。これは min-wise independent ではない(近似)が、実測で偏りは $\pm0.005$ 以内($k=100$, 各 200 組)。

2推定誤差 $\sqrt{J(1-J)/k}$

一致数は二項分布 $\mathrm{Bin}(k,J)$。実測 RMSE($k=100$): $J=0.2$ で 0.0377(理論 0.0400)、$J=0.5$ で 0.0479(0.0500)、$J=0.8$ で 0.0384(0.0400)。**$k=100$ では $J$ を $\pm0.05$ 程度でしか当てられない**ので、最終判定は厳密計算で行う(本問の手順 3)。

3帯分割の S 字曲線

帯 1 本が丸ごと一致する確率 $J^r$、$b$ 本のどれかが一致する確率 $1-(1-J^r)^b$。$r$ を増やすと曲線が右(厳しく)、$b$ を増やすと左(緩く)に動く。$b=20,r=5$ の実測(各 300 組): $J=0.3$ で 0.060(理論 0.047)、$0.5$ で 0.487(0.470)、$0.7$ で 0.977(0.975)。しきい値の目安 $(1/20)^{1/5}=0.549$。

4$(b,r)$ の選び方 = 見逃しと検証コストのトレードオフ

近似重複データ(1000 クラスタ × 10 件、各コピーは元の 20 要素のうち 2 個を入れ替え、真に $J\ge0.5$ の組 45,000)での実測:

$(b,r)$しきい値 $(1/b)^{1/r}$候補 $C$真の組の再現率時間
(10, 10)0.799,33120.7%3.91 s
(20, 5)0.5542,86895.3%3.91 s
(25, 4)0.4544,84699.7%4.09 s
全ペア厳密—5×10^7 組100%27.68 s

目的のしきい値 $\theta$ より 曲線の中心を少し左に置く($b$ を増やす)と見逃しが減り、増えた偽陽性は手順 3 の検証で落とせる。

迷った他案と採らなかった理由

案計算量採否理由
全ペア厳密 Jaccard$O(N^2\bar m)$比較用$N=10^4$ で 27.7 s(実測)。$N=2000$ では 1.13 s と LSH(0.75 s)に近く、**$N$ が数千までなら全ペアで十分**
転置索引 + 共通要素数カウント(prefix filtering, AllPairs)出力依存扱わない厳密で偽陰性 0。しきい値が高く集合が小さいなら有力。本問は LSH の確率的トレードオフを学ぶため除外
MinHash + 帯分割 LSH(本解)$O(k\sum m+C)$採用再現率を $(b,r)$ で連続的に調整できる
1 本の MinHash($k$ 個の置換ではなく bottom-$k$ スケッチ)$O(\sum m\log k)$扱わないハッシュ 1 本で済み速いが、帯分割とは組み合わせにくい
SimHash(Charikar 2002、コサイン類似度)$O(\sum m)$不採用類似度の定義が Jaccard ではない。重み付き特徴向け

前提が変わったら: 偽陰性が許されない(法務の重複調査など)なら prefix filtering などの厳密法。データが 10^8 件なら署名を分散計算し帯ごとにシャッフルする(MapReduce)形が MMDS の標準構成。

よくあるミス

ミス原因正しい書き方
帯ごとに見つけた組を足し合わせる複数の帯で一致した組を重複して数えるset で組を 1 回だけ数える
Jaccard を浮動小数で比べる$5/7\ge0.5$ は安全でも、境界 $J=\theta$ で誤差が出るq * inter >= p * union と整数で比較
署名の値を $\bmod 2^{32}$ に縮める異なる最小値が衝突し、一致率が $J$ より大きくなる$P=2^{61}-1$ のまま比べる
$h_i(x)=(a_ix+c_i)\bmod P$ で $a_i=0$ を許す全要素が同じ値になり常に一致$a_i\in[1,P-1]$
最小値ではなく「最小値を与える要素」を比べる同じ意味だが、ハッシュ値の方が比較が安いどちらでもよいが、本問は値で定義

次のステップ

  • 発展1: 帯のしきい値を $\theta$ に合わせる $(b,r)$ を、偽陽性の検証コストと偽陰性の損失の重み付き和が最小になるように選ぶ(MMDS 3.4.3)
  • 発展2: 文書を文字 $k$-gram(shingle)に変換してから本問に流す。$k=5$ と $k=9$ で近似重複の検出率を比べる
  • 扱わなかった範囲: 重み付き Jaccard(ICWS)、b-bit MinHash による署名の圧縮、LSH Forest

検証ログ(本セッションで実行した数値)

  • 入力例1: 最小値の一致ベクトル $(1,2)$=[1,1,1,1,0,1,1,1](帯一致 T,T,F,T)、$(1,3)$=[1,0,1,1,0,1,0,0](帯 2 のみ)、$(4,5)$=[0,1,0,0,1,0,0,0](帯一致なし)。出力 3 1
  • 素朴な全ペア帯比較との一致(実測): ランダム 200 ケース($N\le15$、要素の値域 15 以下で衝突多め)で不一致 0
  • 推定誤差(実測, $k=100$, $|A|=|B|=100$, 各 200 組): 上の Step 2 のとおり、平均の偏り $+0.0002/-0.0051/+0.0052$
  • S 字曲線(実測, $b=20, r=5$, 各 300 組): $J=0.2$: 0.003(0.006)、0.4: 0.197(0.186)、0.6: 0.787(0.802)、0.8: 1.000(1.000)
  • 規模(実測): $N=2000$($\sum m=4\times10^4$)で LSH 0.75 s・再現率 8590/9000=95.4% vs 全ペア 1.13 s。$N=10^4$ は Step 4 の表
  • 出所ラベル: 誤差・曲線・再現率・秒数は実測(Python 3.14.7)。$\Pr=J$ と $1-(1-J^r)^b$ は Broder 1997・MMDS 3 章の導出。AllPairs・SimHash・ICWS は文献上の比較で本セッションでは実装していない
  • 検証スクリプト: スクラッチパッドの d160/q2_test.py・q2_test2.py・q2_test3.py

自己評価

自分の回答

気づき・メモ