import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
課題1
区間推定アルゴリズムを実装して、実際のデータに適用する。
- データは、http://egg2.wustl.edu/roadmap/data/byDataType/dnamethylation/WGBS/からbigWig形式で取得できる。
- bigWigはbigWigToWigでwigm形式に変換できる。http://hgdownload.cse.ucsc.edu/admin/exe
# 今回は、E003: ESC.H1 H1_Cell_Line を選択。
# メチル化率を求める。
$ wget https://egg2.wustl.edu/roadmap/data/byDataType/dnamethylation/WGBS/FractionalMethylation_bigwig/E003_WGBS_FractionalMethylation.bigwig
$ bigWigToWig E003_WGBS_FractionalMethylation.bigwig E003_WGBS_FractionalMethylation.wig
# リードの長さを求める。
$ wget https://egg2.wustl.edu/roadmap/data/byDataType/dnamethylation/WGBS/ReadCoverage_bigwig/E003_WGBS_ReadCoverage.bigwig
$ bigWigToWig E003_WGBS_ReadCoverage.bigwig E003_WGBS_ReadCoverage.wig
# データの前処理
with open("E003_WGBS_2.wig") as f:
methylation = [float(line.split("\t")[-1].rstrip("\n")) for line in f.readlines() if line[0]!="#"]
with open("E003_WGBS_ReadCoverage.wig") as f:
readcoverage = [int(line.split("\t")[-1].rstrip("\n")) for line in f.readlines() if line[0]!="#"]
print(f"Fractional Methylation Rate: {len(methylation)}")
print(f"Depth of Read Coverage : {len(readcoverage)}")
>>> Fractional Methylation Rate: 51328201
>>> Depth of Read Coverage : 51328201
converted = [round(l*r) for l,r in zip(readcoverage, methylation)] # u
unconverted = [t-u for t,u in zip(readcoverage,converted)] # m
ファイルが重いため、上記の一つ一つの操作にかなり時間がかかったので、メチル化されたリード数(converted
)、されなかったリード数(unconverted
)をまとめたファイル(E003_FractionalMethylatioLength.txt
)を作成したので、それを用いる。
df = pd.read_csv("E003_FractionalMethylatioLength.txt")
df.head()
R = (df.unconverted - df.converted).values
print(f"データ数: {len(R)}")
ここでは、DNAメチル化状態を考察する。微生物・植物・動物はCpG以外でもメチル化を様々な用途(Restriction-Modefication system (R-M system),トランスポゾンの活動抑制,転写制御(〜分化と発生),環境への応答(〜老化・ガン化))で利用しているが、特にCpGメチル化を考察し、機能的な領域を探索する。すると、区間推定問題は、以下のように定義できる。
- 入力:各CpG siteにおける、bisulfite-seqでの(unconverted C, converted C)を読んだリード数を表すペア $\{m_i,u_i\}$(補足:$\left\{m_i,u_i\right\}$ から実数列を $\{r_i = u_i - m_i\}$ で選ぶ。)
- 出力:検出できた低メチル化領域のリスト
なお、メチル化計測データには以下のような種類がある。
- bisulfite sequencing (MethylC-seq): C -> T: bisulfite conversion
- methylation array: beta for selected CpG sites
- SMRT sequencing (PacBio): IPD (inter-pulse duration), IPD ratio for each base
- Nanopore sequencing (ONT)
- Peter A. Jones, Functions of DNA methylation: islands, start sites, gene bodies and beyond, Nature Reviews Genetics 13, 484-492 (July 2012) doi:10.1038/nrg3230: http://www.nature.com/nrg/journal/v13/n7/full/nrg3230.html
- Dirk Schübeler, Function and information content of DNA methylation, Nature 517, 321–326 (15 January 2015) doi:10.1038/nature14192: https://www.nature.com/articles/nature14192.html
また、これを解くモデルは以下の2つが有名である。
課題 | モデル | 概要 | 特徴 |
---|---|---|---|
1-1 | MSS(maximal segment sum)問題 | 与えられた数列上で、その上での和が最大となる区間を見つける問題。つまり、所与の実数列 $r_1,\ldots,r_n$ と正整数 $L$ に対し、次の式を満たす区間の集合 $S$ を求める。$$\underset{S}{\text{argmax}}\sum_{I\in S}\sum_{i\in I}r_i\ \text{ s.t. }\ \forall I\in S, \mid I\mid\geq L$$ | 実数列の長さ $n$ に関する動的計画法を用いることで、線形時間で解くことが可能。また、区間の長さを直接制御することも可能。 |
1-2 | HMM | パラメタと出力確率の関数型を適当に決め、Viterbiアルゴリズムで隠れ状態列を推定する問題。「高メチル化/低メチル化」の2つの隠れ状態を想定する。 | 隠れ状態列の推定(Viterbiアルゴリズム)は線形時間で可能だが、区間が細かく分割されすぎてしまうことがある。また、本来は「マルコフ性」や「区間の長さが幾何分布に従う」ことなどを吟味すべき。HSMMなどの拡張もある。 |
Q.1-1 (必須課題)
最小長制約付区間推定による低メチル化領域検出を実装して実際のデータに適用せよ。また、パラメタの選び方により、低メチル化領域の長さ(genomic distance or #CpGs)の分布はどう変わるか?平均・分散・中央値・90%分位点などをLの関数としてプロットせよ。
解答
Recursion
長さ $n$ の数列に対して、次のような量を計算することを考える。
記号 | 説明 | 再帰式 |
---|---|---|
$$S_n^0$$ | 数列の末尾である $r_n$ を含まないような区間の集合で、その上の和が最大のもの。 | $$S_n^0 = \max\left\{S_{n-1}^0,S_{n-1}^L\right\}$$ |
$$S_n^k\left(k=1,\ldots,L-1\right)$$ | 最も右にある区間が $r_n$ を含み、かつその長さが $k$ である区間の集合で、その上の和が最大のもの。 | $$S_n^k = S_{n-1}^{k-1} + \underline{r_n}$$ |
$$S_n^L$$ | 最も右にある区間が $r_n$ を含み、かつその長さが $L$ 以上である区間の集合で、その上の和が最大のもの。 | $$S_n^L = \max\left\{S_{n-1}^L,S_{n-1}^{L-1}\right\}+\underline{r_n}$$ |
となり、元の問題に対して $O(nL)$ のアルゴリズムが構築できた。ここで、さらに再帰式を使って式を簡潔にすると、
$$S_{n}^{L-1} = S_{n-1}^{L-2} + r_{n} = \cdots = S_{n-(L-1)}^0 + \sum_{n-(L-1)+1}^n r_i$$という関係が導けるので、
$$ \begin{aligned} S_{n}^0 &= \max\left\{S_{n-1}^0,S_{n-1}^L\right\}\\ S_{n}^L &= \max\left\{S_{n-1}^L,S_{n-L}^0 + \sum_{n-L+1}^{n-1}r_i\right\} + r_n \end{aligned} $$と変数の数を $L$ に関係になく $2$ 個に抑えることができるので、$O(n)$ のアルゴリズムが導けたことがわかる。なお、以下で初期化する。
$$ \begin{cases} S_{0}^0 &= 0\\ S_{0}^L &= -\infty\\ r_i &= -\infty\quad(2-L\leq i\leq0) \end{cases} $$TraceBack
次に、トレースバックについて考えると、メモリに関しては、$n$ 番目の値を考える時には $S_{n-L}^0,S_{n-1}^0,S_{n-1}^L$ がどの要素を取得しているかのみが必要となるが、それらから $n+1$ 番目の値を考える際に必要となる $S_{\left(n+1\right)-L}^0$ を再帰式無しで導くことができないので、実際には $S_{n-L}^0,\ldots,S_{n-1}^0,S_{n-1}^L$ の計 $L+1$ 個の集合がどの要素を保持していたか記憶する必要がある。
単純にトレースバックポインタを保持すれば良いことになる。
from kerasy.Bio.maxsets import MSS
model = MSS()
model.run(R, limit=5, verbose=1)
np.save(f"{limit:>02}.npy", model.cotinuous_area)
※ verbose=1
だとjupyter notebookのファイル容量制限を超えてしまうので、以下のプログラムファイルを作成して実行した。
kadai03_MSS.py
def decorate_axes(ax, stats, xlabel):
ax.set_xlabel(xlabel, fontsize=16)
ax.set_title(stats, fontsize=18)
ax.set_ylabel(stats, fontsize=16)
return ax
fig, ((ax1,ax2),(ax3,ax4),(ax5,ax6)) = plt.subplots(nrows=3,ncols=2,figsize=(15,18), sharex="all")
for limit in range(5,11):
detected_area = np.load(f"{limit:>02}.npy")
detected_length = detected_area[:,1]
median, tile90 = np.percentile(a=detected_length, q=[50,90])
mean = np.mean(detected_length)
var = np.var(detected_length)
score = np.sum(detected_length)
num = len(detected_length)
ax1.scatter(limit, median, color="red", s=100)
ax2.scatter(limit, tile90, color="blue", s=100)
ax3.scatter(limit, mean, color="green", s=100)
ax4.scatter(limit, var, color="orange", s=100)
ax5.scatter(limit, score, color="black", s=100)
ax6.scatter(limit, num, color="black", s=100)
ax1 = decorate_axes(ax1, stats="median", xlabel="limit")
ax2 = decorate_axes(ax2, stats="90%-tile", xlabel="limit")
ax3 = decorate_axes(ax3, stats="mean", xlabel="limit")
ax4 = decorate_axes(ax4, stats="variance", xlabel="limit")
ax5 = decorate_axes(ax5, stats="score", xlabel="limit")
ax6 = decorate_axes(ax6, stats="num detected area", xlabel="limit")
plt.suptitle("The relationship between\nlimit length and Statistics.", fontsize=20)
# plt.tight_layout()
plt.show()
plt.hist(detected_length, bins=30)
plt.title(f"The histgram of detected length (limit={limit})")
plt.xlabel("detected length"), plt.ylabel("frequency")
plt.show()
Q.1-2 (選択課題)
HMMによる低メチル化領域検出を実装して実際のデータに適用せよ。また、パラメタの選び方により、低メチル化領域の長さ(genomic distance or #CpGs)の分布はどう変わるか?平均・分散・中央値・90%分位点などを $\theta,t$ の関数としてプロットせよ。
なお、この時簡単のためHMMは以下の条件を満たす。
- 2つの隠れ状態(
Hypermethylated
,Hypomethylated
)を取る。 - 遷移確率は $p_1 = p_2 = t$ とする。(等しい)
- 出力確率は以下のように決める。 $$ \begin{cases} P\left(m | \text{hyper} \right) &= 1 - P\left(u | \text{hyper} \right) &= \theta\\ P\left(u | \text{hypo} \right) &= 1 - P\left(m | \text{hypo} \right) &= \theta\end{cases}\\ \therefore P\left(m_i,u_i | s_i = \text{hyper} \right) = \theta^{m_i}\ast\left(1-\theta\right)^{u_i} $$
この時、
$$ \begin{aligned} \sum_{m_i}\sum_{u_i}P\left(m_i,u_i | s_i = \text{hyper} \right) &= \sum_{m_i\geq1}\theta^{m_i}\ast\sum_{u_i\geq1}\left(1-\theta\right)^{u_i}\\ &= \left(\frac{\theta}{1-\theta}\right)\left(\frac{1-\theta}{\theta}\right)\\ &= 1 \end{aligned} $$となるので、この出力確率が確率の条件を満たすことがわかる。($P\left(m_i,u_i | s_i = \text{hypo} \right) $ も同様。)
※ ここで、題意の意図を汲み取って $m_i\geq1,u_i\geq1$ と定義しているが、正直この定義はよくわからない。また、この確率分布では、単純なリード数が増えることにもペナルティがかかるため、あまり良い定義とは思えない。(※問題文の解釈が間違っている可能性もある。)
例えば、全体で読んだリード数が $\text{total}_i( = m_i + u_i)$ と与えられた状況で、
$$P\left(m_i,u_i | s_i = \text{hyper} \right) = _{\text{total}_i}C_{m_i} \theta^{m_i} \ast \left(1-\theta\right)^{u_i}$$という二項分布でモデル化を行うことも一つの手である。(この時、$\text{total}_i$ は単純なマルコフモデルに従うと考えればサンプリングも行えるが、サンプリングを行う有用性があまり考えられなかったので今回は割愛する。)
したがって、この条件で $Q$ 関数を表すと、
$$ \begin{aligned} Q\left(\boldsymbol{\theta}, \boldsymbol{\theta}^{\mathrm{old}}\right)=& \sum_{k=1}^{K} \gamma\left(z_{1 k}\right) \ln \pi_{k}+\sum_{n=2}^{N} \sum_{j=1}^{K} \sum_{k=1}^{K} \xi\left(z_{n-1, j}, z_{n k}\right) \ln A_{j k} \\ &+\sum_{n=1}^{N} \sum_{k=1}^{K}\gamma\left(z_{n k}\right) \ln \phi_k\left(m_n,u_n\right) \end{aligned}\qquad (13.17) $$となる。なお、ここで
$$ \phi_k\left(m_n,u_n\right) = \begin{cases} \theta^{m_n}\ast\left(1-\theta\right)^{u_n} & \left(z_{nk} = \text{hyper}\right)\\ \left(1-\theta\right)^{m_n}\ast\theta^{u_n} & \left(z_{nk} = \text{hypo}\right)\\ \end{cases} $$解答
※ 先ほどと同様に、以下のプログラムファイルを作成して実行した。
kadai03_HMM.py
median¶
$\theta$ \ $t$ | 0.3 | 0.5 | 0.7 |
---|---|---|---|
0.2 | 16.0 | 30.0 | 36.0 |
0.5 | 1.0 | 1.0 | - |
0.8 | 2.0 | 2.0 | 2.0 |
90% tile¶
$\theta$ \ $t$ | 0.3 | 0.5 | 0.7 |
---|---|---|---|
0.2 | 123.0 | 164.0 | 196.0 |
0.5 | 1.0 | 1.0 | - |
0.8 | 4.0 | 6.0 | 6.0 |
mean¶
$\theta$ \ $t$ | 0.3 | 0.5 | 0.7 |
---|---|---|---|
0.2 | 44.89 | 64.36 | 76.48 |
0.5 | 1.00 | 1.00 | - |
0.8 | 5.31 | 8.22 | 8.57 |
variance¶
$\theta$ \ $t$ | 0.3 | 0.5 | 0.7 |
---|---|---|---|
0.2 | 5568.55 | 9178.02 | 13449.15 |
0.5 | 0.00 | 0.00 | - |
0.8 | 488.51 | 1020.93 | 1374.86 |
score¶
$\theta$ \ $t$ | 0.3 | 0.5 | 0.7 |
---|---|---|---|
0.2 | 45792177 | 45854238 | 46085466 |
0.5 | 25664100 | 1 | - |
0.8 | 5411770 | 5123127 | 5167225 |
num¶
$\theta$ \ $t$ | 0.3 | 0.5 | 0.7 |
---|---|---|---|
0.2 | 1020041 | 712421 | 602620 |
0.5 | 25664100 | 1 | 0 |
0.8 | 1020041 | 623326 | 602620 |
パラメータ学習¶
※ ここでは、遷移確率がただ一つのパラメータ $t$ に依存し、出力確率もただ一つのパラメータ $\theta$ に依存する kerasy.ML.HMM.MSSHMM ではなく、より一般化された Binomial HMM を利用することにする。
from kerasy.ML.HMM import BinomialHMM
from kerasy.clib._pyutils import extract_continuous_area
X = df[["converted", "unconverted"]].values
model = BinomialHMM(n_hstates=2)
model.fit(X)
# Not necessary
model.save_params("kadai03_HMM.json")
# Not necessary
model.load_params("kadai03_HMM.json")
predictions = model.predict(X).astype(np.int32)
detected_area = np.asarray(extract_continuous_area(predictions), dtype=np.int32)
detected_length = detected_area[:,1]
plt.hist(detected_length, bins=30)
plt.title(f"The histgram of detected length (HMM)")
plt.xlabel("detected length"), plt.ylabel("frequency")
plt.show()
median, tile90 = np.percentile(a=detected_length, q=[50,90])
print(f"- median : {median}")
print(f"- 90%-tile: {tile90}")
print(f"- mean : {np.mean(detected_length)}")
print(f"- variance: {np.var(detected_length)}")
print(f"- score : {np.sum(detected_length)}")
print(f"- num : {len(detected_length)}")
課題2
以下のデータの関連を調べよ。例えば、「(プロモータ領域の)CpG islandsは低メチル化しているか」「高/低メチル化領域を伴う遺伝子は、それぞれ特徴があるか」など。
- 低メチル化領域:g;Profiler -> http://biit.cs.ut.ee/gprofiler/gost など。
- CpG islands:hg38参照配列から検出を行う。この時、CpG islandsの定義は "Gardiner-Garden, Frommer (1987) CpG islands in vertebrate genomes" で定義されているものがよく参照される。
(CGIs are regions with >= 200 bp, O/E CpG >= 0.6, %GC >= 0.5) - 遺伝子領域:GENCODEを利用する。
解答
raise NotImplementedError("Not Checked.")
課題3
動的計画法(応用例題)額面が $d_k\left(1\leq k\leq K\right)$ の $K$ 種類の硬貨が通用しているとする。 $T$ 単位をちょうど支払うのに必要な硬貨の最小枚数 $m(T)$ は?ただし、額面 $d_1$ の硬貨は同時に $L$ 枚以下しか使えないものとする。
$m(T)$ と、具体的な支払方法を出力するアルゴリズムを実装する。この時の計算量を $K,T,L$ の式で表現し、実際に計測する。
解答
# 内訳を整形して記述する関数
def breakdown(combs):
""" display breakdowns """
use_coins = sorted(set(combs))
num_coins = [combs.count(coin) for coin in use_coins]
total_pay = [n*coin for n,coin in zip(use_coins,num_coins)]
width_coin = max([len(str(e)) for e in use_coins]+[len("coins")])
width_num = max([len(str(e)) for e in num_coins]+[len("number")])
width_total = max([len(str(e)) for e in total_pay]+[len("pay"),len(str(sum(total_pay)))])
width_line = width_coin+width_num+width_total+2
print_func = lambda c,n,p: print(f"{c:^{width_coin}}|{n:>{width_num}}|{p:>{width_total}}")
print_func('coins','number','pay')
print("="*width_line)
for coin,num,t in zip(use_coins,num_coins,total_pay):
print_func(coin,num,t)
print("-"*width_line)
print_func('total',sum(num_coins),sum(total_pay))
アルゴリズム¶
制約なし¶
まず、額面 $d_1$ の硬貨が $L$ 回しか使えないという制約式がないとして、上記の問題を考える。すると、この問題は最適性の原理を満たす。
$$m\left(T\right) = \min_k\left\{m\left(T - d_k\right)\right\} + 1$$
これは当然のことで、もしこの等号が成り立たなければ、$m\left(T - d_k\right)$ と $m\left(T\right)$ のどちらかが最小枚数でないことになってしまうので、矛盾が生じてしまう。ゆえに、この再帰的関係の右辺を展開してゆけば、そのうち解が求まる。
def not_restricted_smart_pay(coins, total):
"""
Find the minimum number of coin combinations by using Dynamic Programming.
@params coins: (list) Coin Types.
@params total: (int) Amount of Payment.
"""
total += 1 # because 0-origin.
coins = np.asarray(coins, dtype=int)
if len(np.unique(coins)) < len(coins):
raise ValueError("All elements of `coins` must be different integers.")
# Initialization.
B = np.zeros(shape=(total), dtype=int) # Memory for Traceback.
m = np.full(fill_value=np.inf, shape=(total))
m[0] = 0
for coin in coins: m[coin] = 1
# Recursion
for t in range(1,total):
cands = [m[t-coin] if (t-coin)>=0 else np.inf for coin in coins]
if np.all(np.array(cands)==np.inf): continue
k = np.argmin(cands)
m[t] = m[t-coins[k]]+1
B[t] = t-coins[k]
# Trace Back.
idx = total-1
combs = []
while idx:
last = B[idx]
combs.append(idx-last)
idx = last
breakdown(combs)
not_restricted_smart_pay([11,7,4,2], 100)
制約付き①¶
次に、ただし、額面 $d_1$ の効果は同時に $L$ 枚以下しか使えないものとする。という制約条件を考える。そこで、「$d_1$ をちょうど $l$ 枚だけ使って $t$ 単位支払うのに必要な硬貨の枚数の最小枚数」 を表す $m_l(t)\left(0\leq l\leq L\right)$ を導入し、先ほどの区間分割アルゴリズムの考え方を用いて、これらの効率的な求め方を考える。
まず、純粋に $L+1$ 個の状態を別々の変数として保持すると、以下のようになる。
$$ \begin{cases} \begin{aligned} m_0(t) &= \min_{k\neq1}\left\{m_0(t-d_k)\right\}\\ m_l(t) &= m_{l-1}\left(t-d_1\right) + 1\qquad\left(0 < l < L\right)\\ m_L(t) &= \min\left\{\min_{k\neq1}\left\{m_{L}(t-d_k)\right\}, m_{L-1}\left(t-d_1\right)\right\} + 1\\ \end{aligned} \end{cases} $$ここで、
$$ \begin{aligned} m_{L-1}(t) &= m_{L-2}(t-d_1)+1\\ &= \cdots\\ &= m_0\left(t - (L-1)d_1\right) + \left(L-1\right) \end{aligned} $$という関係が導けるので、上記を用いて整理すると、
$$ \begin{cases} \begin{aligned} m_0(t) &= \min_{k\neq1}\left\{m_0(t-d_k)\right\}\\ m_l(t) &= m_{0}\left(t-ld_1\right) + l\qquad\left(0 < l < L\right)\\ m_L(t) &= \min\left\{\min_{k\neq1}\left\{m_{L}(t-d_k)\right\}+1, m_0\left(t-Ld_1\right) + L\right\}\\ \end{aligned} \end{cases} $$となり、$L$ に依存することなく、"2つ"の変数のみでこれを表せた。
def restricted_smart_pay(coins, total, L):
# Initialization.
coins = np.asarray(coins, dtype=int)
if len(np.unique(coins)) < len(coins):
raise ValueError("All elements of `coins` must be different integers.")
K = len(coins)
total += 1 # because 0-origin.
B = np.zeros(shape=(total*2), dtype=int) # Memory for Traceback.
m = np.full(fill_value=np.inf, shape=(total,2))
m[0,0] = 0
for k in range(1,K): m[coins[k],0] = 1
# Recursion
for t in range(1,total):
# m0
cands = [m[t-coins[k], 0] if (t-coins[k])>=0 else np.inf for k in range(1,K)]
if np.all(np.array(cands)==np.inf): continue
k = np.argmin(cands)+1
m[t,0] = m[t-coins[k], 0] + 1
B[t] = t-coins[k]
# mL
cands = [m[t-coins[k], 1] if (t-coins[k])>=0 else np.inf for k in range(1,K)]
k = np.argmin(cands)+1
if t-L*coins[0]>=0 and m[t-L*coins[0],0]+L<m[t-coins[k],1]+1:
m[t,1] = m[t-L*coins[0],0]+L
B[t+total] = t-L*coins[0]
else:
if np.all(np.array(cands)==np.inf): continue
m[t,1] = m[t-coins[k],1]+1
B[t+total] = t-coins[k]+total
num_d1 = np.argmin([m[-1-coins[0]*l,0]+l for l in range(L+1)])
idx = total-1-num_d1*coins[0] if num_d1!=L else total*2-1
combs = [coins[0] for _ in range(num_d1)]
while idx:
last = B[idx]
combs.append(idx%total-last%total)
idx = last
if num_d1==L: combs.pop(-1)
breakdown(combs)
restricted_smart_pay([11,7,4,2], 100, 1)
制約付き②¶
しかし、
$$ \begin{aligned} m_L(t) &= \min \begin{cases} \begin{aligned} &\min_{k\neq1}\left\{m_{L}(t-d_k)\right\}+1\\ &m_0\left(t-Ld_1\right) + L \end{aligned} \end{cases}\\ &=\min \begin{cases} \begin{aligned} &\min_{k\neq1}\left\{m_{0}\left(\left(t-Ld_1\right)-d_k\right)\right\}+\left(L+1\right)\\ &m_0\left(t-Ld_1\right) + L \end{aligned} \end{cases}\\ \end{aligned} $$と変形でき、最終式の中身が等しくなることは明らか。したがって、
$$ \begin{cases} \begin{aligned} m_0(t) &= \min_{k\neq1}\left\{m_0(t-d_k)\right\}\\ m_l(t) &= m_{0}\left(t-ld_1\right) + l\qquad\left(0 < l \leq L\right)\\ \end{aligned} \end{cases} $$となり、$L$ に依存することなく、"1つ"の変数のみでこれを表せた。
def smart_pay(coins, total, limit=None, verbose=1):
"""
Find the minimum number of coin combinations by using Dynamic Programming.
@params coins: (int list) Coins.
@params total: (int) Amount of Payment.
@params limit: (int) Maximum number of times a restricted coin can be used.
"""
total += 1 # because 0-origin.
if len(set(coins)) < len(coins):
raise ValueError("All elements of `coins` must be different integers.")
restricted = coins[0]
free_coins = coins[1:]
if limit is None:
limit = total//restricted+1
elif verbose:
print(f'{restricted} coin can only be used up to {limit} times at the same time.')
# Initialization.
B = [0 for _ in range(total)] # Memory for Traceback.
m = [0 if t==0 else 1 if t in free_coins else float('inf') for t in range(total)]
# Recursion
for t in range(1,total):
cands = [m[t-coin] if (t-coin)>=0 else float('inf') for coin in free_coins]
if sum([e!=float('inf') for e in cands])==0:
continue # B[t]=0; m[t]=float('inf') : default.
minnum = min(cands)
m[t],B[t] = [(e+1,t-coin) for e,coin in zip(cands,free_coins) if e==minnum][0]
ms = [(l,m[-1-restricted*l]+l) for l in range(limit+1) if restricted*l<=total]
num_restricted, num_total = min(ms, key=lambda x:x[1])
idx = total-1-restricted*num_restricted
combs = [restricted for _ in range(num_restricted)]
while idx:
last = B[idx]
combs.append(idx-last)
idx = last
if verbose: breakdown(combs)
for l in range(0,9,2):
smart_pay([11,7,4,2], 100, limit=l)
print()
計算量¶
$L$ に依存することなく、"1つ"の変数のみで再帰式を表せたことから、アルゴリズムの計算量は $O\left(KT\right)$ となることがわかる。
実測¶
from kerasy.utils import measure_complexity
seed = 0
coin_cands = np.arange(1,100)
limit
¶
total = 100000
K = 10
coins = np.random.RandomState(seed).choice(coin_cands, K, replace=False)
for l in [10,50,100]:
kwargs = {'limit':l, 'verbose':0}
time = measure_complexity(smart_pay, coins, total, **kwargs)
print(f"L:{l:>3}, processing time: {time:>5.3f}")
以上より、少し雑だが $L$ が計算量に関与しないことがわかる。続いて、$K$ と $T$ について調べる。
kwargs = {'limit':10, 'verbose':0}
Tcands = [1000,10000,100000,1000000]
Kcands = [3,30,300]
coin_cands = np.arange(1,1000)
print_func = lambda K,T,time: print(f"{K:>5}|{T:>7}|{time:>7.3f}[s]")
memory = []
print(f"{'coins':^5}|{'total':^7}|{'time[s]':>10}")
print("="*24)
for K in Kcands:
coins = np.random.RandomState(seed).choice(coin_cands, K, replace=False)
for T in Tcands:
time = measure_complexity(smart_pay, coins, T, repetitions_=3, **kwargs)
print_func(K,T,time)
memory.append([K,T,time])
上記より、$K,T$ に関しても線形に計算量が増えていることがわかる。プロットで確かめると、以下のようになる。
K
¶
fig, axes = plt.subplots(nrows=1, ncols=3, figsize=(18,6))
for ax,k in zip(axes,Kcands):
tmp = [(e[1],e[2]) for e in memory if e[0]==k]
for t,time in tmp:
ax.scatter(t,time, color="red")
ax.grid()
ax.set_xscale("log"), ax.set_yscale("log")
ax.set_title(f"$K={k}$"), ax.set_xlabel("total amount"), ax.set_ylabel("Processing time")
plt.show()
T
¶
fig, axes = plt.subplots(nrows=1, ncols=4, figsize=(24,6))
for ax,T in zip(axes,Tcands):
tmp = [(e[0],e[2]) for e in memory if e[1]==t]
for k,time in tmp:
ax.scatter(k,time, color="blue")
ax.grid()
ax.set_xscale("log"), ax.set_yscale("log")
ax.set_title(f"$T={t}$"), ax.set_xlabel("total amount"), ax.set_ylabel("Processing time")
plt.show()
こちらも、共に線形に増えているように見える。