0
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

量子探索Groverのアルゴリズムを解説!

Last updated at Posted at 2025-05-17

7537d75e-5375-464d-be91-1cbdf21511d1.png

0. はじめに

こんにちは!

最近、仕事や研究で量子コンピューティングの世界に足を踏み入れた方、あるいはこれからその可能性を探求しようとされている方が、ますます増えているように感じます。何を隠そう、私もその一人です。この分野への関心は日に日に高まっており、「量子コンピュータって、具体的にどんなことができるんだろう?」「有名なアルゴリズムがあるとは聞くけれど、一体どんな仕組みで動いているの?」といった純粋な疑問や大きな期待をお持ちの方も多いことでしょう。

本記事では、まさにそのような皆さまに向けて、量子計算の代表的なアルゴリズムの一つである「Groverの探索アルゴリズム」を取り上げます。初学者の方の目線に立ち、その基本概念から丁寧に、そしてできる限り分かりやすく解説することを目指しました。

「Groverのアルゴリズムという名前は聞いたことがあるけれど、まだ核心を掴めていない…」という方から、「具体的な動作原理はもちろん、Python (Qiskit) を使った実装方法まで理解したい!」という意欲的な方まで、幅広い読者の皆さまにとって「読んでよかった!」と思っていただけるような情報をお届けできればと思います。

※ 内容に誤りやおかしな点などもございましたら、ぜひご指摘いただけますと幸いです。

この記事を読むことで、以下のことがわかります:

  • Groverのアルゴリズムが解決しようとしている「探索問題」の概要
  • アルゴリズムの直感的な流れと、古典計算と比較した優位性
  • 各ステップ(初期化、オラクル、増幅)の動作原理と数式による裏付け
  • Python (Qiskit) を使った具体的なシミュレーション方法と、振幅・確率の変化の視覚化
  • Groverの反復回数や「行き過ぎ」現象の幾何学的な理解

1. 「探索問題」とは?Groverのアルゴリズムの意義

1.1. 「正解」を見つけ出す!

c5af4034-6fcc-427a-b344-ff82cc8cb63a.png

私たちの周りには「探索問題」が溢れています。例えば、

  • 膨大な電話帳から特定の人の電話番号を探す
  • たくさんの鍵の中から、たった一つの正しい鍵を見つける
  • データベースから特定の条件に合致するデータを探し出す

このような問題で、もしデータが整理されていなければ、一つ一つ順番に調べていくしかありません。
N個の項目の中から1つの「正解」を見つける場合、

古典コンピュータでは:
* 運が悪ければ、最大$N$回の調査が必要。
* 平均しても約$N/2$回の調査が必要。
* 計算量としては、$O(N)$ と表現されます。

1.2. 量子の世界では探索が速くなる!

a4b75996-0177-492b-a228-986973690508.png

ここで登場するのがGroverのアルゴリズムです。量子コンピューティングの力を借りることで、この探索を高速化できます。

  • Groverのアルゴリズムでは:
    • 約$\sqrt{N}$回の調査で「正解」を見つけられる可能性が高まります!
    • 計算量としては、$O(\sqrt{N})$ と表現されます。
    • 例えば、100万個のデータなら、古典的には最悪100万回かかるところを、Groverなら約1000回で済むイメージです。これは大きな違いですね!
      19d9d27e-081f-4000-8fdf-cd479c24094b.png

今回は、N=16個の項目(0~15までの番号で、4量子ビットで表現)の中から、たった一つの「当たり」の番号(例: 5<0101>)を見つけ出すケースを具体的に見ていきます。
古典的なら最悪15回かかりますが、Groverのアルゴリズムなら、後で詳しく見るように、たったの約3回の「量子的な調査」で高い確率で見つけ出せます。
3bfab980-4046-4121-a67a-b62ae593cfeb.png

2. Groverのアルゴリズムの流れ

