动态规划解法(O(n²)时间复杂度)
虽然动态规划不是本题的最优解(n=100000时O(n²)会超时),但理解其思路对学习算法非常重要。以下是动态规划解决最长严格上升子序列问题的详细方法:
算法思路:
- 状态定义:定义
dp[i]
表示以第i
个元素结尾的最长严格上升子序列的长度 - 状态转移方程:
- 对于每个位置
i
,检查所有j < i
- 如果
arr[j] < arr[i]
,则dp[i] = max(dp[i], dp[j] + 1)
- 对于每个位置
- 初始化:每个位置的初始值为1(因为单独一个元素就是长度为1的子序列)
- 结果获取:遍历
dp
数组找出最大值
代码实现:
def main():
n = int(input())
arr = list(map(int, input().split()))
# 初始化dp数组,每个元素至少可以形成长度为1的子序列
dp = [1] * n
# 记录最终结果
ans = 1
# 动态规划填表
for i in range(1, n):
for j in range(i):
# 如果前面的元素小于当前元素,则更新dp[i]
if arr[j] < arr[i]:
dp[i] = max(dp[i], dp[j] + 1)
# 更新全局最大值
ans = max(ans, dp[i])
print(ans)
if __name__ == "__main__":
main()
算法分析:
- 时间复杂度:O(n²) - 嵌套循环遍历所有元素对
- 空间复杂度:O(n) - 需要额外dp数组存储状态
- 优点:思路直观,易于理解
- 缺点:当n=100000时,计算量达到10¹⁰,远超过现代计算机的处理能力(一般1秒处理10⁷次操作)
贪心+二分(O(n log n)时间复杂度)
为了解决这个问题,我们需要找到给定数组中最长的严格上升子序列(LIS)的长度。由于数组长度可能达到100,000,传统的O(n²)动态规划方***超时,因此我们采用O(n log n)的高效算法。
方法思路
- 问题分析:最长严格上升子序列问题要求在不改变元素顺序的情况下,找出一个子序列,其中每个元素严格大于前一个元素,并且这个子序列的长度最大。
- 关键观察:维护一个动态数组
dp
,其中dp[i]
表示长度为i+1
的上升子序列的最小末尾元素。这个数组始终保持严格递增。 - 算法选择:使用贪心结合二分查找的方法:
- 遍历数组中的每个元素。
- 对于每个元素,使用二分查找在
dp
数组中找到第一个大于或等于当前元素的位置。 - 如果该位置在
dp
数组末尾,则将当前元素添加到dp
数组中(扩展了最长上升子序列)。 - 否则,用当前元素替换
dp
数组中该位置的元素(保证后续能形成更长的子序列)。
- 复杂度分析:遍历数组需要O(n)时间,每个元素的二分查找需要O(log n)时间,因此总时间复杂度为O(n log n)。空间复杂度为O(k),其中k是最长上升子序列的长度。
解决代码
import bisect
def main():
n = int(input().strip())
arr = list(map(int, input().split()))
dp = []
for num in arr:
idx = bisect.bisect_left(dp, num)
if idx == len(dp):
dp.append(num)
else:
dp[idx] = num
print(len(dp))
if __name__ == "__main__":
main()
代码解释
- 输入处理:读取数组长度
n
和数组arr
。 - 初始化:创建一个空数组
dp
,用于存储不同长度上升子序列的最小末尾元素。 - 遍历数组:对于数组中的每个元素
num
:- 使用
bisect_left
在dp
数组中查找第一个大于或等于num
的位置idx
。 - 如果
idx
等于dp
的长度,说明num
大于dp
中所有元素,将其添加到dp
末尾(扩展了最长上升子序列)。 - 否则,用
num
替换dp[idx]
(保证后续能形成更长的子序列)。
- 使用
- 输出结果:最终
dp
数组的长度即为最长严格上升子序列的长度。
此算法高效地利用了二分查找来维护最小末尾元素数组,确保了在处理大规模数据时的时间效率。