量子ゲートブログ4:「巡回セールスマン問題」をフルスクラッチで攻略する

こんにちは!量子計算の世界へようこそ。

これまでの連載で、量子ゲートの基礎やMax-Cut問題を学んできました。今回は、いよいよ組み合わせ最適化における「ラスボス」とも言える巡回セールスマン問題(TSP)に挑みます。

「4都市をどう回れば最短か?」という一見シンプルなパズルですが、量子コンピュータで解くには16個の量子ビットを巧みに操る必要があります。数式の展開から、Qiskitでの最新の実装まで、世界一わかりやすく解説します!


1. 変数設計:なぜ「16個」も量子ビットが必要なのか?

Max-Cut問題では「4つのノード=4ビット」という単純な対応でした。しかし、TSPはそうはいきません。なぜなら、「いつ(Step)」「どこに(City)いるか」という2つの情報を同時に記録しなければならないからです。

  • 都市: A, B, C, D(4つ)
  • ステップ: 0, 1, 2, 3(4回)

これらを掛け合わせると、 4×4=16 マスの「スケジュール表」が出来上がります。この1マスずつに「0(行かない)」か「1(行く)」を割り当てるため、16量子ビットが必要になるのです。

Pythonコードでは、この「2次元の表」を「1次元のビット列」に変換する住所変換ツール(idx関数)を定義します。

# 都市番号(city)と時間(step)を、16個のビットの何番目かに変換する
def idx(city, step):
    return city * n_steps + step

例えば、「都市B(1)にステップ3(2)で訪問する」なら、1×4+2 = 6 番目のビットを見れば良い、という仕組みです。


2. 数式の魔法:QUBOから物理(イジング)への変換

量子コンピュータは「0か1」という論理(QUBO)を直接は理解できません。計算できるのは「1か-1」という物理的なスピン(Z ゲート)の世界です。

そこで、すべての数式に x = (1-Z)/2を代入して展開(イジング化)します。

ここでは、ブログの核心である「なぜコードの係数が 0.5 や 0.25 になるのか」を解き明かします。

① 制約条件:1ステップに1都市ルール

「ある時間 tに、必ず1つの都市にしか行けない」というルールを数式にすると:

P = A(x_0 + x_1 + x_2 + x_3 – 1)^2

これに x=(1-Z)/2 を代入して地道に展開(※中略)すると、驚きの結果になります。

$$P = \dots = 2A – A \sum Z_i + 0.5 A \sum_{i<j} Z_i Z_j$$ (lateXですみません。。。)

ここがポイント!

  • 1次の項(-A): 単独の Z ゲートに掛ける係数。
  • 2次の項(0.5A): 2つの Zゲートの相互作用(ZZ)に掛ける係数。

コードを見ると、この理論値がそのまま実装されているのがわかります!

# 1次の項:-A
Hconst1 += -1.0 * Z_op(idx(city, step))
# 2次の項:0.5A
Hconst1 += 0.5 * ZZ_op(idx(city1, step), idx(city2, step))

② コスト条件:距離の最小化

「都市 i から j へ移動したときの距離 $W$」のペナルティは:

$$\text{Cost} = W \cdot x_{i, t} \cdot x_{j, t+1}$$ (lateXですみません。。。)

これも代入してバラすと…

$$\text{Cost} = \frac{W}{4} (1 – Z_1 – Z_2 + Z_1 Z_2)$$

すべての項に 0.25 が出てきましたね。

# weight / 4 (0.25) が至る所に!
Hcost3 += (weight * 0.25) * I_op()
Hcost3 += -(weight * 0.25) * Z_op(bit_i_t)
Hcost3 += -(weight * 0.25) * Z_op(bit_j_t_next)
Hcost3 += (weight * 0.25) * ZZ_op(bit_i_t, bit_j_t_next)

3. レベル3の進化:手動で量子回路を組み立てる

ハミルトニアン(エネルギーの設計図)ができたら、それを「量子回路」に翻訳します。QAOA(量子近似最適化アルゴリズム)の心臓部です。

ここで登場するのが CNOTサンドイッチのショートカット である rzz ゲートです。

# 1次の項(単一のZ) -> 単独の回転ゲート(rz)
for i, hi in nonzero_h:
    qc.rz(2.0 * g * hi, i)

# 2次の項(ZZの相互作用) -> 相互作用ゲート(rzz)
for (i, j), Jij in nonzero_J:
    qc.rzz(2.0 * g * Jij, i, j)
  • なぜ2倍するの?: 数式の $e^{-i \gamma \dots}$ の形に合わせるため、係数を2倍(2.0 * g * hi)にして角度として流し込みます。
  • RXミキサー: 最後に rx ゲートを並べて「量子トンネル効果」を引き起こし、壁を突き抜けて最適解を探します。

4. 実践!C++シミュレータによる超高速計算