Groverのアルゴリズムは、直感的には以下のステップで進みます。

  1. 全ての候補を同時に候補として扱う (均等な重ね合わせ)

    • 最初に、0から15までの全ての番号を「同時に」候補として扱えるように、量子的な重ね合わせ状態にします。
    • この時点では、どの番号も同じだけ「ありえそう」な状態です。
  2. 「当たり」はこれだ (オラクルで印つけ🎯)

    • 「当たり」と予想される番号(探索対象)だけに印をつけます。
    • 具体的に言うと、この「当たり」の番号の振幅の符号だけを反転させます。他の番号の振幅はそのままです。
  3. 「当たり」をもっと目立たせていく! (振幅増幅📢)

    • 印がついた「当たり」の番号が、他の番号よりも目立つように、振幅を大きくしていきます
    • これは、全体の振幅の平均値を利用した巧妙な操作(平均値まわりの反転)で行われます。
  4. どんどん強調! (繰り返し🔄)

    • ステップ2(印つけ)とステップ3(振幅増幅)のペアを数回繰り返します。
    • 繰り返すたびに、「当たり」の番号の振幅はどんどん大きくなり、他の番号の振幅は相対的に小さくなっていきます。
  5. 答えを発見! (測定)

    • 最後に測定を行うと、振幅が最も大きくなった(一番目立っている)「当たり」の番号が、非常に高い確率で出てきます。

まとめると、Groverのアルゴリズムの全体的な処理フローは以下の模式図のようになります。
ce3bd5fa-46ee-489d-93a6-da277c999479.png

それでは、各ステップを数式とPython (Qiskit) でのシミュレーション結果(今回は具体例として、$n=4$, 探索対象を0101とします)と共に詳しく見ていきましょう。

3. 詳細解説: Groverのアルゴリズムの各ステップ

3.1. ステップ0: 初期状態の準備 - 全状態の均等な重ね合わせ

  • 目標: 全ての可能な状態を、同じ「可能性の大きさ(振幅)」で同時に存在させること。

  • 操作: 初期状態が$|00\dots0\rangle$である$n$個の量子ビットのそれぞれに、アダマールゲート(H)を作用させます。

  • 数式表現:
    初期状態$|s\rangle$は以下のように表されます。
    $$|s\rangle = H^{\otimes n} |0\rangle^{\otimes n} = \frac{1}{\sqrt{N}} \sum_{x=0}^{N-1} |x\rangle$$
    ここで、$N=2^n$ は全状態の数です。各状態$|x\rangle$の振幅は $\frac{1}{\sqrt{N}}$ となります。

  • $n=4$ の場合 ($N=16$, 探索対象$|w\rangle = |0101\rangle$):

    • 各状態の初期振幅は $1/\sqrt{16} = 1/4 = 0.25$ です。
    • 初期状態での$|0101\rangle$の振幅も $0.2500$、確率は $(0.25)^2 = 0.0625$ です。
  • グラフで見る初期状態 (実数振幅):
    26da844e-1f5b-4bff-8880-8a8982b5536e.png

    初期状態だと、全16状態の振幅が等しく+0.25です。(状態 $|0101\rangle$: 実数振幅=0.2500, 確率=0.0625)

  • グラフで見る初期状態 (確率):
    8ea0d32c-bd8b-4d70-bad5-b6acdeaed4af.png

    初期状態の確率。全16状態の確率が等しく0.0625です。

3.2. ステップ1: オラクル - 解に印をつける

  • 目標: 探している解の状態 $|w\rangle$ (ここでは $|0101\rangle$) だけを区別すること。

  • 操作: オラクル演算子 $U_w$ は、ターゲット状態 $|w\rangle$ の振幅の符号のみを反転させます。

  • 数式表現: $U_w |x\rangle = (-1)^{f(x)} |x\rangle$ (ここで $f(w)=1$, $f(x \neq w)=0$)
    状態変化: $|s'\rangle = U_w |s\rangle = \frac{1}{\sqrt{N}} \left(-|w\rangle + \sum_{x \neq w} |x\rangle \right)$

  • $n=4$, ターゲット $|0101\rangle$ の場合:

    • $|0101\rangle$ の振幅: $0.25 \rightarrow -0.25$
    • 他の状態の振幅: $0.25$ (変化なし)
    • オラクル適用後の$|0101\rangle$の振幅は $-0.2500$、確率は $(-0.25)^2 = 0.0625$ のままです。
  • グラフで見るオラクル適用後 (実数振幅):
    ccb9662b-345b-484d-951b-173a2d9e386b.png
    オラクル適用後。状態$|0101\rangle$の振幅のみが-0.25に反転。(状態 $|0101\rangle$: 実数振幅=-0.2500, 確率=0.0625)

  • グラフで見るオラクル適用後 (確率):
    ce88a453-41ca-4217-9f54-a8883375d7d6.png
    オラクル適用後の確率。符号は反転しましたが、各状態の確率はまだ0.0625のままです。

