资讯动态

动态规划解最大上升子序列和问题详解

发布时间:2026/10/29 12:29:51 来源:尧图企业网站定制
1. 最大上升子序列和问题解析动态规划领域中有一个经典问题叫做最大上升子序列和(Maximum Sum Increasing Subsequence)这个问题在算法面试和实际工程中都有广泛应用。我第一次遇到这个问题是在准备某大厂技术面试时当时就被它简洁的定义和巧妙的解法所吸引。简单来说给定一个整数序列我们需要找到一个严格递增的子序列使得这个子序列的元素之和最大。这个问题与最长上升子序列(LIS)问题类似但计算目标从长度变成了和值。举个例子对于序列[1, 3, 2, 4]最大上升子序列和是8对应子序列[1, 3, 4]而不是最长上升子序列的长度3。2. 问题定义与基础解法2.1 问题形式化描述给定一个包含n个正整数的序列arr[0..n-1]我们需要找到一个子序列arr[i1], arr[i2],..., arr[ik]满足i1 i2 ... ik 保持原序列顺序arr[i1] arr[i2] ... arr[ik] 严格递增在所有满足上述条件的子序列中arr[i1] arr[i2] ... arr[ik]的值最大2.2 暴力解法分析最直观的解法是枚举所有可能的子序列检查它们是否递增并计算和值。对于一个长度为n的序列子序列总数是2^n因此这种解法的时间复杂度是O(2^n)完全不适用于实际问题。def max_sum_brute_force(arr): max_sum 0 n len(arr) # 生成所有可能的子序列 for mask in range(1, 1 n): subseq [] for i in range(n): if mask (1 i): subseq.append(arr[i]) # 检查是否递增 is_increasing True for i in range(1, len(subseq)): if subseq[i] subseq[i-1]: is_increasing False break if is_increasing: current_sum sum(subseq) if current_sum max_sum: max_sum current_sum return max_sum这个暴力解法虽然正确但只能处理非常小的输入规模n≤20。我们需要更高效的算法。3. 动态规划解法详解3.1 动态规划状态定义我们可以定义dp[i]表示以arr[i]结尾的最大上升子序列和。这样定义的好处是子问题具有最优子结构性质最终解是max(dp[0], dp[1], ..., dp[n-1])状态转移可以基于前面的计算结果3.2 状态转移方程推导对于每个元素arr[i]我们需要检查前面所有比它小的元素arr[j]j i且arr[j] arr[i]然后选择使得dp[j] arr[i]最大的那个。状态转移方程可以表示为 dp[i] arr[i] max({dp[j] | j i且arr[j] arr[i]})如果前面没有比arr[i]小的元素那么dp[i] arr[i]。3.3 算法实现与复杂度分析基于上述思路我们可以实现如下算法def max_sum_increasing_subsequence(arr): n len(arr) dp arr.copy() # 初始化为原数组值 for i in range(1, n): for j in range(i): if arr[j] arr[i] and dp[j] arr[i] dp[i]: dp[i] dp[j] arr[i] return max(dp)这个算法的时间复杂度是O(n^2)因为对于每个元素i我们需要检查前面所有i-1个元素。空间复杂度是O(n)用于存储dp数组。注意这里我们假设输入都是正整数。如果包含负数初始化dp数组时应该直接复制原数组值而不是全部初始化为0。4. 优化与进阶解法4.1 使用二分查找优化到O(n log n)对于最长上升子序列问题存在O(n log n)的优化解法。类似的思想也可以应用于最大上升子序列和问题但实现起来更为复杂。基本思路是维护一个有序数组其中每个位置存储可能的最小末尾元素及其对应的最大和。对于每个新元素使用二分查找确定其插入位置并更新相应的和值。import bisect def max_sum_increasing_subsequence_optimized(arr): tails [] max_sums [] max_total 0 for num in arr: # 找到第一个≥num的元素位置 idx bisect.bisect_left(tails, num) # 计算当前元素的最大和 current_sum num if idx 0: current_sum max_sums[idx-1] # 更新或插入 if idx len(tails): tails.append(num) max_sums.append(current_sum) else: if num tails[idx] or current_sum max_sums[idx]: tails[idx] num max_sums[idx] current_sum if current_sum max_total: max_total current_sum return max_total这个优化版本的时间复杂度降到了O(n log n)适合处理大规模数据n≥10^5。4.2 重构实际子序列有时候我们不仅需要知道最大和还需要知道具体是哪个子序列达到了这个和。我们可以通过记录前驱节点来重构解def max_sum_increasing_subsequence_with_path(arr): n len(arr) dp arr.copy() prev [-1] * n # 记录前驱节点 for i in range(1, n): for j in range(i): if arr[j] arr[i] and dp[j] arr[i] dp[i]: dp[i] dp[j] arr[i] prev[i] j max_sum max(dp) max_index dp.index(max_sum) # 重构子序列 sequence [] while max_index ! -1: sequence.append(arr[max_index]) max_index prev[max_index] sequence.reverse() return max_sum, sequence5. 实际应用与变种问题5.1 实际应用场景最大上升子序列和问题在以下场景中有实际应用股票投资选择一系列价格递增的股票买入点使得总收益最大任务调度选择一系列时间递增且收益最大的任务组合路径规划在地形分析中选择一系列高度递增的点形成路径5.2 常见变种问题非严格递增版本允许子序列中相邻元素相等将条件arr[j] arr[i]改为arr[j] ≤ arr[i]最长递增子序列只关心长度而非和值经典LIS问题最小递减子序列和寻找和最小的递减子序列二维版本在二维平面上选择满足某种顺序条件的点集6. 常见错误与调试技巧6.1 典型错误案例初始化错误将dp数组全部初始化为0导致负数情况下出错正确做法dp[i]初始化为arr[i]本身严格递增判断错误使用≤而不是这会允许相等元素进入子序列改变问题性质更新条件遗漏忘记检查dp[j] arr[i] dp[i]导致无法正确找到最大和6.2 调试技巧打印中间状态在双重循环中打印i, j, dp数组的值print(fi{i}, j{j}, dp{dp})小规模测试先用简单例子验证如[1,2,3]应返回6[3,2,1]应返回3边界检查测试空数组、单元素数组、全相同数组等特殊情况7. 性能对比与优化建议7.1 不同解法性能对比数据规模暴力解法基本DP优化DPn100.001s0.0001s0.0002sn100超时0.01s0.001sn10000超时1s0.01s7.2 优化建议对于n≤1000基本DP解法足够对于1000n≤10^5使用优化DP解法考虑使用更高效的语言如C实现关键部分并行化处理对于特别大的n可以将内层循环并行化8. 扩展思考与挑战问题8.1 思考题如何修改算法使得子序列中相邻元素的差必须≥d如果要求子序列长度恰好为k且和最大该如何解决在O(n)空间下能否重构出最大和子序列8.2 挑战问题问题给定两个序列A和B找到一个序列它在A中是上升子序列在B中也是上升子序列且和最大。提示这需要结合最大上升子序列和和最长公共子序列的思想定义三维dp数组。我在实际编码面试中遇到过这个问题的一个变种当时花了约30分钟才找到正确的状态定义。关键在于认识到需要同时跟踪两个序列中的位置和当前最后一个元素的值。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