16量子ビットの探索範囲は2^16 = 65,536 通り。これを普通のPythonで計算すると日が暮れてしまいます。そこで、今回はIBMが提供する最新エンジン AerSimulator (C++製)EstimatorV2 を使用します。

これは、本物の量子コンピュータ(実機)で動かすための「業界標準フォーマット」です。人間が書いた綺麗な回路を、マシンが理解できる形にコンパイル(翻訳)して実行します。

# C++シミュレータの準備
sim = AerSimulator()
pass_manager = generate_preset_pass_manager(3, sim)
isa_circuit = pass_manager.run(qc) # 実機向けに翻訳!

まとめ:このコードが「最強」である理由

このプログラムがすごいのは、Qiskitの自動機能に丸投げせず、人間が紙とペンで導き出した数式(0.5AやW/4)をそのままコードに打ち込んでいる点です。

  1. 物理のルールを文字(I, Z)で作る。
  2. 数式から係数を抜き出す。
  3. 抜き出した係数に合わせてゲート(rz, rzz)を置く。

このプロセスを完璧にマスターすれば、どんなに複雑なビジネス課題(物流最適化やシフト作成など)が来ても、自分の手で量子回路を設計できるようになります!


実行コードの全貌

import numpy as np

from qiskit.quantum_info import SparsePauliOp

from qiskit_algorithms import QAOA

from qiskit.primitives import StatevectorSampler as Sampler

from qiskit_algorithms.optimizers import COBYLA

n_cities = 4

n_steps = 4

n_bits = n_cities * n_steps

city_labels = [“A”, “B”, “C”, “D”]

distance = np.array([

[0, 2, 5, 8],

[2, 0, 4, 6],

[5, 4, 0, 6],

[8, 6, 6, 0]

])

def idx(city, step):

return city * n_steps + step

def I_op(n_bits=n_bits):

return SparsePauliOp.from_list([(“I” * n_bits, 1.0)])

def Z_op(bit_index, n_bits=n_bits):

pauli_str_list = [‘I’] * n_bits

pauli_str_list[bit_index] = ‘Z’

pauli_str_list.reverse() # Qiskitのエンディアン(右端が0)に合わせる

pauli_str = “”.join(pauli_str_list)

return SparsePauliOp.from_list([(pauli_str, 1.0)])

def ZZ_op(bit_i, bit_j, n_bits=n_bits):

pauli_str_list = [‘I’] * n_bits

pauli_str_list[bit_i] = ‘Z’

pauli_str_list[bit_j] = ‘Z’

pauli_str_list.reverse()

pauli_str = “”.join(pauli_str_list)

return SparsePauliOp.from_list([(pauli_str, 1.0)])

def zero_op(n_bits=n_bits):

return SparsePauliOp.from_list([(“I” * n_bits, 0.0)])

# Hconst1: 各ステップで訪問する都市は1つだけ

Hconst1 = zero_op()

for step in range(n_steps):

Hconst1 += 2.0 * I_op()

for city in range(n_cities):

Hconst1 += -1.0 * Z_op(idx(city, step))

for city1 in range(n_cities):

for city2 in range(city1 + 1, n_cities):

Hconst1 += 0.5 * ZZ_op(idx(city1, step), idx(city2, step))

# Hconst2: 各都市は1回だけ訪問される

Hconst2 = zero_op()

for city in range(n_cities):

Hconst2 += 2.0 * I_op()

for step in range(n_steps):

Hconst2 += -1.0 * Z_op(idx(city, step))

for step1 in range(n_steps):

for step2 in range(step1 + 1, n_steps):

Hconst2 += 0.5 * ZZ_op(idx(city, step1), idx(city, step2))

# Hcost3: 距離の最小化

Hcost3 = zero_op()

for t in range(n_steps):

t_next = (t + 1) % n_steps

for i in range(n_cities):

for j in range(n_cities):

if i != j:

weight = distance[i][j]

bit_i_t = idx(i, t)

bit_j_t_next = idx(j, t_next)

# weight * x_{i,t} * x_{j,t+1} の展開式

Hcost3 += (weight * 0.25) * I_op()

Hcost3 += -(weight * 0.25) * Z_op(bit_i_t)

Hcost3 += -(weight * 0.25) * Z_op(bit_j_t_next)

Hcost3 += (weight * 0.25) * ZZ_op(bit_i_t, bit_j_t_next)

H = Hconst1 + Hconst2 + 0.01 * Hcost3

# 重複したPauli文字列をまとめて計算を軽くする

H = H.simplify()

# ==========================================

# 3. 高速化のためのハミルトニアン事前分解

# ==========================================

print(“— ハミルトニアンを解析中 —“)

const = 0.0

h = np.zeros(n_bits, dtype=float)

J = defaultdict(float)

for P, c in zip(H.paulis, H.coeffs):

