Pythonで最長増加部分列(LIS)の長さを求めるプログラム【二分探索でO(n log n)】
数値のリストが与えられたとき、その中から最長増加部分列(LIS: Longest Increasing Subsequence)の長さを求める問題を考えます。たとえば、入力が [6, 1, 7, 2, 8, 3, 4, 5] の場合、最長増加部分列は [2, 3, 4, 5, 6] となるため、答えは 5 になります。
本記事では、単純な動的計画法(O(n²))よりも高速な、二分探索を組み合わせた O(n log n) のアルゴリズムをPythonで実装する方法を解説します。
アルゴリズムの手順
numsと同じサイズの配列tailsを用意し、すべての要素を 0 で初期化します。size := 0とします。numsの各要素xに対して、以下の処理を行います。i := 0、j := sizeとします。i ≠ jである間、次を繰り返します。mid := i + (j − i) / 2とします。tails[mid] < xならばi := mid + 1、そうでなければj := midとします。
tails[i] := xとします。size := max(i + 1, size)とします。
最後に
sizeを返します。
なぜこのアルゴリズムが機能するのか
配列 tails[k] には、「長さ k+1 の増加部分列の末尾の値として考えられる最小値」が保存されます。この配列は常に昇順に保たれるため、新しい要素 x を挿入すべき位置を二分探索で O(log n) で特定できます。挿入位置が現在の size を超えた場合は、より長い増加部分列が見つかったことを意味し、size が更新されます。結果として、全体の計算量は O(n log n) に抑えられます。
Pythonでの実装例
class Solution(object): def solve(self, nums): tails = [0 for i in range(len(nums))] size = 0 for x in nums: i = 0 j = size while i != j: mid = i + (j - i) // 2 if tails[mid] < x: i = mid + 1 else: j = mid tails[i] = x size = max(i + 1, size) return sizeob = Solution()nums = [7, 2, 8, 3, 9, 4, 5, 6]print(ob.solve(nums))
実行結果
入力:
[7, 2, 8, 3, 9, 4, 5, 6]
出力:
5
この入力では、最長増加部分列は [2, 3, 4, 5, 6] となり、その長さは 5 です。
まとめ
二分探索を活用することで、従来の O(n²) の動的計画法よりも大幅に高速に最長増加部分列の長さを求められます。要素数が多いデータ(数万〜数十万件規模)を扱う場合に特に有効な手法なので、競技プログラミングや実務のデータ処理でもぜひ活用してください。
-
Pythonで最長のバランス括弧部分列の長さを求めるプログラム
問題概要 文字列 s が与えられます。この文字列には括弧「(」と「)」が含まれており、その中からバランスの取れた(対応関係が成立している)括弧の部分列として最も長いものを見つけ、その長さを返すことが目標です。 たとえば、入力が s = ())(()( の場合、出力は 4 になります。「(」と「)」を選び抜いて ()() というバランスの取れた部分列を作れるためです。 解法のアプローチ この問題は、文字列を後ろから走査することで線形時間で解けます。閉じ括弧を先に確保しておき、開き括弧が出てきたときに対を成立させるという発想です。手順は以下の通りです。 結果を格納する変数 res を 0 で初
-
Pythonで最長増加部分列(LIS)を二分探索で効率的に求める方法
ソートされていない整数のリストが与えられたとき、その中から「最長増加部分列(LIS:Longest Increasing Subsequence)」の長さを求める問題を考えてみましょう。 例えば、入力が [10, 9, 2, 5, 3, 7, 101, 18] の場合、増加する部分列としては [2, 3, 7, 101] が最長となるため、答えは 4 になります。 解法のアプローチ この問題は、単純な動的計画法でも O(n²) で解けますが、「tails(末尾管理用の配列)」と二分探索を組み合わせることで、O(n log n) という高速な計算量で解くことができます。 手順は以下の通りです。