Slide 1

Slide 1 text

Pythonで 「お絵描きパズル」 を 解いてみた PyCon JP 2018 09_18 小栗 潤一

Slide 2

Slide 2 text

自己紹介

Slide 3

Slide 3 text

小栗 潤一 ・ 広島在住 ・ 大学時代は福岡で デザインの勉強 ・ 広島のチラシ制作会社勤務 3年間デザイナーとして働き 9年間社内業務改善のプログラマー 自己紹介

Slide 4

Slide 4 text

自己紹介 私とPythonについて ・ 勉強を初めて約2年 ・ 広島のもくもく会 「すごい広島」 「すごい広島 with Python」

Slide 5

Slide 5 text

今日の議題

Slide 6

Slide 6 text

Python初心者の私が約3ヶ月かけて ロジックを考えPythonのコードにした話^^ そのコードを使って 某懸賞雑誌一冊分 120問に挑戦しました どこまで解けたのかお楽しみに!!

Slide 7

Slide 7 text

今日の議題 ・お絵描きパズルのルール ・パズルを解くのにPythonを使用した利点 ・ロジックを考え、 コードにする話 ・Jupyter Notebookを使用したデモ ・120問中どこまで解けたのか? ・まとめ

Slide 8

Slide 8 text

お絵描きパズル とは

Slide 9

Slide 9 text

お絵描きパズルとは 数字をヒントに マスを塗っていき 現れたイラストを 答えるゲーム 1990年代に 流行

Slide 10

Slide 10 text

お絵描きパズルとは お絵かきロジック (世界文化社) ピクロス (任天堂) ののぐらむ イラストロジック 名称が違うだけで 同じルール

Slide 11

Slide 11 text

お絵描きパズルとは 用語の説明 白のマス ・ 処理が未確定の空白のマス ・ □□□で表現 ・ 配列内では[0, 0, 0] 黒のマス ・ 塗りが確定した塗りつぶしたマス ・ ■■■で表現 ・ 配列内では[1, 1, 1] 塗り指示 ・ 問題の左・上辺にあるヒントの数字 ×のマス ・ 塗られないことが確定したマス ・ □□□で表現 ・ 配列内では[-1, -1, -1] ×××

Slide 12

Slide 12 text

「お絵描きパズルを解いてみた」 初心者が考えた Pythonの利点

Slide 13

Slide 13 text

「お絵描きパズルを解いてみた」 Pythonの利点 Pandas、 Numpyなどの便利なライブラリ [0,0,0,0,0,0,0,0,0,0] [10] [0,0,0,0,0,0,0,0,0,0] [3,2,2] DataFrame Numpyの配列計算 Series

Slide 14

Slide 14 text

それを踏まえて もう一度ご覧ください

Slide 15

Slide 15 text

「お絵描きパズルを解いてみた」 Pythonの利点 Pandas、 Numpyなどの便利なライブラリ [1, 1, 1, 1, 1, 1, 1, 1, 1, 1] [10] [1, 1, 1,-1, 1, 1,-1,-1, 1, 1] [3,2,2] DataFrame Numpyの配列計算 Series

Slide 16

Slide 16 text

「お絵描きパズルを解いてみた」 Pythonの利点 Jupyter Notebookを使った開発 ・ デモ環境 Jupyter Notebook ipywidgets RISE (拡張機能)

Slide 17

Slide 17 text

Jupyter Notebookを使って 問題を解かせてみましょう or デバッグ環境の紹介

Slide 18

Slide 18 text

・Pandas,Numpyを使って 簡単な配列計算で解けそう ・ 動きのあるデバック環境が 簡単に作れる 私の考えたPythonを使う利点

Slide 19

Slide 19 text

お絵描きパズルのルール ロジックの話

Slide 20

Slide 20 text

お絵描きロジックの基本的なルール ・数字と同じマスを塗りつぶす ・数字が複数ある場合は必ず1マス以上空ける 3 2,3 2,2 3 3

Slide 21

