4/15(水)~17(金) NexTech Week@ビッグサイトに出展します☆彡
量子アニーリング

New!! 量子アニーリング(QUBO)でスリザーリンク(Slitherlink)を解く

AI要約

量子アニーリング(QUBO)を用いてパズル「スリザーリンク」を解きます。最大の難関である「一本のループにする」制約に対し、直線接続へのボーナス付与という独自の手法でアプローチ。離れ小島の発生を抑え、アニーリングで見事正解を導き出せるか検証します。

やること

久しぶりに量子アニーリングでパズルを解きます。

以前、「スリザーリンクのような線で囲む系はQUBOにできない」と言いました。(「スリザーリンク」で記事検索すると出てきます)

正確に言うと、線を繋ぐのは簡単だが「1本の線で囲む」というルールがQUBO化できないのです。

まあでも、できるところまでやってみましょうよ、ということで今回はスリザーリンクをQUBOで解いてみます。

スリザーリンクとは

いつもお世話になっている Puzzle Team さんから問題を拝借。いつも感謝です。

条件を満たすように格子点に辺を書き込んでいくパズルです。こちらが例題。

左が正しい答え、右が惜しい答えです。

数字は4近傍の辺の数を指定していて、辺は途切れることなく1本のループになっている必要があります。

なんだ基本の「n個の量子ビットからm個を1にする」で手を繋いでいけば完成じゃないか、と思うのですが、それだけでは離れ小島の解もできてしまうのです。

これを防ぐ式を作るのが難しいんですね。「自分はあの量子ビットと連続的に繋がっているか?」の関係を記述する方法が思いつきません。(少なくともQUBOの範囲では)

おさらい

今回はこちらの3パターンの条件設定を使います。

基本「n個の量子ビットからm個を1にする」
A3「2個の量子ビットが同時に1になったら報酬を与える」
D2 解が0と1だけの方程式(右辺が複数)

定式化

簡単のためにこちらの最小問題を試します。

このような解が想定されます。どちらも手の本数は正しい。

まず、辺に量子ビットを割り当てます。

格子点にも補助量子ビットを割り当てます。

制約条件1は、「各格子点から出る手の本数が0本または2本」です(D2の式)。これで線が途切れたり分岐したりすることはなくなります。

制約条件2は、「数字セルの4辺は数字の数だけ1にする」です。これは基本の式ですね。

これだけだと離れ小島の解も同率1位で出てきてしまいます。

そこでコストとして「各格子点とも直線接続ならプチボーナス」を与えます(A3の式)。2つの解を見比べてみると、一つの島にしたほうが直線接続が多いことがわかります。果たしてこれでいけるでしょうか??

コードはバカみたいにベタ書きしているところがありますが、ご勘弁を。Pythonで格子の辺にIDを割り振る方法がないのが悪いんですよ(←お前が作れ)。

import itertools
import numpy as np
import matplotlib.pyplot as plt
from tytan import *

#問題
size = [1, 3]
rule = {(0, 1): 2}

#辺の数
num1 = 2 * (size[0] * size[1]) + size[0] + size[1] 
print(f'num1 = {num1}')

#点の数
num2 = (size[0] + 1) * (size[1] + 1) 
print(f'num2 = {num2}')

#量子ビット
q = symbols_list(num1, 'q{}')

#補助量子ビット
s = symbols_list(num2, 's{}')

#セルに量子ビット(辺)を対応付け
edge = {
        (0, 0): [q[0], q[3], q[4], q[7]],
        (0, 1): [q[1], q[4], q[5], q[8]],
        (0, 2): [q[2], q[5], q[6], q[9]],
        }

#各格子点から出る手の本数が0本または2本
Hconst1 = 0
Hconst1 += (q[0] + q[3] - 2*s[0])**2
Hconst1 += (q[0] + q[1] + q[4] - 2*s[1])**2
Hconst1 += (q[1] + q[2] + q[5] - 2*s[2])**2
Hconst1 += (q[2] + q[6] - 2*s[3])**2
Hconst1 += (q[3] + q[7] - 2*s[4])**2
Hconst1 += (q[4] + q[7] + q[8] - 2*s[5])**2
Hconst1 += (q[5] + q[8] + q[9] - 2*s[6])**2
Hconst1 += (q[6] + q[9] - 2*s[7])**2

#数字セルの4辺は数字の数だけ1にする
Hconst2 = 0
for i, (key, value) in enumerate(rule.items()):
    Hconst2 += (np.sum(edge[key]) - value)**2

