动态规划中的 求“最长”、“最大收益”、“最多区间”、“最优策略” 双重 for + 状态转移
以最长递增子序列为例
🎯 首先明确目标
以最长上升子序列(LIS)为例,假设输入是:
nums := []int{10, 9, 2, 5, 3, 7, 101, 18}
我们定义:
dp[i]:以 nums[i] 为结尾的最长上升子序列长度
目标:求 dp[i]
的最大值。
🔍 双重 for
的意义是什么?
for i := 1; i < n; i++ {for j := 0; j < i; j++ {if nums[j] < nums[i] {dp[i] = max(dp[i], dp[j] + 1)}}
}
这里我们来解释:
✅ 第一层循环:从左到右遍历每一个位置 i
我们要计算:以 nums[i]
结尾的“最长上升子序列”是多少?
✅ 第二层循环:查看所有在 i 之前的数 j
对于每一个前面的数 nums[j]
,判断:
它是否可以作为
nums[i]
的前一个元素?
也就是判断nums[j] < nums[i]
。
如果是,就说明:
如果我以
j
为结尾的子序列有长度dp[j]
,
那么我在它后面加上nums[i]
,就可以构成一个更长的上升序列,长度是dp[j] + 1
。
于是我们尝试更新 dp[i]
:
dp[i] = max(dp[i], dp[j] + 1)
🔁 用人话解释一遍:
“我想知道
nums[i]
能不能接在某个前面的上升序列后面,
如果能,那我就更新一下它作为结尾时,能组成多长的子序列。”
🧠 一个具体例子(带注释)
nums := []int{10, 9, 2, 5, 3, 7}初始化 dp := []int{1, 1, 1, 1, 1, 1} // 每个位置起码是自己i = 3, nums[3] = 5j = 0, nums[0] = 10 -> 跳过j = 1, nums[1] = 9 -> 跳过j = 2, nums[2] = 2 -> 2 < 5 -> 更新 dp[3] = max(1, dp[2] + 1) = 2i = 4, nums[4] = 3j = 0 -> 10 > 3 -> 跳过j = 1 -> 9 > 3 -> 跳过j = 2 -> 2 < 3 -> dp[4] = max(1, dp[2]+1) = 2j = 3 -> 5 > 3 -> 跳过i = 5, nums[5] = 7j = 0 -> 10 > 7 -> 跳过j = 1 -> 9 > 7 -> 跳过j = 2 -> 2 < 7 -> dp[5] = max(1, 1+1) = 2j = 3 -> 5 < 7 -> dp[5] = max(2, 2+1) = 3j = 4 -> 3 < 7 -> dp[5] = max(3, 2+1) = 3
🧩 为什么不能只用一个循环?
因为我们求的是:
“在前面所有满足条件的数里,找一个最优的情况来更新当前的状态”。
只有看完所有的 j < i
才能找到最优的更新路径,所以必须有一个内层循环来“扫描过去”。
✅ 总结记忆方法:
-
i
是当前状态 -
j
是过去状态 -
if 条件成立
,说明可以从过去j
走到现在i
-
dp[i] = max(dp[i], dp[j]+1)
就是取过去所有能转移过来的路径中最优的那一条
for i := 1; i < n; i++ {for j := 0; j < i; j++ {// 某种条件成立// dp[i] = max(dp[i], dp[j] + ...)}
}
这类“双重 for + 状态转移”的写法,在一类特定的动态规划问题中非常经典和高频。这类问题一般具有以下特征:
✅ 典型问题特征
-
子问题具有前后关系(i/j 结构)
-
当前状态
i
要依赖过去某些状态j < i
-
类似“前缀分析”
-
-
具有单调性约束
-
如
nums[j] < nums[i]
、end[j] <= start[i]
等条件
-
-
求解最大/最小值
-
求“最长”、“最大收益”、“最多区间”、“最优策略”
-
✅ 高频问题示例
问题类型 | 描述 | dp含义 | 转移条件 |
---|---|---|---|
最长上升子序列 (LIS) | 找递增的最大长度 | dp[i] :以 i 结尾的最长长度 | nums[j] < nums[i] |
最长不下降子序列 | 可以等于也递增 | dp[i] | nums[j] <= nums[i] |
最长摆动子序列 | 上下起伏交替 | up[i] , down[i] | 比较大小后转移 |
最大不重叠区间数 | 按 end 排序后贪心/DP | dp[i] :前 i 个的最大区间数 | end[j] <= start[i] |
最大信封嵌套数(俄罗斯套娃) | 求最多可嵌套信封数 | 对宽升高做 LIS | w[j] < w[i] && h[j] < h[i] |
打家劫舍变形 | 不偷相邻的 | dp[i] :前 i 个最大金额 | dp[i] = max(dp[i-1], dp[i-2]+nums[i]) |
最大连续子数组积 | 需要 max 和 min | max[i], min[i] 同时维护 | 根据乘积正负转移 |
✅ 模板结构(可抽象成函数)
for i := 1; i < n; i++ {for j := 0; j < i; j++ {if 满足条件(j, i) {dp[i] = max(dp[i], dp[j] + 某个值)}}
}
✅ 小技巧:可以加上前向指针以恢复路径
prev := make([]int, n) // 记录转移路径
for i := range prev {prev[i] = -1
}
...
if dp[j] + 1 > dp[i] {dp[i] = dp[j] + 1prev[i] = j
}
如果你以后看到类似“最XXX的子序列”“最XXX的不重叠区间”,只要是“i依赖j”型的问题,几乎都可以优先尝试这个模板。