3.3. ステップ2: 増幅 - 印をつけた状態の振幅を増やす

  • 目標: オラクルで符号反転されたターゲット状態の振幅を、他の状態に比べて大きくすること。

  • 操作 (平均値まわりの反転): 増幅演算子(ディフューザー)$U_s$ を作用させます。
    $$ U_s = 2|s\rangle\langle s| - I $$
    ここで $|s\rangle$ は初期の均等な重ね合わせ状態、$I$ は恒等演算子です。
    この操作は、現在の各状態の振幅 $a_x$ を、全振幅の算術平均 $\mu = \frac{1}{N}\sum_x a_x$ を使って、新しい振幅 $a_x' = 2\mu - a_x$ へと変換します。

    • 直感的イメージ: 全振幅の「平均ライン」を引き、各振幅をそのラインに対して鏡映しにするイメージです。
      • ターゲット状態の振幅はオラクルで負になり、平均よりずっと下に位置します。これを反転すると、平均よりずっと上にジャンプし、振幅が大きく正になります。
      • 他の状態の振幅は平均に近いので、反転させても変化は比較的小さく、多くは減少します。
  • 「平均値まわりの反転」の模式図:
    9018173a-7789-4171-9e34-5a56a22a6300.png
    振幅の平均値(青い点線)に対して、各振幅(薄赤色)が反転され、新しい振幅(黄緑色)になる様子。解(W)の振幅が大きく伸びています。

  • $n=4$, 1回目の増幅後:

    • 1回反復後の$|0101\rangle$の振幅は $0.6875$、確率は $(0.6875)^2 \approx 0.4727$ になります。
      (これらの値は、Pythonスクリプトで $U_s = 2|s\rangle\langle s| - I$ に対応するディフューザー(グローバル位相を調整したもの)を使った場合の値です。)
  • グラフで見る1回反復後 (実数振幅):
    09d1502b-4ed5-4cf9-a776-2fcb702689ec.png
    1回目のGrover反復後。状態$|0101\rangle$の振幅が大きく正の値に増加!(状態 $|0101\rangle$: 実数振幅=0.6875, 確率=0.4727)

  • グラフで見る1回反復後 (確率):
    4819ac8f-fb45-4179-9615-27621786ada6.png
    1回目のGrover反復後の確率。状態$|0101\rangle$の確率が顕著に高くなりました。

4. 反復によるさらなる増幅と「行き過ぎ」現象

4.1. 反復回数と最適値

「オラクル($U_w$)」と「増幅($U_s$)」の操作を1セットとして、これを複数回繰り返します。
1つの解を探す場合の最適な反復回数 $k$ は、近似的に
$$ k \approx \frac{\pi}{4}\sqrt{N} $$
で与えられます。

なぜこの回数?なぜ90度を目指すの?(直感的説明)
以下のGif画像のように、Groverのアルゴリズムは、状態ベクトルを「解の状態」に向かって少しずつ回転させる操作と見なせます。
grover_rotation_N16_overshoot_detailed.gif

  • 目標は90度!: 「解」の方向を真上(90度)とすると、ベクトルが真上を向けば解の振幅(確率)が最大になります。
  • スタートは遠い: 初期状態のベクトルは、解の方向からわずかに(角度 $\theta_0$ で、$\sin\theta_0 = 1/\sqrt{N}$なので、$\theta_0 \approx 1/\sqrt{N}$ ラジアンだけ)傾いています。
  • 少しずつ回転: 1回のGroverステップで、ベクトルは一定の小さな角度 ($2\theta_0 \approx 2/\sqrt{N}$ ラジアン) だけ解の方向に回転します。
  • なぜ $\frac{\pi}{4}\sqrt{N}$ 回?:
    目標は、状態ベクトルを初期のほぼ横向き(解の方向から見てほぼ0度に近い)から真上(解の方向と完全に一致する90度、つまり $\pi/2$ ラジアン)まで回転させることです。
    したがって、必要な繰り返し回数 $k$ は、おおよそ以下のように計算できます。
    $$ k \approx \frac{\text{目標とする総回転角}}{\text{1ステップあたりの回転角}} \approx \frac{\pi/2 \text{ ラジアン}}{2/\sqrt{N} \text{ ラジアン}} = \frac{\pi}{4}\sqrt{N} $$
    • このように、$\pi/4$ という係数は、目標角度(90度)と1ステップの回転角($2\theta_0 \approx 2/\sqrt{N}$)の比率から自然に出てきます。