#直線接続ならプチボーナス
Hcost = 0
Hcost -= q[0]*q[1] + q[1]*q[2] + q[7]*q[8] + q[8]*q[9]

#合体
H = Hconst1 + Hconst2 + 0.01*Hcost

#コンパイル
qubo, offset = Compile(H).get_qubo()
print(f'offset = {offset}')

#サンプラー選択
solver = sampler.ArminSampler()

#サンプリング
result = solver.run(qubo, shots=500, T_num=1000)

#上位5件
for r in result[:5]:
    print(f'Energy {r[1]}, Occurrence {r[2]}')
    
    #辺の配列(上書きしていることに注意)
    q, subs = Auto_array(r[0]).get_ndarray('q{}')
    
    #再度、セルに量子ビット(辺)を対応付け
    edge = {
            (0, 0): [q[0], q[3], q[4], q[7]],
            (0, 1): [q[1], q[4], q[5], q[8]],
            (0, 2): [q[2], q[5], q[6], q[9]],
            }
    
    #表示
    plt.figure(figsize=(8, 8))
    ax = plt.axes()
    #辺
    for _, (key, value) in enumerate(edge.items()):
        tmp = edge[key]
        i = key[1]
        j = key[0]
        if tmp[0] == 1:
            plt.plot([i, i+1], [j, j], '-k', lw=5)
        if tmp[1] == 1:
            plt.plot([i, i], [j, j+1], '-k', lw=5)
        if tmp[2] == 1:
            plt.plot([i+1, i+1], [j, j+1], '-k', lw=5)
        if tmp[3] == 1:
            plt.plot([i, i+1], [j+1, j+1], '-k', lw=5)
    #格子点
    for i in range(size[0] + 1):
        for j in range(size[1] + 1):
            plt.plot(j, i, 'ok', ms=10)
    #数字
    for _, (key, value) in enumerate(rule.items()):
        i = key[0]
        j = key[1]
        plt.text(j+0.5, i+0.55, f'{value}', ha='center', va='center', weight='bold', size=28)
    
    ax.set_aspect('equal')
    plt.xlim(-1, size[1] + 1)
    plt.ylim(size[0] + 1, -1)
    plt.show()
num1 = 10
num2 = 8
offset = 4.0
MODE: GPU
DEVICE: cuda:0
Energy -4.040000915527344, Occurrence 190
Energy -4.0, Occurrence 9
Energy -3.020000457763672, Occurrence 7
(以下略)

上位3つの解を表示しました。正解である第1位が-4.04、離れ小島の第2位は-4.0としっかり差がついています。ボーナスが4ヶ所なので。これはいまくいきそうですね!

例題1

では、冒頭の例題を解いてみましょう。

誰も見ないと思いますが、量子ビット番号を確認したい方はこれを参照してください。

問題サイズに合わせてコードを書き換えます。

#問題
size = [5, 5]
rule = {
        (0, 0): 3,
        (1, 4): 3,
        (2, 1): 3,
        (2, 2): 3,
        (2, 3): 1,
        (2, 4): 3,
        (3, 1): 0,
        (3, 4): 3,
        (4, 1): 3,
        }
#セルに量子ビット(辺)を対応付け
edge = {
        (0, 0): [q[0], q[5], q[6], q[11]],
        (0, 1): [q[1], q[6], q[7], q[12]],
        (0, 2): [q[2], q[7], q[8], q[13]],
        (0, 3): [q[3], q[8], q[9], q[14]],
        (0, 4): [q[4], q[9], q[10], q[15]],
        
        (1, 0): [q[11], q[16], q[17], q[22]],
        (1, 1): [q[12], q[17], q[18], q[23]],
        (1, 2): [q[13], q[18], q[19], q[24]],
        (1, 3): [q[14], q[19], q[20], q[25]],
        (1, 4): [q[15], q[20], q[21], q[26]],
        
        (2, 0): [q[22], q[27], q[28], q[33]],
        (2, 1): [q[23], q[28], q[29], q[34]],
        (2, 2): [q[24], q[29], q[30], q[35]],
        (2, 3): [q[25], q[30], q[31], q[36]],
        (2, 4): [q[26], q[31], q[32], q[37]],
        
        (3, 0): [q[33], q[38], q[39], q[44]],
        (3, 1): [q[34], q[39], q[40], q[45]],
        (3, 2): [q[35], q[40], q[41], q[46]],
        (3, 3): [q[36], q[41], q[42], q[47]],
        (3, 4): [q[37], q[42], q[43], q[48]],
        
        (4, 0): [q[44], q[49], q[50], q[55]],
        (4, 1): [q[45], q[50], q[51], q[56]],
        (4, 2): [q[46], q[51], q[52], q[57]],
        (4, 3): [q[47], q[52], q[53], q[58]],
        (4, 4): [q[48], q[53], q[54], q[59]],
        }
