最長増加部分列(LIS: Longest Increasing Subsequence)を求めるアルゴリズム(O(N log N)) の流れを、図付きでわかりやすく解説します。
dp[i]は「長さがi+1の増加部分列の末尾の最小値」を保持します。- 各要素
A[i]を見ながら、dpの中でA[i]より初めて大きい or 等しい値を二分探索で探し、- その位置を
A[i]で置き換えます。
- 最終的な
dpの長さが答えになります。
初期状態:dp = [](空)
- dp は空 →
2を追加
dp = [2]
3はdpの最後の要素より大きい → 末尾に追加
dp = [2, 3]
1はdp[0] (=2)より小さい → 先頭を置き換える
dp = [1, 3]
6はdpの最後より大きい → 末尾に追加
dp = [1, 3, 6]
4はdp[2] (=6)より小さいが、dp[1] (=3)より大きい- →
dp[2] (=6)を4に置き換える
dp = [1, 3, 4]
5はdp[2] (=4)より大きい → 末尾に追加
dp = [1, 3, 4, 5]
dp.length = 4- よって、最長増加部分列の長さは 4
A = [2, 3, 1, 6, 4, 5]
↓ ↓ ↓ ↓ ↓
dp = [1, 3, 4, 5] ← 各ステップで更新
↑
長さ4のLIS
dp配列そのものは「実際の LIS を構成する部分列」とは限らない(あくまで長さを求めるための補助)。- 実際の増加部分列を求めたい場合は、トラッキングが必要(別途インデックス配列を持つ)。
入力:
A = [2, 3, 1, 6, 4, 5]
この配列に対して最長増加部分列(LIS)の長さは 4 で、
実際の LIS としては **[2, 3, 4, 5]** がありえます。
でも、アルゴリズムで途中生成される dp = [1, 3, 4, 5] なので混乱しがちです。
この dp は **最長増加部分列を構成する「候補の末尾の最小値」**を管理していて、
「最長増加部分列そのものではない」からです。
| i | A[i] | dpの更新 | dp |
|---|---|---|---|
| 0 | 2 | 追加 | [2] |
| 1 | 3 | 追加 | [2, 3] |
| 2 | 1 | dp[0]更新 | [1, 3] |
| 3 | 6 | 追加 | [1, 3, 6] |
| 4 | 4 | dp[2]更新 | [1, 3, 4] |
| 5 | 5 | 追加 | [1, 3, 4, 5] |
→ この最終 dp は「長さ4の増加列が存在する」という情報を持っているだけで、
実際の列 [2, 3, 4, 5] の構成順序ではありません。
dpは 実際の LIS ではなく、その長さを得るための道具。[2, 3, 4, 5]のように、正しい LIS は他のルートで得られます。- もし 実際の LIS を出力したい場合は、別の配列でパスを記録するようにします。
図解付きで「実際のLIS(列)を復元する処理」を分かりやすく解説します!
流れを追いつつ、dp, dpIndex, prevIndex の3つの配列がどう変化していくかをステップごとに図解していきます。
A = [2, 3, 1, 6, 4, 5]| 配列 | 意味 |
|---|---|
dp |
各長さのLISの末尾の最小値 |
dpIndex |
dp[i] が A の何番目だったか |
prevIndex |
A[i] の直前のLIS要素のインデックス(たどると元の列がわかる) |
dpは空 → 追加dp = [2]dpIndex = [0]prevIndex[0] = -1
dp: [2]
dpIndex: [0]
prevIndex: [-1, _, _, _, _, _]
- 3 > dp[0] → 追加
dp = [2, 3]dpIndex = [0, 1]prevIndex[1] = dpIndex[0] = 0
dp: [2, 3]
dpIndex: [0, 1]
prevIndex: [-1, 0, _, _, _, _]
- 1 < dp[0] → dp[0] を更新
dp = [1, 3]dpIndex = [2, 1]prevIndex[2] = -1(先頭なので)
dp: [1, 3]
dpIndex: [2, 1]
prevIndex: [-1, 0, -1, _, _, _]
- 6 > dp[1] → 追加
dp = [1, 3, 6]dpIndex = [2, 1, 3]prevIndex[3] = dpIndex[1] = 1
dp: [1, 3, 6]
dpIndex: [2, 1, 3]
prevIndex: [-1, 0, -1, 1, _, _]
- 4 < dp[2] (=6) → dp[2] を更新
dp = [1, 3, 4]dpIndex = [2, 1, 4]prevIndex[4] = dpIndex[1] = 1
dp: [1, 3, 4]
dpIndex: [2, 1, 4]
prevIndex: [-1, 0, -1, 1, 1, _]
- 5 > dp[2] (=4) → 追加
dp = [1, 3, 4, 5]dpIndex = [2, 1, 4, 5]prevIndex[5] = dpIndex[2] = 4
dp: [1, 3, 4, 5]
dpIndex: [2, 1, 4, 5]
prevIndex: [-1, 0, -1, 1, 1, 4]
最後の LIS の末尾は dpIndex[3] = 5 → A[5] = 5
そこから prevIndex をたどっていく:
index = 5 → 4 → 1 → 0
A[index] = 5 → 4 → 3 → 2
→ 逆順なので LIS = [2, 3, 4, 5]
4
2 3 4 5dpは LIS の長さを求めるための道具dpIndex+prevIndexを使えば、実際の列も完全復元できる!