Slide 21 text

アルゴリズム (ロジック) を考えてみましょう

Slide 22

Slide 22 text

ロジックを考える まずは手動で解いてみる 普段何気なく問題を解く際に 頭の中で行なっている アルゴリズムを書き出してみる

Slide 23

Slide 23 text

まずは問題を解いてみよう 処理1 確定マスの塗りつぶし 処理5 端から確定マスを見つける 処理2 端が確定した際の処理 処理6 指示の最大数と同じ連続した塗り 処理3 届かないマスの確定処理 処理7 端から指示 +1のマスが塗られていたら 処理9 端から 「1」 と 「2」 の連続した塗り指示の確定処理 処理4 「×」 のマスで分割して処理1〜3を実行 処理8 端から 「1」 の連続した塗り指示の確定処理 処理10 連続された塗りから対象の指示数字を特定する ロジックを書き出してみる

Slide 24

Slide 24 text

ロジックの構造と全体の流れ # 変数の指定 n_list = np.array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0]) n_vals = [3,3] # 関数の指定 def nuri_syori(n_list, n_vals): ck_n_list = copy.copy(n_list) ck_n_list[0] = 2 while not np.allclose(n_list, ck_n_list) and 0 in n_list: ck_n_list = copy.copy(n_list) for i in range(2): # リストの反転 if i == 1: n_list = n_list[::-1] n_vals = n_vals[::-1] # 処理1〜10 logic_shori_1 () 〜 logic_shori_10 () # リストの反転(戻す処理) if i == 1: n_list = n_list[::-1] n_vals = n_vals[::-1]

Slide 25

Slide 25 text

ロジックで 某懸賞雑誌 120問に挑戦します どこまで解けたのでしょうか^^ ロジックの解説から始めましょう

Slide 26

Slide 26 text

6,2 処理1 確定マスの塗りつぶし 解き方

Slide 27

Slide 27 text

処理1 確定マスの塗りつぶし 解き方 6,2 6,2 6,2 6,2 左から詰めて6の配置 右から詰めて6の配置 左詰め時の一番右のマスから 右詰め時の一番左マスまでは確定 6,2 6,2 6,2 左から詰めて2の配置 右から詰めて2の配置 左詰め時の一番右のマスから 右詰め時の一番左マスまでは確定

Slide 28

Slide 28 text

解き方 n_list = np.array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0]) n_vals = [6,2] # 隙間の数を埋める処理リファクト後の処理 for n,val in enumerate(n_vals): af = sum(n_vals[n:]) + len(n_vals[n:]) - 1 bf = sum(n_vals[:n+1]) + len(n_vals[:n+1]) - 1 if not list(n_list[-af:bf]):continue n_list[-af:bf] = 1 ## n_list = [0,1,1,1,1,1,0,0,1,0] 左から詰めて6の配置 6,2 右から詰めて6の配置 6,2 左詰め時の一番右のマスから 右詰め時の一番左マスまでは確定 6,2

Slide 29

Slide 29 text

処理2 端が確定した際の処理 解き方 3,2 3,2 × 3,2 最初は確定できるマスはありません 行 ・ 列を進める際に確定マスができた 塗りマスと直後の 「×」 確定ができる

Slide 30

Slide 30 text

解き方 n_list = np.array([1, 0, 0, 0, 0, 0, 0, 0, 0, 0]) n_vals = [3,2] # 前方から処理 if n_list[0] == 1.0: n_list[0:n_vals[0]] = 1 n_list[n_vals[0]:n_vals[0]+1] = -1 # 後方から処理 if n_list[-1] == 1.0: n_list[-n_vals[-1]:len(n_list)] = 1 n_list[-n_vals[-1] -1:-n_vals[-1]] = -1 ## n_list = [1,1,1,-1,0,0,0,0,0,0] 3,2 × 3,2 行・列を進める際に確定マスができた 塗りマスと直後の 「×」 確定ができる

Slide 31

Slide 31 text

