摘要
上期介绍动态规划的什么,有什么特性,能解决什么类型的问题。这期就通过一个案例,看一下动态规划解决问题的过程。
动态规划解决问题,需要做好这三步,1 定义状态,2 梳理状态转换方程,3 确定状态初始状态,最后得出的结果值。
接下来通过求最多子序列和这个案例,来体会一下。
最多子序列和
给定一个长度为 n 的整数序列,求它的最大连续子序列和,比如 -2、1、-3、4、-1、2、1、-5、4 的最大连续子序列和是 4 + (-1) + 2 + 1 =6。
首先定义状态,假设 dp(i)
是以 nums[i]
结尾的最大子序列和(nums
是整个序列):
- 以
nums[0] = -2
结尾的最大子序列是 -2,所以dp(0)
= -2; - 以
nums[1] = 1
结尾的最大子序列是 1,所以dp(1)
= 1; - 以
nums[2] = -3
结尾的最大子序列是 1、-3,所以dp(2)
=dp(1)
-3 = -2; - 以
nums[3] = 4
结尾的最大子序列是 4,所以dp(3)
= 4; - 以
nums[4] = -1
结尾的最大子序列是 4、-1,所以dp(4)
=dp(3)
- 1 = 3; - 以
nums[5] = 2
结尾的最大子序列是 4、-1、2,所以dp(5)
=dp(4)
+ 2 = 5; - 以
nums[6] = 1
结尾的最大子序列是 4、-1、2、1,所以dp(6)
=dp(5)
+ 1 = 6; - 以
nums[7] = -5
结尾的最大子序列是 4、-1、2、1、-5,所以dp(7)
=dp(6)
- 5 = 1; - 以
nums[8] = 5
结尾的最大子序列是 4、-1、2、1、-5、4,所以dp(8)
=dp(7)
+ 4 = 5;
注意:
定义状态部分中“以 xx 结尾”,就是这个连续子序列必须以 xx 结尾。
然后梳理总结出状态转移方程,即总结出规律:
- 如果
dp(i-1)
≤ \leq ≤ 0,那么dp(i)
=nums[i]
; - 如果
dp(i-1)
> 0, 那么dp(i)
=dp(i-1)
+nums[i]
。
然后确定初始状态,dp(0) = nums[0]
。
最后,就可以得出最大子序列和是所以 dp(i)
中的最大值 max { dp(i) }, i
∈
\in
∈ [0, nums.length]。
实现代码
按照上面的梳理,实现代码如下:
static int maxSubArray1(int[] nums) {
if (nums == null || nums.length == 0) return 0;
int[] dp = new int[nums.length];
dp[0] = nums[0];
int max = dp[0];
for (int i = 1; i < nums.length; i++) {
int prev = dp[i-1];
if (prev <= 0) {
dp[i] = nums[i];
} else {
dp[i] = prev + nums[i];
}
max = Math.max(dp[i], max);
}
return max;
}
实现代码的空间复杂度和时间复杂度都是 O(n)。
看代码逻辑,每次 dp(i)
的处理只和 dp(i-1)
有关,那么就可以存在一个变量记录 dp(i-1)
的值,不需要一个 dp 数组去处理。优化代码如下:
static int maxSubArray(int[] nums) {
if (nums == null || nums.length == 0) return 0;
int dp = nums[0];
int max = dp;
for (int i = 1; i < nums.length; i++) {
int prev = dp;
if (prev <= 0) {
dp = nums[i];
} else {
dp = prev + nums[i];
}
max = Math.max(dp, max);
}
return max;
}
优化后的时间复杂度是 O(n),但是空间复杂度降为 O(1) 了。
总结
-
定义状态
-
梳理状态转换方程
-
确定状态初始状态
这就是动态规划的正确解决套路。