蓝桥杯 DP 优化:从 O(n\^2) 到 O(n) 的滚动数组实战
问题背景
在动态规划中,当状态转移方程仅依赖前一层状态时,可通过滚动数组将空间复杂度从 O(n\^2) 优化到 O(n)。以下以经典问题 最长上升子序列(LIS) 为例演示优化过程。
原始解法:O(n\^2) 空间
状态定义 :
dp\[i\] 表示以第 i 个元素结尾的最长上升子序列长度
状态转移 :
dp\[i\] = \\max_{1 \\leq j \< i} \\{ dp\[j\] + 1 \\} \\quad (\\text{当} \\ a\[j\] \< a\[i\])
空间复杂度:O(n),但计算每个 dp\[i\] 需遍历 i-1 个状态,总时间复杂度 O(n\^2)
python
def lis_n2(a):
n = len(a)
dp = [1] * n # 初始化
for i in range(n):
for j in range(i):
if a[j] < a[i]:
dp[i] = max(dp[i], dp[j] + 1)
return max(dp)
滚动数组优化:O(n) 空间
核心思想 :
用一维数组动态维护 长度为 k 的上升子序列的最小末尾值,记为 tail\[k\]。
优化步骤:
- 初始化 tail = \[-\\infty\](哨兵值)
- 遍历每个元素 a\[i\]:
- 若 a\[i\] \> tail\[-1\]:直接扩展序列(tail.append(a\[i\]))
- 否则:二分查找第一个 \\geq a\[i\] 的位置 pos,更新 tail\[pos\] = a\[i\]
- 结果:len(tail) - 1(扣除哨兵)
时间复杂度 :O(n \\log n)
空间复杂度:O(n)
python
def lis_n(a):
tail = [float('-inf')] # 哨兵
for x in a:
if x > tail[-1]:
tail.append(x)
else:
# 二分查找插入位置
l, r = 1, len(tail)-1
while l <= r:
mid = (l + r) // 2
if tail[mid] < x:
l = mid + 1
else:
r = mid - 1
tail[l] = x # 更新该长度对应的最小末尾值
return len(tail) - 1 # 扣除哨兵
关键分析
-
tail 数组单调性
tail\[k\] \< tail\[k+1\] \\quad (\\forall k \\geq 1)
该性质保证二分查找的正确性。
-
状态转移优化
原始 O(n\^2) 解法中,每个 dp\[i\] 需遍历前 i 个状态;优化后通过维护 tail 数组,将状态转移降为 O(\\log n)。
-
滚动本质
tail 数组动态更新:
- 当追加元素时,数组长度增加(状态升维)
- 当更新元素时,覆盖旧值(滚动覆盖)
适用场景
滚动数组优化适用于满足以下条件的 DP 问题:
- 状态转移仅依赖 前一层状态
- 状态维度存在 单调性(如 tail 数组递增)
- 可通过 覆盖更新 丢弃历史数据
典型应用:背包问题、序列比对、路径计数等蓝桥杯常考题型。
通过这种优化,在 n=10\^6 规模下,空间从 4GB 降至 8MB,时间从 O(10\^{12}) 降至 O(10\^6 \\log 10\^6),实现高效求解。