処理3 届かないマスの確定処理 解き方 3 3 × × × × × 3 3 左右に3マス届く可能性のマス 届かないマスは塗られる可能性がないことが確定

Slide 32

Slide 32 text

解き方 n_list = np.array([0, 0, 0, 1, 0, 0, 0, 0, 0, 0]) n_vals = [3] # 指示が一つ以上あるものは処理中断 if not len(n_vals) == 1: return n_list leftNumm = int(np.where(n_list == 1)[0][0])+int(n_vals[0]) if leftNumm < len(n_list): n_list[leftNumm:len(n_list)] = -1 # 反転してもう一度 n_list = n_list[::-1] leftNumm = int(np.where(n_list == 1)[0][0])+int(n_vals[0]) if leftNumm < len(n_list): n_list[leftNumm:len(n_list)] = -1 # 反転してもとに戻す n_list = n_list[::-1] ## n_list = [-1,0,0,1,0,0,-1,-1,-1,-1] 3 × × × × × 3 3 左右に3マス届く可能性のマス 届かないマスは塗られる 可能性がないことが確定

Slide 33

Slide 33 text

処理1・2・3は最初に 実装したロジック 難易度★★☆☆☆ 28問中約8割の問題が 解けるようになった^^v

Slide 34

Slide 34 text

ロジックの言語化 Try & Err の繰り返し 処理1 確定マスの塗りつぶし 処理5 端から確定マスを見つける 処理2 端が確定した際の処理 処理6 指示の最大数と同じ連続した塗り 処理3 届かないマスの確定処理 処理7 端から指示 +1のマスが塗られていたら 処理9 端から 「1」 と 「2」 の連続した塗り指示の確定処理 処理4 「×」 のマスで分割して処理1〜3を実行 処理8 端から 「1」 の連続した塗り指示の確定処理 処理10 連続された塗りから対象の指示数字を特定する

Slide 35

Slide 35 text

ロジックの言語化 Try & Err の繰り返し 処理1 確定マスの塗りつぶし 処理5 端から確定マスを見つける 処理2 端が確定した際の処理 処理6 指示の最大数と同じ連続した塗り 処理3 届かないマスの確定処理 処理7 端から指示 +1のマスが塗られていたら 処理9 端から 「1」 と 「2」 の連続した塗り指示の確定処理 処理4 「×」 のマスで分割して処理1〜3を実行 処理8 端から 「1」 の連続した塗り指示の確定処理 処理10 連続された塗りから対象の指示数字を特定する 一部のロジックは Wikipediaに載ってる為、 本日は解説しません

Slide 36

Slide 36 text

まずは問題を解いてみよう 全てのロジックを まとめた資料 興味のある方は こちらから http://bit.ly/2PEDues

Slide 37

Slide 37 text

ロジックの言語化 Try & Err の繰り返し 処理1 確定マスの塗りつぶし 処理5 端から確定マスを見つける 処理2 端が確定した際の処理 処理6 指示の最大数と同じ連続した塗り 処理3 届かないマスの確定処理 処理7 端から指示 +1のマスが塗られていたら 処理9 端から 「1」 と 「2」 の連続した塗り指示の確定処理 処理4 「×」 のマスで分割して処理1〜3を実行 処理8 端から 「1」 の連続した塗り指示の確定処理 処理10 連続された塗りから対象の指示数字を特定する

Slide 38

Slide 38 text

wikiに載ってない オリジナルロジック の解説

Slide 39

Slide 39 text

処理7 端から指示 +1のマスが塗られていたら 5,1 × 5,1 5,1 × 5,1 左端から 「5」 を塗ったと仮定すると 「5」 の塗りの可能性は 左端のマスは塗られないことが確定

Slide 40

Slide 40 text

処理8 端から 「1」 の連続した塗り指示の確定処理 1,1,1,2 × × × × 1,1,1,2 × × × 1,1,1,2 仮に 「1」 が左から詰めて塗られていたら この範囲にある塗りは必ず 「1」 になる ここまではWikipediaに載ってるロジック

