1. 最長増加部分列問題
最長増加部分列問題 (LIS = Longest Increasing Subsequence) は,与えられた数列 $A = (a_0, a_1, \dots, a_{n - 1})$ の部分列であって, 単調増加であるもののうち最長のもの,ないし,その長さを求める問題.
2. 長さだけで良い場合
列 $B = (b_0, b_1, \dots, b_{m - 1})$ を次のように構築する.
2.1. 狭義単調列の場合
- 最初は,$B = ()$.
- $i = 0, 1, …, n - 1$ の順に,次を実行.
- $a_i \leq b_k$ となる最小の $k$ を二分探索で求める.C++ なら次のコード:
k = lower_bound(ALL(B), a[i]) - B.begin();
- このような $k$ が存在しない場合には,$a_i$ を $B$ に加える.($B$ の長さが 1 増える.)
- 存在する場合には,$b_k$ を $a_i$ で置き換える. ($a_i = b_k$ なら何もしないことになる)
- $a_i \leq b_k$ となる最小の $k$ を二分探索で求める.C++ なら次のコード:
- 作成された $B$ の長さが,求める長さになる.
途中の段階 ($a_i$ を処理したとき) の $b_k$ は,「$(a_0, \dots, a_i)$ の長さ $k$ の増加列の末項の最小値」 になっている.
2.2. 広義単調列の場合
上とほぼ同じだが,条件「$a_i \leq b_k$」を「$a_i < b_k$」 に差し替える. C++ なら次のコード:
k = upper_bound(ALL(B), a[i]) - B.begin();
3. 列を 1 つ求めたい場合
上記の構築の場合,どの $a_i$ についても,「それが LIS に入るならば,その前の項が何であるか」は決まっている. これを記録していくようにする.
- $B$ と同時に,同じ長さの列 $J$ も作っていく.$b_k$ が,「$i$ まで見たときにもっとも効率の良い長さ $k$ の増加列の末項の値」であるのに対し,$j_k$ は,「$i$ まで見たときにもっとも効率の良い長さ $k$ の増加列の末項の $A$ における添字」である.すなわち,$a_{j_k} = b_k$ が成り立つ.
- (全体として) 長さ $n$ の配列 $C$ を作る.$c_i$ は,$a_i$ が LIS に入る場合の,1つ前の項の $A$ における添字である.
具体的な手順は以下の通り.
- 最初は,$B = ()$,$J = ()$.
- $i = 0, 1, …, n - 1$ の順に,次を実行する.
- この時点で,$B = (b_0, \dots, b_m)$,$J = (j_0, \dots, j_m)$ であるとする.
- $a_i \leq b_k$ となる (広義単調列なら,$a_i < b_k$ となる) 最小の $k$ を二分探索で求める.
- このような $k$ が存在しない場合には,
- $B$ には,$a_i$ を加える.
- $J$ には,$i$ を加える.
- $k := m + 1$ とする.($B = ()$ の場合には,$m = -1$ と考えて,$k := 0$)
- 存在する場合には,
- $b_k$ を $a_i$ で置き換える.
- $j_k$ を $i$ で置き換える.
- このような $k$ が存在しない場合には,
- $c_i$ については,
- $k > 0$ の場合には,$c_i := j_{k - 1}$ とする.
- $k = 0$ の場合には,$c_i := -1$ とする.
- 最終的に $B = (b_1, \dots, b_m)$,$J = (j_1, \dots, j_m)$ が得られたとする.
1 つの LIS が,次の手順で求められる.
- 求める LIS の末項は,$b_m$ すなわち $a_{j_m}$ である.
- $a_{j}$ が LIS の項である時,$c_j > 0$ なら,その一つ前の項は $a_{c_j}$ である.$c_j = -1$ ならそれが初項である.