print(f'edge = {edge}')

#各格子点から出る手の本数が0本または2本
Hconst1 = 0
Hconst1 += (       q[0] + q[5]  - 2*s[0])**2
Hconst1 += (q[0] + q[1] + q[6]  - 2*s[1])**2
Hconst1 += (q[1] + q[2] + q[7]  - 2*s[2])**2
Hconst1 += (q[2] + q[3] + q[8]  - 2*s[3])**2
Hconst1 += (q[3] + q[4] + q[9]  - 2*s[4])**2
Hconst1 += (q[4]        + q[10] - 2*s[5])**2

Hconst1 += (q[5]          + q[11] + q[16] - 2*s[6])**2
Hconst1 += (q[6]  + q[11] + q[12] + q[17] - 2*s[7])**2
Hconst1 += (q[7]  + q[12] + q[13] + q[18] - 2*s[8])**2
Hconst1 += (q[8]  + q[13] + q[14] + q[19] - 2*s[9])**2
Hconst1 += (q[9]  + q[14] + q[15] + q[20] - 2*s[10])**2
Hconst1 += (q[10] + q[15]         + q[21] - 2*s[11])**2

Hconst1 += (q[16]         + q[22] + q[27] - 2*s[12])**2
Hconst1 += (q[17] + q[22] + q[23] + q[28] - 2*s[13])**2
Hconst1 += (q[18] + q[23] + q[24] + q[29] - 2*s[14])**2
Hconst1 += (q[19] + q[24] + q[25] + q[30] - 2*s[15])**2
Hconst1 += (q[20] + q[25] + q[26] + q[31] - 2*s[16])**2
Hconst1 += (q[21] + q[26]         + q[32] - 2*s[17])**2

Hconst1 += (q[27]         + q[33] + q[38] - 2*s[18])**2
Hconst1 += (q[28] + q[33] + q[34] + q[39] - 2*s[19])**2
Hconst1 += (q[29] + q[34] + q[35] + q[40] - 2*s[20])**2
Hconst1 += (q[30] + q[35] + q[36] + q[41] - 2*s[21])**2
Hconst1 += (q[31] + q[36] + q[37] + q[42] - 2*s[22])**2
Hconst1 += (q[32] + q[37]         + q[43] - 2*s[23])**2

Hconst1 += (q[38]         + q[44] + q[49] - 2*s[24])**2
Hconst1 += (q[39] + q[44] + q[45] + q[50] - 2*s[25])**2
Hconst1 += (q[40] + q[45] + q[46] + q[51] - 2*s[26])**2
Hconst1 += (q[41] + q[46] + q[47] + q[52] - 2*s[27])**2
Hconst1 += (q[42] + q[47] + q[48] + q[53] - 2*s[28])**2
Hconst1 += (q[43] + q[48]         + q[54] - 2*s[29])**2

Hconst1 += (q[49]         + q[55] - 2*s[30])**2
Hconst1 += (q[50] + q[55] + q[56] - 2*s[31])**2
Hconst1 += (q[51] + q[56] + q[57] - 2*s[32])**2
Hconst1 += (q[52] + q[57] + q[58] - 2*s[33])**2
Hconst1 += (q[53] + q[58] + q[59] - 2*s[34])**2
Hconst1 += (q[54] + q[59]         - 2*s[35])**2
#直線接続ならプチボーナス
Hcost = 0
Hcost -= q[0]*q[1] + q[1]*q[2] + q[2]*q[3] + q[3]*q[4]
Hcost -= q[11]*q[12] + q[12]*q[13] + q[13]*q[14] + q[14]*q[15]
Hcost -= q[22]*q[23] + q[23]*q[24] + q[24]*q[25] + q[25]*q[26]
Hcost -= q[33]*q[34] + q[34]*q[35] + q[35]*q[36] + q[36]*q[37]
Hcost -= q[44]*q[45] + q[45]*q[46] + q[46]*q[47] + q[47]*q[48]
Hcost -= q[55]*q[56] + q[56]*q[57] + q[57]*q[58] + q[58]*q[59]
Hcost -= q[5]*q[16] + q[16]*q[27] + q[27]*q[38] + q[38]*q[49]
Hcost -= q[6]*q[17] + q[17]*q[28] + q[28]*q[39] + q[39]*q[50]
Hcost -= q[7]*q[18] + q[18]*q[29] + q[29]*q[40] + q[40]*q[51]
Hcost -= q[8]*q[19] + q[19]*q[30] + q[30]*q[41] + q[41]*q[52]
Hcost -= q[9]*q[20] + q[20]*q[31] + q[31]*q[42] + q[42]*q[53]
Hcost -= q[10]*q[21] + q[21]*q[32] + q[32]*q[43] + q[43]*q[54]
#サンプリング
result = solver.run(qubo, shots=20000, T_num=200000)
num1 = 60
num2 = 36
offset = 64.0
MODE: GPU
DEVICE: cuda:0
Energy -64.08000183105469, Occurrence 10
Energy -64.05999755859375, Occurrence 4
Energy -63.100006103515625, Occurrence 7
(以下略)