Slide 41

Slide 41 text

処理9 端から 「1」 と 「2」 の連続した塗り指示の確定処理 1,2,1,1 × 1,2,1,1 × 1,2,1,1 × × × × 1,2,1,1 処理8は 「1の連続した」 という条件があった 「1と2の連続した」 という条件に変える この範囲にある[■,□,■]の[□]は×が確定

Slide 42

Slide 42 text

ロジックの言語化 難易度★★★★☆まで解ける(一部を除いて) 処理1 確定マスの塗りつぶし 処理5 端から確定マスを見つける 処理2 端が確定した際の処理 処理6 指示の最大数と同じ連続した塗り 処理3 届かないマスの確定処理 処理7 端から指示 +1のマスが塗られていたら 処理9 端から 「1」 と 「2」 の連続した塗り指示の確定処理 処理4 「×」 のマスで分割して処理1〜3を実行 処理8 端から 「1」 の連続した塗り指示の確定処理 処理10 連続された塗りから対象の指示数字を特定する どれ一つとして難しい 処理は行なっていません PandasのDataFrameを Seriesで取得して Numpyのndarrayに変換して 配列計算しているだけ

Slide 43

Slide 43 text

それでも解けない問題

Slide 44

Slide 44 text

・確定するマスがなくなる ・仮説処理で進めるしかない それでも解けない問題がでてくる

Slide 45

Slide 45 text

それでも解けない問題がでてくる 総当たり処理 ・確定するマスがなくなる ・仮説処理で進めるしかない

Slide 46

Slide 46 text

どれくらいの計算量になるのか? それでも解けない問題がでてくる × × × × × × 30 2,2,1,3,2,1,1 2,2,1,3,2,1,1 30 13 13**8= 815,730,721 ??????

Slide 47

Slide 47 text

それでも解けない問題がでてくる 総当たり処理 ・確定するマスがなくなる ・仮説処理で進めるしかない 計算量の少ない仮説

Slide 48

Slide 48 text

ではどう進める?

Slide 49

Slide 49 text

上下左右のうち 計算量の少ない1辺の 仮説を立てるだけ

Slide 50

Slide 50 text

仮説処理 1辺だけの総当たり処理 itertoolsを使って 4辺のパターン数を計算 1行目 :4パターン 15行目 :11パターン 1列目 :19パターン 20列目 :120パターン

Slide 51

Slide 51 text

仮説処理 1行目:4パターン そのまま最後まで解ける 確定マスがなくなる (さらなる仮説処理へ) 矛盾が発生 一番計算処理が少ない 辺から進める

Slide 52

Slide 52 text

仮説処理 0個 1個 複数 ありえない 確定パターン パターンを 記憶して 別の辺を進める 確定マスがなくなる パターン数が重要

Slide 53

Slide 53 text

仮説処理 2つ目の辺も 複数残った場合は 1つ目と2つ目の パターンを 総当たりチェック 必ず確定パターンが見つかる 1つ目の辺の残った パターンを記憶して 2つ目の辺を仮説処理

Slide 54

Slide 54 text

仮説処理を 追加することで 懸賞雑誌に載っている 問題がほぼ全て解けた 難易度★★★★★

Slide 55

Slide 55 text

懸賞雑誌一冊分 120問中カラーロジックを除く 116問全て解けた ^^

Slide 56

Slide 56 text

本日デモに使用した 問題もPythonで自作 イラストから問題作成 作成した問題を解く

Slide 57

Slide 57 text

まとめ

Slide 58

Slide 58 text

まとめ ・Pythonには便利なライブラリが豊富にあり 「お絵描きパズル」 のような配列計算で答えが 出せるゲームと相性が良い ・ ロジックを考えるのは難しいが コード自体はDataFrameをSeriesで取得して Numpyのndarrayで計算している簡単なコード 「お絵描きパズル」 「数独」 とも相性がいい ぜひ自分でトライしてみてください ^ ^ v