$n=4$ ($N=16$) の場合は?
最適反復回数 $\approx \frac{\pi}{4}\sqrt{16} = \frac{\pi}{4} \times 4 = \pi \approx 3$ 回ということになります。

4.2. 反復ごとの変化 (状態$|0101\rangle$の振幅と確率)

以下の表は、反復回数ごとの状態$|0101\rangle$の実数振幅と確率の変化を示します(Pythonスクリプトの出力に基づき、$U_s = 2|s\rangle\langle s| - I$ に対応するディフューザーを使用した場合)。

反復回数 実数振幅 確率 備考
0回 (初期状態) 0.2500 0.0625
1回 0.6875 0.4727
2回 0.9531 0.9084
3回 0.9805 0.9613 (最適回数)
4回 0.7852 0.6165 (最適超過)

(注: 上記の値はPythonスクリプトで $U_s=2|s\rangle\langle s|-I$ を実装した場合の理論値です。)

各反復後のグラフ:

  • 2回反復後 (実数振幅):
    a4ce9af1-e33c-4d2a-89fa-10750d343b2b.png
    2回反復後。状態$|0101\rangle$の振幅がさらに増加。
  • 3回反復後 (最適・実数振幅):
    ac5d95f7-2a57-48e5-aecf-1e10b6c7f678.png
    3回反復後(最適)。状態$|0101\rangle$の振幅がほぼ1に!

4.3. 「行き過ぎ」現象

最適な反復回数を超えて操作を続けると、解の状態の振幅は逆に減少し始めます。これは、幾何学的に状態ベクトルが解の方向(90度)を「行き過ぎて」しまうためです。

  • 「行き過ぎ」の幾何学的イメージのアニメーション(再掲):
    grover_rotation_N16_overshoot_detailed.gif

  • 4回反復後(最適超過・実数振幅):
    ae24fb9c-3ad0-4381-b33a-dfa445b50534.png
    4回反復後(最適超過)。状態$|0101\rangle$の振幅が減少しています。

5. 測定

適切な回数(最適回数)だけGrover反復を行った後、量子状態を測定します。
各状態が観測される確率は、その状態の振幅の絶対値の2乗に比例します。
最適回数後には解の状態の振幅(とその2乗である確率)が非常に大きくなっているため、高い確率で解の状態が観測されることになります(今回は96.13%)。

6. QiskitによるGroverアルゴリズムの実装($n=4$の例)

これまでの説明で用いたグラフや値を生成するためのPythonスクリプトを示します(私が雑に書いたプログラムを生成AIを使って綺麗にしてもらってます)。量子回路の実装には、qiskitライブラリを使用しました。
また、探索対象の項目は marked_item_binary = '0101' としています。

7537d75e-5375-464d-be91-1cbdf21511d1.png
↑本プログラムで実装した量子回路図

# Qiskit (v1.xを想定)

# Qiskitライブラリのインストール(1回だけ実行)
!pip install qiskit
!pip install pylatexenc
!pip install japanize_matplotlib

import matplotlib
import japanize_matplotlib
matplotlib.use('Agg')  # 図を保存するための非対話型バックエンド

import numpy as np
import matplotlib.pyplot as plt


from qiskit import QuantumCircuit, transpile
from qiskit.quantum_info import Statevector
from qiskit.visualization import plot_histogram
from qiskit.providers.basic_provider import BasicSimulator 
from qiskit.circuit.library import Diagonal

import os

# プロット保存ディレクトリ (適宜変更してください)
plots_dir = "grover_article_plots" 
if not os.path.exists(plots_dir):
    os.makedirs(plots_dir)

