ここまで、ソートと探索を実装してきました。今回は 「そもそも何が効率的に解けて、何が解けないのか」 という、コンピュータサイエンスの最も深い問い ── 計算理論の世界を覗きます。教科書の枠を超えて、現代の研究の地平までいきます。
① P と NP の違いを実例で分かる ② 「効率的には解けない(と信じられている)問題」が現実にある ③ 完璧を諦めて「近似で解く」という現代の主流の考え方 ④ Bloom Filter・ のような「確率的データ構造」を実装する ⑤ 2020 年代の新発見(量子優位性・新しいハッシュ等)
Step 0: 問題 ─ 100万ドル懸賞
P = 多項式時間で解ける問題(ソート、最短経路など)。NP = 多項式時間で検証できる問題(解を渡されれば正解か確かめられる)。 P = NP か? ── 解くのと検証するのが同じ難しさか。21世紀最大の未解決問題で、Clay Mathematics Institute が 100 万ドル懸賞 をかけています(ミレニアム懸賞問題)。
もし P=NP が証明されたら、現代の暗号(RSA, AES)はほぼ全部破られる。タンパク質構造予測・最適配送ルート計画・新薬開発・ ソルバが劇的に高速化する。世界が変わる。逆に P≠NP が証明されれば「効率解は原理的にない」と確定。多くの研究者は P≠NP と予想していますが、まだ証明されていない。
Step 1: NP-完全な問題を実装してみる
ナップサック問題:価値と重さを持つ品物の集合から、重量制限内で価値最大の組み合わせを選ぶ。現実的な品数(n=100)でも厳密解は爆発的に時間がかかる。
import time, random, itertools
random.seed(0)n, capacity = 22, 50 # 全数探索の限界くらいitems = [(random.randint(1, 20), random.randint(1, 30)) for _ in range(n)] # (weight, value)
# ① 総当たり O(2^n)def brute(items, cap): best = 0 for mask in range(1 << len(items)): w = sum(items[i][0] for i in range(len(items)) if mask >> i & 1) v = sum(items[i][1] for i in range(len(items)) if mask >> i & 1) if w <= cap: best = max(best, v) return best
# ② DP O(n × cap)def dp(items, cap): table = [0] * (cap + 1) for w, v in items: for c in range(cap, w - 1, -1): table[c] = max(table[c], table[c - w] + v) return table[cap]
# ③ 貪欲(価値/重さ比、近似)def greedy(items, cap): sorted_items = sorted(items, key=lambda x: -x[1] / x[0]) total = 0 for w, v in sorted_items: if cap >= w: cap -= w; total += v return total
t0 = time.perf_counter(); v_b = brute(items, capacity); t_b = time.perf_counter() - t0t0 = time.perf_counter(); v_d = dp(items, capacity); t_d = time.perf_counter() - t0t0 = time.perf_counter(); v_g = greedy(items, capacity); t_g = time.perf_counter() - t0print(f"総当たり: {v_b} ({t_b:.4f} s)")print(f"DP: {v_d} ({t_d:.4f} s)")print(f"貪欲近似: {v_g} ({t_g:.4f} s, ratio={v_g/v_b:.2%})")n=100 では総当たり 2^100 ≒ 10^30 通り、宇宙の年齢でも終わらない。だから現代は:① 厳密解が必要なら DP(疑似多項式時間)/ ② 大規模なら 近似アルゴリズム / ③ で解の良さそうな部分から探索(学習型解法)。
Step 2: 確率的データ構造 ─ Bloom Filter
「URL がスパムリストに入ってるか」を 100 億エントリで瞬時に判定したい。普通の set だと数百GBのメモリ。Bloom Filter は誤判定(false positive)を許容する代わりに、メモリを 1/100 〜 1/1000 に削減できます。
import hashlib, math
class BloomFilter: def __init__(self, n_items, fp_rate=0.01): # ビット配列のサイズと使うハッシュ関数の数 self.size = int(-n_items * math.log(fp_rate) / (math.log(2) ** 2)) self.k = int(self.size / n_items * math.log(2)) self.bits = [False] * self.size
def _hashes(self, x): h = hashlib.md5(str(x).encode()).digest() for i in range(self.k): yield int.from_bytes(h[i*2:(i+1)*2], 'big') % self.size
def add(self, x): for h in self._hashes(x): self.bits[h] = True
def contains(self, x): return all(self.bits[h] for h in self._hashes(x))
# 使ってみるbf = BloomFilter(10000, fp_rate=0.01)for x in range(10000): bf.add(f"item-{x}")
print("入ってる:", bf.contains("item-42")) # → Trueprint("入ってない:", bf.contains("nope")) # → ほぼ Falsefp = sum(bf.contains(f"unknown-{x}") for x in range(10000)) / 10000print(f"誤検出率: {fp:.2%} (目標 1.00%)")Cassandra / HBase の SSTable、CDN のキャッシュ判定、Bitcoin の SPV クライアント、Chrome の悪質URLチェック ── どれも Bloom Filter かその親戚を使っています。
Step 3: HyperLogLog ─ 「ユニーク数を 1KB で数える」
「今日サイトに来たユーザー数(重複なし)」を10億ユーザーから求めたい場合、set だと数十GB。HyperLogLog は 1.5KB 程度のメモリで誤差 1% 以内で数えられます。Google・Reddit・Redis などで実用。
import hashlib, math
class HyperLogLog: def __init__(self, p=14): # p=14 → 16384 バケット → 約 0.81% 誤差 self.p = p self.m = 1 << p self.M = [0] * self.m
def add(self, x): h = int(hashlib.md5(str(x).encode()).hexdigest()[:16], 16) idx = h & (self.m - 1) rest = h >> self.p # 末尾の 0 の個数 + 1 rho = (rest & -rest).bit_length() if rest else 64 - self.p self.M[idx] = max(self.M[idx], rho)
def estimate(self): alpha = 0.7213 / (1 + 1.079 / self.m) Z = 1.0 / sum(2 ** -m for m in self.M) return alpha * self.m * self.m * Z
# 1000万ユニーク要素を投入hll = HyperLogLog(p=14)true_count = 10_000_000for x in range(true_count): hll.add(x)estimated = hll.estimate()print(f"真の値: {true_count}")print(f"推定: {estimated:.0f} 誤差: {abs(estimated-true_count)/true_count:.2%}")Step 4: 2010〜2020 年代の新発見・標準化 年表
「ソートも探索も完成された分野」と思われがちですが、実は今も毎年新しい論文・標準が出ています。直近10数年分の主な動きを年表で。
| 年 | 発見・採用 | 発明者・発表元 | ポイント |
|---|---|---|---|
| 2013 | HyperLogLog++ | ユニーク数推定の改良版。Redis 7 で改良が入る | |
| 2014 | Cuckoo Filter | Fan, Andersen, Kaminsky, Mitzenmacher (CMU) | Bloom より省メモリ・削除可能・誤検出率低 |
| 2016 | Malkov & Yashunin | 高次元ベクトル空間の近似最近傍探索。現代ベクトルDBの中核 | |
| 2018 | Pdqsort(pattern-defeating quicksort) | Orson Peters | Rust 標準ソート。クイックソートの敵対的入力を回避 |
| 2018 | Learned Index Structures | Kraska et al.(Google) | 機械学習で B-tree を置き換える研究 |
| 2019 | Quantum Supremacy 主張(Sycamore) | 53 量子ビットで「古典では実用時間で解けない」課題を解決と主張(再現性で議論続く) | |
| 2022 | Powersort | Munro & Wild | Timsort のマージ戦略を数学的に最適化。後に Java 21 で標準採用 |
| 2024 | Java 21 が Powersort を標準採用 | Oracle / OpenJDK | 20年使われた Timsort 系統に改善余地があったと再認識 |
| 2024〜 | LSH の継続的改良 | 学術界(毎年) | ベクトル検索の数学的下限を更新する論文が継続 |
「ソートはもう完成された分野」と思われがちですが、実は今でも論文が出ています。コンピュータの使い方が変われば、最適なアルゴリズムも変わる。クラウド・GPU・量子・ストリーミング ── 計算機が新しくなるたび、CS は新しい問題を見つけます。
自由研究の提案
① 自分の SAT ソルバ: 「3SAT 問題」を貪欲・WalkSAT・DPLL の3手法で実装。NP-完全問題の難しさを肌で感じる。 ② Cuckoo Filter を実装: Bloom より新しい確率的構造。要素削除ができる、誤検出率が低い、を確認する。 ③ Learned Index: ソート済みデータの位置を線形回帰で「予測」してから。普通の二分探索より速くなるか実験。 ④ P vs NP の証明を試す: 一晩あれば証明できそうな問題ではないですが、「なぜ難しいのか」を考えるだけでも CS の本質に近づきます。Cook-Levin の定理(任意の NP 問題は SAT に還元可能)あたりが入り口。
ここまでを振り返ると
EP.01 から EP.16 まで、関数のグラフから核分裂、ソート、計算理論まで、Python が「世界を理解するための最高の道具」だということを示してきました。プログラミングは数学・物理・化学・統計・ と地続き。 「もっとやりたい」を感じてくれたなら、それが何より。次のテーマブロックでは、シミュレーション物理(流体・生態系・遺伝アルゴリズム)、データサイエンス応用、CSの最新研究などを扱う予定です。シリーズはこの先も続きます。
この記事の感想を教えてください
あなたの 1 クリックで、本当にこの記事は更新されます。「もっと詳しく」「続編希望」が一定数集まった記事は、 ふくふくが 実際に内容を拡充したり続編記事を公開 します。 送信したリアクションはお使いのブラウザに記録され、再カウントされません。