最長増加部分列の長さ取得アルゴリズムLISをpythonで書いてみる
最長増加部分列(Longest Increasing Subsequence )とは
数列 が与えられたとき, その部分列
であって
のとき
を満たすものを増加部分列と言います.
その中で最も長いものの長さを求めるアルゴリズムを以下に説明していきます.
1. 計算量が
のアルゴリズム
まずは, 帰納的に長さを求めることを考えましょう.
とすると, 次のようなアルゴリズムが書けます.
def LIS(L): res = 0 dp = [1] * N for i in range(N): for j in range(i): if L[j] < L[i]: dp[i] = max(dp[i], dp[j]+1) res = max(res, dp[i]) return res
,
についての二重ループが含まれているため, このアルゴリズムの計算量は
です.
考え方
を
より小さい自然数とする.
仮に任意の に対して数列
の増加部分列のうち, 最後が
となるようなもののうちの最長増加部分列の長さ
が与えられたとしましょう.
このとき, 数列の増加部分列のうち, 最後が
となるような最長部分列の長さは,
という漸化式で与えらることを用います.
ちなみに, 漸化式の2行目ですが,
あるいは, に対して
を
と定めたとき,
と表すことができます.
2. 計算量が
のアルゴリズム
とすると, 下記のアルゴリズムで求めることが出来ます.
def LIS(L): from bisect import bisect_left seq = [] for ai in L: pos = bisect_left(seq, ai) if len(seq) <= pos: seq.append(ai) else: seq[pos] = ai return len(seq)
のループが含まれている一方で, 要素の追加ラベルを探すのには二分探索を用いています.
そのため, このアルゴリズムの計算量は です.
ただし seq そのものは最長増加部分列になっているわけではないので注意しましょう.
考え方
を固定して,
の値を数列
の第
項までを用いて表すことを考えましょう.
すると, これは について単調減少になるはずなので, その場合に更新することを考えています.
更新する際に考えなくてはならないのは が
の第何項に該当するかですが,
は常に単調減少な配列になっていることから二分探索 bisect を用いることができて計算量を減らしています.
3. 広義単調増大部分列の場合
追加された要素の挿入位置を探す際に, 二分探索して同じ数値が得られる場合には考えられる右端の index を返すようにします.
このためには, `bisect_left` の代わりに, `bisect` もしくは `bisect_right` を用います.
bisect --- 配列二分法アルゴリズム — Python 3.9.4 ドキュメント
def improper_LIS(L): from bisect import bisect seq = [] for ai in L: pos = bisect(seq, ai) if len(seq) <= pos: seq.append(ai) else: seq[pos] = ai return len(seq)