Pythonで動的計画法を勉強してみた

ナップサック問題を解いていたら、出てきた「動的計画法」
結構複雑なのでまとめてみた。

3312 views

参考:https://o-treetree.hatenablog.com/entry/DPL1A

問題の出し方

例題

6種類のコインで15を表すとき、枚数を最も少なくする組み合わせはどれ?

額面が大きい順にコインを使うと[ 12, 1, 2]の三種類になる。
しかし、答えは[ 7 , 8 ]の二種類になる。
じゃあ、どんな計算をすれば、答えが出るの?ってのを解説します。

解き方

イメージ編

DPテーブルを作成する

動的計画法の定義にあるように、"問題を複数の部分問題に分割して、部分問題の計算結果を利用して元の問題を解く"を実行するために、計算結果を格納するテーブルを作成する。
DPテーブルは、数値を表現するために、枚数が最も少なくなる組み合わせを記録している。

実際にテーブルを作ってみると、こんな感じになる。

  • I:使えるコインの種類
  • J:表現する値

DPテーブルの見方と表現

上図(DPテーブル)は、一見とっつきにくく思えるが、見方さえ覚えれば、割と楽です。
本項では、DPテーブル見方と表現方法を例とともに記します。

例:J = 8の時

今回は、例題としてJ(表現する値)=8の時を例に解説する。
J=8は、下図で赤く囲っている箇所である。

赤く囲っている箇所を見ると、「8 ・・4・・」と数値が異なる。
数値が異なっている理由は、「使えるコインの種類が増えた」からである。
つまり、下進むほど、使えるコインの種類が増え、少ない枚数のコインで値を表現できる可能性が増える。

I=0の時

使えるコインの種類:0

0枚のコインでは8を表現できないため、値はINF

I=1の時

使えるコインの種類:1(1)

8を1で表現する場合、コインが8枚必要なるため、値は8

I = 2の時

使えるコインの種類:2(1 , 2)
※8を表現するのに、1と2の二枚を使用できる

8を1 , 2で表現する場合、コインが4枚必要になるため、値は4

I = 3の時

使えるコインの種類:3(1 , 2 , 7)
※8を表現するのに、1と2と7の三枚を使用できる

8を1 , 2 , 7で表現する場合、コインが2枚必要になるため、値は2

と、続いていく。つまり、下図のようなことをしている。

実装1

コード

かなりざっくりしている気がする。
テーブルを作る前に、ターゲットを絞って最小の組み合わせを表示するアルゴリズムを作ってみた。
※これ、厳密な意味では動的計画法ではないのでは?もう少し調査します

  1. COINS = [1, 2, 7, 8, 12, 50]
  2. target = 15
  3. length = len(COINS)
  4. cntMemo = []
  5. #とりあえず、初期値として最多手数を入れる
  6. #初期値の設定
  7. if cntMemo == 0:
  8. cntMemo.append(0)
  9. else:
  10. cntMemo.append("INF")
  11. #コインをしまう処理
  12. def select(type):
  13. num = type-1
  14. #現在の値
  15. memo = COINS[num]
  16. #必要な種類
  17. cnt = 1
  18. while True:
  19. if memo == target:
  20. break
  21. if memo >target:
  22. memo -= COINS[num]
  23. num -=1
  24. memo += COINS[num]
  25. if memo < target:
  26. cnt += 1
  27. memo += COINS[num]
  28. #cntの最小値をメモとして保存する処理
  29. if len(cntMemo) == 1:
  30. cntMemo.append(cnt)
  31. else:
  32. if cnt < cntMemo[type-1]:
  33. cntMemo.append(cnt)
  34. else:
  35. cntMemo.append(cntMemo[type-1])
  36. return cnt
  37. def coin():
  38. cnt = target
  39. if cntMemo[0] == 0:
  40. cnt = 0
  41. else:
  42. type = 1
  43. while type <= length:
  44. select(type)
  45. type += 1
  46. # コインの種類が最小の組み合わせが見つかったとき
  47. cnt = cntMemo[type-1]
  48. return cnt
  49. print(target, "を表現するためには、", coin(), "種類のコインが必要")
  50. for memo in cntMemo:
  51. print(memo)

出力

実装2

今度は、テーブルを作成していく。

実装

  1. # コインの種類
  2. COINS = [1, 2, 7, 8, 12, 50]
  3. # 求める範囲(30までのテーブルを作成する)
  4. coverage = 30
  5. # 求める値
  6. target = 15
  7. dp = [[[]for j in range(coverage+1)] for i in range(len(COINS) + 1)]
  8. def serachNum(i,j):
  9. target = j
  10. num = i - 1
  11. # 現在の値
  12. memo = COINS[num]
  13. cnt = 1
  14. while True:
  15. # ここの処理は変更していない。
  16. if memo == target:
  17. break
  18. if memo > target:
  19. memo -= COINS[num]
  20. num -= 1
  21. memo += COINS[num]
  22. if memo < target:
  23. cnt += 1
  24. memo += COINS[num]
  25. return cnt
  26. def coinChange():
  27. for i in range(len(COINS)+1):
  28. for j in range(coverage + 1):
  29. if i == 0:
  30. # 0行目の時の処理
  31. if j == 0:
  32. # 配列0個目
  33. dp[i][j] = 0
  34. else:
  35. # 配列1個目以降
  36. dp[i][j] = "INF"
  37. print(j)
  38. else:
  39. # 1行目以降の処理
  40. if j == 0:
  41. # 配列0個目
  42. dp[i][j] = 0
  43. else:
  44. # 配列1個目以降
  45. # 一つ前のコインの組み合わせ方を取得
  46. # 今回求めたコインの組み合わせを取得
  47. comparison = dp[i-1][j]
  48. ans = serachNum(i,j)
  49. if type(comparison) == int:
  50. if ans < comparison:
  51. # 今回の組み合わせが最短手だった時
  52. dp[i][j] = ans
  53. else:
  54. # 前回以前の組み合わせが最短手だった時
  55. dp[i][j] = comparison
  56. else:
  57. # 一つ前の最短手が数字ではないとき
  58. dp[i][j] = ans
  59. def coinGet(type,target):
  60. return dp[type][target]
  61. coinChange()
  62. ans = coinGet(len(COINS),target)
  63. print(len(COINS),"種類のコインを使って",target,"を表現するには",ans,"種類のコインが必要")

出力

お、いい感じの値が求められている。
今回のコツは、前回求めた最短手をメモしておき、今回求めた最短手と比較することだと思う。

Page 4 of 5.

前のページ 次のページ



[添付ファイル]


お問い合わせ

プロフィール

owl

自己紹介

駆け出しエンジニア
だいたいweb系をかじってる
最近ちょとブロックチェーンに興味出てきた

サイト/ブログ

https://github.com/owl0109

ツイッター

@kijiken1