coeff = float(np.real(c))

if abs(coeff) < 1e-12:

continue

z_mask = P.z

zs = list(np.where(z_mask)[0])

if len(zs) == 0:

const += coeff

elif len(zs) == 1:

h[zs[0]] += coeff

elif len(zs) == 2:

i, j = zs

a, b = (i, j) if i < j else (j, i)

J[(a, b)] += coeff

print(f”解析完了: 定数項={const:.2f}, 1次項(Z)の数={np.count_nonzero(np.abs(h) > 1e-12)}, 2次項(ZZ)の数={len(J)}”)

# ==========================================

# 4. 手動量子回路(Ansatz)の超高速構築

# ==========================================

p = 10 # レイヤー数(10段で深い探索を行う)

theta = ParameterVector(“θ”, 2 * p)

gammas = theta[:p]

betas = theta[p:]

nonzero_h = [(i, float(h[i])) for i in range(n_bits) if abs(h[i]) > 1e-12]

nonzero_J = [((i, j), float(coeff)) for (i, j), coeff in J.items() if abs(coeff) > 1e-12]

qc = QuantumCircuit(n_bits)

qc.h(range(n_bits))

for layer in range(p):

g = gammas[layer]

b = betas[layer]

# コストレイヤー (gamma)

for i, hi in nonzero_h:

qc.rz(2.0 * g * hi, i)

for (i, j), Jij in nonzero_J:

qc.rzz(2.0 * g * Jij, i, j)

# ミキサーレイヤー (beta)

for q in range(n_bits):

qc.rx(2.0 * b, q)

# ==========================================

# 5. C++シミュレータ (AerSimulator) による最適化

# ==========================================

print(“\nQAOA計算をC++シミュレータで高速実行中…”)

sim = AerSimulator()

pass_manager = generate_preset_pass_manager(3, sim)

isa_circuit = pass_manager.run(qc)

H_isa = H

layout = getattr(isa_circuit, “layout”, None)

if layout is None:

layout = getattr(isa_circuit, “_layout”, None)

if layout is not None:

try:

H_isa = H.apply_layout(layout)

except Exception:

pass

est = EstimatorV2(options={“default_precision”: 0.0, “backend_options”: {“method”: “statevector”}})

isa_param_names = [p_.name for p_ in isa_circuit.parameters]

def energy(params):

by_name = {theta[i].name: float(params[i]) for i in range(2 * p)}

ordered = [by_name[name] for name in isa_param_names]

pub = (isa_circuit, H_isa, ordered)

job = est.run([pub])

return float(np.real(job.result()[0].data.evs))

optimizer = COBYLA(maxiter=100)

x0 = np.random.rand(2 * p)

result = optimizer.minimize(energy, x0=x0)

print(f”最小エネルギー: {result.fun:.4f}”)

# ==========================================

# 6. 正しいルートのデコードと出力

# ==========================================

def decode_tour(bit_int):

tour = []

for t in range(n_steps):

chosen = [c for c in range(n_cities) if ((bit_int >> idx(c, t)) & 1) == 1]

if len(chosen) != 1:

return None # ルール違反(1ステップに2都市など)

tour.append(chosen[0])

if sorted(tour) != list(range(n_cities)):

return None # ルール違反(同じ都市に2回行ったなど)

return tour

def tour_length(tour):

return float(sum(distance[tour[t], tour[(t + 1) % n_steps]] for t in range(n_steps)))

# 最適化されたパラメータを回路に代入して状態ベクトルを取得

bind = {theta[i]: float(result.x[i]) for i in range(2 * p)}

qc_opt = qc.assign_parameters(bind, inplace=False)

psi = Statevector.from_instruction(qc_opt)

probs = psi.probabilities()

# 制約を満たす(ルール違反をしていない)ルートの中で、最も確率が高いものを探す

best = None

for k in range(1 << n_bits):

tour = decode_tour(k)

if tour is None:

continue

P = float(probs[k])

if best is None or P > best[2]:

best = (tour, tour_length(tour), P, k)

if best is None:

print(“\n⚠️ 有効なルートが見つかりませんでした。ペナルティ係数の調整などが必要です。”)

else:

tour, L, P, k = best

tour_labels = [city_labels[c] for c in tour]

print(“\n✅ 最も確率の高い有効なルート:”)

print(” -> “.join(tour_labels + [tour_labels[0]]))

print(f”総移動距離: {L}”)

print(f”観測確率: {P:.6f}”)


いかがでしたか?16量子ビットのTSPを自力で解く快感、ぜひ皆さんも味わってみてください!

あくまで量子計算は確率なので何回か実行するかp = 50などにするなど工夫は必要ですが、最適解には到達します。

次もお楽しみに!

コメントする

メールアドレスが公開されることはありません。 が付いている欄は必須項目です