# --- n=4 の例パラメータ ---
n = 4 # 量子ビット数
N = 2**n # 全状態数
marked_item_binary = '0101' # 探索対象の状態 ★ここを変更すると探索対象が変わります
marked_item_int = int(marked_item_binary, 2)

# --- 状態実数振幅プロット関数 ---
def plot_state_real_amplitudes(statevector, title_prefix, filename, marked_label=None, marked_amplitude_val=None, marked_prob_val=None):
    if not isinstance(statevector, Statevector):
        sv = Statevector(statevector)
    else:
        sv = statevector

    amplitudes_complex = sv.data 
    amplitudes_real = np.real(amplitudes_complex)
    state_labels = [format(i, f'0{n}b') for i in range(N)]

    title = title_prefix
    if marked_label:
        title += f"\n状態 |{marked_label}>: 実数振幅={marked_amplitude_val:.4f}, 確率={marked_prob_val:.4f}"

    fig, ax = plt.subplots(figsize=(12, 6))
    bars = ax.bar(state_labels, amplitudes_real, color='cornflowerblue', width=0.8)

    if marked_label in state_labels:
        marked_idx_for_plot = state_labels.index(marked_label)
        bars[marked_idx_for_plot].set_color('salmon')
        
    ax.set_xlabel("計算基底状態")
    ax.set_ylabel("実数振幅")
    ax.set_title(title)
    ax.set_xticks(np.arange(N))
    ax.set_xticklabels(state_labels, rotation=90)
    ax.axhline(0, color='black', linewidth=0.8)
    
    min_amp = np.min(amplitudes_real)
    max_amp = np.max(amplitudes_real)
    current_ylim_min = min(min_amp * 1.1 if min_amp < 0 else min_amp -0.1, -0.1)
    current_ylim_max = max(max_amp * 1.1 if max_amp > 0 else max_amp +0.1, 0.1)
    # Y軸の範囲が極端に狭くならないように調整
    if current_ylim_max - current_ylim_min < 0.2:
        current_ylim_min -= 0.1
        current_ylim_max += 0.1
    ax.set_ylim(current_ylim_min, current_ylim_max)
    plt.tight_layout() 

    filepath = os.path.join(plots_dir, filename)
    fig.savefig(filepath)
    plt.close(fig)
    print(f"実数振幅プロットを保存しました: {filepath}")

# --- 状態確率プロット関数 ---
def plot_state_probabilities(statevector, title_prefix, filename, marked_label=None, marked_amplitude_val=None, marked_prob_val=None):
    if not isinstance(statevector, Statevector):
        sv = Statevector(statevector)
    else:
        sv = statevector

    probabilities = sv.probabilities()
    state_labels = [format(i, f'0{n}b') for i in range(N)]

    title = title_prefix
    if marked_label:
         title += f"\n状態 |{marked_label}>: 実数振幅={marked_amplitude_val:.4f}, 確率={marked_prob_val:.4f}"

    fig, ax = plt.subplots(figsize=(12, 6))
    bars = ax.bar(state_labels, probabilities, color='deepskyblue', width=0.8)

    if marked_label in state_labels:
        marked_idx_for_plot = state_labels.index(marked_label)
        bars[marked_idx_for_plot].set_color('orangered')
        
    ax.set_xlabel("計算基底状態")
    ax.set_ylabel("確率")
    ax.set_title(title)
    ax.set_xticks(np.arange(N))
    ax.set_xticklabels(state_labels, rotation=90)
    ax.set_ylim(0, 1.05)
    plt.tight_layout()

    filepath = os.path.join(plots_dir, filename)
    fig.savefig(filepath)
    plt.close(fig)
    print(f"確率プロットを保存しました: {filepath}")


# --- 初期化回路 ---
init_qc = QuantumCircuit(n, name="初期化")
init_qc.h(range(n))

# --- オラクル回路 ---
oracle_diag_elements = np.ones(N, dtype=complex)
oracle_diag_elements[marked_item_int] = -1
oracle_gate_op = Diagonal(oracle_diag_elements)
oracle_circuit = QuantumCircuit(n, name=f"オラクル({marked_item_binary})")
oracle_circuit.append(oracle_gate_op, range(n))