おお、できてますね。正解が-64.08、離れ小島が-64.06となり0.02の差がついています。たしかに、正解から直線を3本捨てて1本増やすと惜しい解になるので理論通りのエネルギーです。

例題2

こちらの例題も試してみましょう。中央が正解で、離れ小島の解がたくさんあります。

#問題
size = [5, 5]
rule = {
        (0, 1): 2,
        (0, 2): 1,
        (0, 3): 2,
        (0, 4): 3,
        (2, 3): 1,
        (2, 4): 3,
        (3, 2): 2,
        (4, 3): 2,
        (4, 4): 0,
        }
num1 = 60
num2 = 36
offset = 36.0
MODE: GPU
DEVICE: cuda:0
Energy -36.10000228881836, Occurrence 2
Energy -36.10000228881836, Occurrence 3
Energy -36.08000183105469, Occurrence 1
(以下略)

やったか…!?()

ほとんどの離れ小島解はブロックできたのですが、第2位のパターンだけは同率で出てきてしまいました。やはり定式化が完璧ではなかったか・・・惜しい!

おわりに

まあ同率1位で出してからOpenCVで島の個数を数えればいいんですけどね~

QUBO一本でガッチリ正解を出すことはできませんでした。ほぼ解けたということで勘弁していただきましょう ( ˘ω˘ ; )

リアクションのお願い

「参考になった!」「刺激された!」と思ったらぜひリアクションをしましょう。エンジニアの世界はGive and Takeによって成り立っています。これからも無料で良質な情報にアクセスできるよう、Giveする人への感謝をリアクションで示しましょう!

この記事をシェアする

自身のブログ等で使用する場合は引用を忘れずに!

また、寄付も受け付けています。コーヒー1杯でとても喜びます(*˘︶˘*)

 Amazonでギフト券(アマギフ)を贈る

こちらのリンク から金額を指定してお贈りください。(デフォルトで10000円になっているのでご変更ください)

配送:Eメール
受取人:staffあっとvigne-cla.com
贈り主:あなたのお名前やニックネーム
メッセージ:◯◯の記事が参考になりました。など

のようにご入力ください。見返りはありませんのでご了承ください。

 Amazonで食事券(すかいらーく優待券)を贈る

500円 1000円 2000円 5000円 からお贈りください。

配送:Eメール
受取人:staffあっとvigne-cla.com
贈り主:あなたのお名前やニックネーム
メッセージ:◯◯の記事が参考になりました。など

のようにご入力ください。見返りはありませんのでご了承ください。

 その他、ギフト券やクーポン券をメールで贈る

デジタルのギフト券/クーポン券はメールアドレス(staffあっとvigne-cla.com)までお送りください。受領の返信をいたします。
紙のギフト券/クーポン券は 「郵便物はこちらへ」の住所 まで送付してください。名刺やメールアドレスを同封していただければ受領の連絡をいたします。
余った株主優待券等の処理におすすめです。
いずれも見返りはありませんのでご了承ください。

不明点はSNSでお気軽にご連絡ください

ビネクラのTwitter・Youtubeでコメントをください!


Slack・Discordの場合はこちらの公開グループに参加してShoya YasudaまでDMをください!


※当ブログに関することは何でもご相談・ご依頼可能です。

この記事を書いた人
Yasuda

博士(理学)。専門は免疫細胞、数理モデル、シミュレーション。米国、中国で研究に携わった。遺伝的アルゴリズム信者。物価上昇のため半額弁当とともに絶滅寸前。

タイトルとURLをコピーしました