問題
長さ $N$ の数列 $A_1, A_2, \ldots, A_N$ と $Q$ 個のクエリが与えられる。各クエリ $(l_i, r_i)$ に対して、$A[l_i..r_i]$ の最長増加部分列(LIS)の長さを求めよ。
部分列はインデックスが単調増加かつ値が狭義単調増加のものとする。
制約
$1 \le N \le 3000$
$1 \le Q \le 3000$
$1 \le A_i \le 10^9$
$1 \le l_i \le r_i \le N$
時間制限: 5秒
入出力例
入力例 1
6 3
3 1 4 1 5 9
1 6
1 3
3 6
出力例 1
4
2
3
全体LIS: $[1,4,5,9]$ 長さ4。$A[1..3]=[3,1,4]$ LIS: $[1,4]$ 長さ2。$A[3..6]=[4,1,5,9]$ LIS: $[4,5,9]$ 長さ3
概念図: 左端固定による patience sorting の拡張
ヒント(段階的開示)
ヒント1: 方向性
各クエリを独立に $O(N \log N)$ で解くと $O(QN \log N)$ となります。$N=Q=3000$ では約 $10^8$ でギリギリです。より効率的な方法として、左端 $l$ を固定して右端 $r$ を拡張するテーブルを事前計算します。
ヒント2: patience sorting の増分更新
左端 $l$ を固定し、$r$ を $l$ から $N-1$ まで増やすとき、$A[r]$ を tails 配列に挿入するだけで $\text{lis\_table}[l][r]$ が更新されます。$O(N \log N)$ で全 $r$ の値を計算できます。
ヒント3: テーブル構築の骨格
from bisect import bisect_left
lis_table = [[0] * N for _ in range(N)]
for l in range(N):
tails = []
for r in range(l, N):
val = A[r]
pos = bisect_left(tails, val)
if pos == len(tails):
tails.append(val)
else:
tails[pos] = val
lis_table[l][r] = len(tails)
# クエリ (l, r) の回答: lis_table[l-1][r-1]
模範解答 (Python)
import sys
from bisect import bisect_left
def solve():
data = sys.stdin.buffer.read().split()
idx = 0
N, Q = int(data[idx]), int(data[idx+1]); idx += 2
A = [int(data[idx+i]) for i in range(N)]; idx += N
# Precompute lis_table[l][r] = LIS of A[l..r] (0-indexed)
# O(N^2 log N) time, O(N^2) space
lis_table = [[0] * N for _ in range(N)]
for l in range(N):
tails = []
for r in range(l, N):
val = A[r]
pos = bisect_left(tails, val)
if pos == len(tails):
tails.append(val)
else:
tails[pos] = val
lis_table[l][r] = len(tails)
out = []
for _ in range(Q):
l, r = int(data[idx]) - 1, int(data[idx+1]) - 1; idx += 2
out.append(lis_table[l][r])
sys.stdout.write('\n'.join(map(str, out)) + '\n')
solve()
Step-by-Step 解説
1patience sorting によるLIS
配列 $A$ の LIS 長さを求めるには patience sorting(二分探索 + tails 配列)が使えます。$A[r]$ を tails の適切な位置に挿入(または追加)し、tails の長さが LIS の長さになります。
配列 $A$ の LIS 長さを求めるには patience sorting(二分探索 + tails 配列)が使えます。$A[r]$ を tails の適切な位置に挿入(または追加)し、tails の長さが LIS の長さになります。
2左端固定・右端拡張
左端 $l$ を固定して右端 $r$ を増やすとき、新しい要素 $A[r]$ を tails に追加するだけで $\text{lis\_table}[l][r]$ が得られます。$l$ を一つ変えるたびに tails を初期化し直します。
左端 $l$ を固定して右端 $r$ を増やすとき、新しい要素 $A[r]$ を tails に追加するだけで $\text{lis\_table}[l][r]$ が得られます。$l$ を一つ変えるたびに tails を初期化し直します。
3テーブルのメモリ
$N=3000$ では $9 \times 10^6$ 要素のテーブルが必要です。Python の `int` は大きいため、`array.array('H', ...)` や numpy を使うとメモリを節約できます。
$N=3000$ では $9 \times 10^6$ 要素のテーブルが必要です。Python の `int` は大きいため、`array.array('H', ...)` や numpy を使うとメモリを節約できます。
4クエリへの回答
前計算後は各クエリを $O(1)$ で回答できます。1-indexed から 0-indexed に変換して `lis_table[l-1][r-1]` を返します。
前計算後は各クエリを $O(1)$ で回答できます。1-indexed から 0-indexed に変換して `lis_table[l-1][r-1]` を返します。
5計算量
前計算: $O(N^2 \log N)$。クエリ: $O(Q)$。$N=3000$ では約 $3000^2 \times 12 \approx 10^8$ で制限内。
前計算: $O(N^2 \log N)$。クエリ: $O(Q)$。$N=3000$ では約 $3000^2 \times 12 \approx 10^8$ で制限内。
計算量
前計算: $O(N^2 \log N)$
各クエリ: $O(1)$
全体: $O(N^2 \log N + Q)$
空間: $O(N^2)$(テーブル)
各クエリ: $O(1)$
全体: $O(N^2 \log N + Q)$
空間: $O(N^2)$(テーブル)
よくあるミス
| ミス | 原因 | 正しい書き方 |
|---|---|---|
| tails をループ間でリセットしない | l が変わる際に前の tails が残る | l ループの先頭で tails = [] |
| 狭義 vs 広義 LIS | bisect_left vs bisect_right | 狭義増加なら bisect_left |
| 1-indexed と 0-indexed の混同 | クエリが 1-indexed | l-1, r-1 に変換 |
| メモリ不足 | N=3000 で N^2 テーブル | array.array('H', ...) で節約 |
次のステップ
- 発展問題: 3次元 LIS(インデックス・値・色の3条件)を CDQ で $O(N \log^2 N)$
- 関連: Day034 Q2(3D LIS + CDQ)の復習
- 応用: 区間 LIS を使った問題(区間マージ最長共通部分列等)