# --- ディフューザー回路 (U_s = 2|s><s|-I を実装する) ---
def build_diffuser_circuit_corrected(num_qubits):
    diffuser = QuantumCircuit(num_qubits, name="ディフューザー(2|s><s|-I)")
    if num_qubits == 1:
        diffuser.x(0) # n=1 の場合、U_s = X
    else:
        diffuser.h(range(num_qubits))
        diffuser.x(range(num_qubits))
        diffuser.h(0)
        diffuser.mcx(list(range(1, num_qubits)), 0)
        diffuser.h(0)
        diffuser.x(range(num_qubits))
        diffuser.h(range(num_qubits))
        # 上記で I - 2|s><s| が構成されるので、全体に-1を掛けるためにグローバル位相πを追加
        diffuser.global_phase += np.pi 
    return diffuser

diffuser_circuit = build_diffuser_circuit_corrected(n)

# --- Grover反復回路 ---
grover_iteration_circuit = QuantumCircuit(n, name="Grover反復")
grover_iteration_circuit.append(oracle_circuit.to_instruction(), range(n))
grover_iteration_circuit.append(diffuser_circuit.to_instruction(), range(n))

# --- シミュレーションとプロット ---
sv_current = Statevector(init_qc)
amp_current_real = np.real(sv_current.data[marked_item_int])
prob_current_marked = sv_current.probabilities_dict().get(marked_item_binary, 0)
print(f"初期状態: |{marked_item_binary}> 実数振幅={amp_current_real:.4f}, 確率={prob_current_marked:.4f}")
plot_state_real_amplitudes(sv_current, f"初期状態 (n={n})", "0_initial_state_real_amp.png", marked_item_binary, amp_current_real, prob_current_marked)
plot_state_probabilities(sv_current, f"初期状態 (n={n})", "0_initial_state_prob.png", marked_item_binary, amp_current_real, prob_current_marked)

# オラクル適用
qc_temp = init_qc.copy()
qc_temp.append(oracle_circuit.to_instruction(), range(n))
sv_after_oracle_only = Statevector(qc_temp) # オラクルのみ適用した状態
amp_oracle_real = np.real(sv_after_oracle_only.data[marked_item_int])
prob_oracle_marked = sv_after_oracle_only.probabilities_dict().get(marked_item_binary, 0)
print(f"オラクル後: |{marked_item_binary}> 実数振幅={amp_oracle_real:.4f}, 確率={prob_oracle_marked:.4f}")
plot_state_real_amplitudes(sv_after_oracle_only, f"オラクル後", "1_after_oracle_state_real_amp.png", marked_item_binary, amp_oracle_real, prob_oracle_marked)
plot_state_probabilities(sv_after_oracle_only, f"オラクル後", "1_after_oracle_state_prob.png", marked_item_binary, amp_oracle_real, prob_oracle_marked)


# Grover反復の実行と各ステップのプロット
optimal_iters = int(np.floor(np.pi/4 * np.sqrt(N))) # 解が1つの場合
print(f"最適反復回数: {optimal_iters}")

# 1回から4回までの反復をシミュレーション
for k_iter in range(1, 5): # 1, 2, 3, 4回の反復
    qc_iter_step = init_qc.copy()
    for _ in range(k_iter):
        qc_iter_step.append(grover_iteration_circuit.to_instruction(), range(n))
    
    sv_iter_step = Statevector(qc_iter_step)
    amp_iter_real = np.real(sv_iter_step.data[marked_item_int])
    prob_iter_marked = sv_iter_step.probabilities_dict().get(marked_item_binary, 0)
    
    remark = ""
    if k_iter == optimal_iters:
        remark = " (最適)"
    elif k_iter > optimal_iters:
        remark = " (最適超過)"
        
    print(f"{k_iter}回反復後{remark}: |{marked_item_binary}> 実数振幅={amp_iter_real:.4f}, 確率={prob_iter_marked:.4f}")
    plot_state_real_amplitudes(sv_iter_step, f"{k_iter}回反復後{remark}", f"{k_iter+1}_after_{k_iter}_iterations_state_real_amp.png", 
                               marked_item_binary, amp_iter_real, prob_iter_marked)
    plot_state_probabilities(sv_iter_step, f"{k_iter}回反復後{remark}", f"{k_iter+1}_after_{k_iter}_iterations_state_prob.png", 
                               marked_item_binary, amp_iter_real, prob_iter_marked)

