最長増加部分列 (LIS) について
Latest Author
最長増加部分列問題 (LIS) について
最長増加部分列問題とは?
最長増加部分列問題は、次のような有名問題です。
- 長さ の数列 が入力されるとき、そのうちいくつかの値を順番を変えずに取り出したときに、これが増加列となる中で最大の長さのものを求めなさい。
- 数列 が「増加列」であるとは、 が成り立つことである。
例えば、次のような例があります。
- のとき、最長増加部分列は
- のとき、最長増加部分列は や 、 などたくさんあります。
この記事では、最長増加部分列問題を通して、いろいろな問題と解法に触れていこうと思います。
最長増加部分列の解法
最長増加部分列は計算量 で解くことができます。しかし、それに至るまでにいろいろなアプローチがあるので順に説明していきます。
ここでは、最長増加部分列の長さを求める問題について考えます。
解法 #1: 通り全探索 - 計算量
長さ の数列の「部分列」(数列のいくつかの値を順番を変えずに取り出した数列) は、"長さ の数列" を含めて 通りあります。
例えば、数列 の部分列は、 の 通りあります。
なぜ部分列は 通りあるのでしょうか?
数列のいくつかの値に丸を付けて、丸がつけられた数を左から読んだのが「部分列」というイメージで考えます。
それぞれの値は丸をつけられるかつけられないかの 通りなので、全部で 通りです。
どのように 通り全探索することができるのでしょうか?これには つの方法があります。
- 再帰を使った全探索
- 進数を使った全探索
まず、再帰を使った全探索について説明します。実装すると、下のようなコードになります。
ans = 0 function solve(pos, seq): if pos == N: if seq が増加部分列: ans := max(ans, seq の長さ) return solve(pos + 1, seq) solve(pos + 1, seq の末尾に A[pos] を追加したもの)
solve(0, [])
を呼び出すと、計算が始まります。pos
は「現在選ぶかどうか決めようとしている数が A[pos]
」、seq
は「現在の部分列」です。
その擬似コードの ~ 行目は、選ぶ部分列が決まった場合の処理です。つまり、~ 行目を 回通ります。
7, 8 行目の意味は、A[pos]
を選ぶか選ばないかで枝分かれする、という意味です。その第 引数が pos+1
になっているのは、A[pos]
を選ぶかどうか決める次に A[pos+1]
を選ぶかどうか決める、からである。
次に、 進数を利用した全探索について説明します。実装すると、下のようなコードになります。
ans = 0 for i = 0 to 2^N-1: seq = [] for j = 0 to N-1: if (i を 2 進数で表したときの 2^j の位が 1 である): seq := (seq の末尾に A[j] を追加したもの) if seq が増加部分列: ans := max(ans, seq の長さ)
例えば、 のとき。~ までの数を 進数で表すと 000, 001, 010, 011, 100, 101, 110, 111
となります。ここで「」となっいる桁に対応する数に丸を付け、「」となっている桁に対応する数には丸をつけない、という感じです。
すると、ありうる 通りすべての部分列 seq
について調べることができます。
計算量は、両者ともに 通りすべてに対して増加部分列判定をするので、 です。
初心者にとって再帰を理解するのは難しいと思うので、 進数を利用した全探索の方が分かりやすいでしょう。実装量は両方そんなに変わりません。
解法 #2: 動的計画法 (DP) で計算量改善! - 計算量
この問題は、動的計画法 (DP) で解くことができます。最長増加部分列問題を DP を使って解く方法を説明します。
DP を理解していない人も、方法を読むことによって理解が進む良い問題の一例なので、ぜひ読んでみましょう。
左から順に丸を付けるかどうか決めることを考えます。 を「最後に丸を付けた値が のときの、最長増加部分列の長さ」とします。
までの値が求まっているとき、なんと の値も求まってしまいます。
は、 かつ であるような の中での の最大値 (ない場合は ) に、 を足したものになります。
なぜなら、 での答えとなる増加部分列の末尾に を追加したものが、最適になる数少ない候補だからです。
図で表すと、こんな感じの処理をすることになります。下図では、一番最後の の値を、今までに求めた の値を使って求めています。矢印を見ると DP が何なのか分かりやすいかもしれません。

実装は次のようになります。
for i = 0 to N-1: B[i] = 1 // 増加部分列が A[i] だけになるパターン for j = 0 to i-1: if A[j] < A[i]: B[i] := max(dp[i], dp[j] + 1) // 増加部分列を A[j] から A[i] に「つなげる」パターン ans = 0 for i = 0 to N-1: ans := max(ans, B[i])
計算量は明らかに で、先ほどの全探索の方法より改善しています。
このように、問題を「順番を決めて」前に求まった結果を利用して答えを求め、全探索の計算量を改善するみたいな方法を、「動的計画法」と言います。