# 測定回路とシミュレーション (最適回数で)
qc_measure = init_qc.copy()
for _ in range(optimal_iters):
    qc_measure.append(grover_iteration_circuit.to_instruction(), range(n))
qc_measure.measure_all() # 全ての量子ビットを測定

simulator = BasicSimulator()
compiled_qc_measure = transpile(qc_measure, simulator)
job = simulator.run(compiled_qc_measure, shots=2048)
result_measure = job.result()
counts = result_measure.get_counts(qc_measure)

# 測定結果ヒストグラム
sv_final_optimal = Statevector(qc_optimal_iters) # qc_optimal_iters は最適反復後の状態ベクトルを保持する回路
prob_final_optimal_marked = sv_final_optimal.probabilities_dict().get(marked_item_binary,0)

fig_hist, ax_hist = plt.subplots(figsize=(12,6))
plot_histogram(
    counts, ax=ax_hist,
    title=f"{optimal_iters}回反復後の測定結果 (ターゲット: {marked_item_binary})\nターゲットの最終確率(理論値): {prob_final_optimal_marked:.4f}",
    number_to_keep=N, color='mediumseagreen'
)
hist_filepath = os.path.join(plots_dir, f"6_measurement_histogram_{optimal_iters}_iterations.png")
fig_hist.savefig(hist_filepath)
plt.close(fig_hist)
print(f"測定ヒストグラムを保存しました: {hist_filepath}")

print(f"\n全てのプロットは '{os.path.abspath(plots_dir)}' に保存されました。")

7. まとめ

本記事では、Groverの探索アルゴリズムについて、その基本的な考え方から具体的なステップ、数式による裏付け、幾何学的な解釈、そして、Python (Qiskit) を用いたシミュレーションまでを解説してきました。

Groverのアルゴリズムの核心は、

  1. 初期化: 全ての可能性を均等な振幅で重ね合わせ、
  2. オラクル: 解となる状態にのみ特別な印(位相の反転)をつけ、
  3. 増幅: その印がついた状態の振幅を、全体の平均値を利用して選択的に大きくする、
    という巧妙なステップを繰り返すことにあります。

この結果、古典コンピュータでは$O(N)$の計算量が必要だった未整理データベースからの探索が、理論上は量子コンピュータでは$O(\sqrt{N})$という劇的な高速化を達成できるようになります。これは、特に$N$が巨大な場合に、量子コンピュータの大きなアドバンテージとなり得る重要な結果です。

今回の$n=4$($N=16$)の例では、わずか3回の反復で、探索対象である$|0101\rangle$を見つけ出す確率を初期の6.25%から96%以上にまで高めることができました。この振幅増幅こそが、Groverのアルゴリズムの真髄と言えるでしょう。

もちろん、Groverのアルゴリズムを実社会の問題解決に役立てるためには、

  • 効率的なオラクルの設計: 問題ごとに「解とは何か」を量子回路で表現するオラクルをどう作るか。
  • エラー耐性のある量子コンピュータの実現: 現在の量子コンピュータはノイズの影響を受けやすく、大規模な計算にはまだ課題があります。
    といったハードルを越える必要があります。

しかし、Groverのアルゴリズムが示した計算能力の可能性は、量子情報科学の発展における大きなマイルストーンであり、今後さらに多くの研究者やエンジニアがこの分野に参画することで、新たなブレークスルーが生まれることが期待されます。

この記事を通じて、Groverのアルゴリズムの面白さ、そして量子コンピューティングの奥深さの一端を感じていただけたなら幸いです。ぜひ、ご自身でもQiskitなどのツールを使って、さまざまな量子アルゴリズムを試してみてください!

8. 参考文献・さらに学ぶために

  • Qiskit Textbook - Grover's Algorithm: https://qiskit.org/textbook/ch-algorithms/grover.html (Qiskitの公式教科書にあるGroverのアルゴリズムの解説とチュートリアルです。英語ですが、非常に読みごたえがあり、勉強になります。)

  • Quantum Native Dojo!: https://dojo.qulacs.org/ja/latest/ (日本の量子コンピュータ学習コミュニティによる、日本語の丁寧なチュートリアルサイトです。QiskitやBlueqatを使った実践的な内容も豊富です。)

最後までお読みいただき、ありがとうございました!

0
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?