动态规划和分治类似,但他们有异同点:
使用动态规划都可以使用递归(分治)来完成,在递归的基础上加上记忆化即可去除重复的计算,推导出最优解的方程(当前解和之前已经算出来的解之间的关系)来得出结果。
计算斐波那契数列的和: 0,1,2,3,5,8…
递归算法
时间复杂度 O(2n),空间复杂度O(1)
private int fib(int n) {
return (n <= 1) ? n : fib(n - 1) + fib(n - 2);
}
动态规划
时间复杂度O(n),空间复杂度O(n)
private int dpFib(int n) {
int[] memo = new int[n];
memo[0] = 0; memo[1] = 1;
for (int i = 2; i < n; i++) {
// 从小到大递推,如果从大到小,又是一堆重复计算
memo[i] = memo[i - 1] + memo[i - 2];
}
return memo[n-1];
}
给定一个棋盘,从最左上角到最右下角有多少条路径,棋盘中有些格子中有石头无法通过。
递归解法
private int paths(boolean[][] isStone, int m, int n) {
if (isStone[m][n]) return 0;
if (m == 0 || n == 0) return 1;
return paths(isStone, m - 1, n) + paths(isStone, m, n - 1);
}
动态规划
private int dpPaths(boolean[][] isStone, int m, int n) {
int[][] opt = new int[m + 1][n + 1];
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
// opt[i,j] = opt[i-1,j] + opt[i,j-1]
if (i == 0 || j == 0) {
opt[i][j] = 1;
continue;
}
if (isStone[i][j]) {
//碰到石头
opt[i][j] = 0;
} else {
//可以通过
opt[i][j] = opt[i - 1][j] + opt[i][j - 1];
}
}
}
return opt[m][n];
}
假设需要n阶能够爬到顶楼,每一次只能爬1阶或者2阶,求问,有多少种不同的方法爬上楼顶?
分析:由于每次只能爬1阶或者2阶,所以,从站在楼顶那一刻倒推,倒推1阶,假设有f(n-1)种办法,倒推2阶,假设有f(n-2)种办法,则,f(n) = f(n-1) + f(n-2),这是一个标准的斐波那契数列的和解法。见 2.3.1 斐波那契数列
给定一个三角形,找出自顶向下的最小路径和,每一步只能移动到下一行的相邻节点上(角标:(i+1,j+1);(i+1,j))
例如:
[
[2],
[3,4],
[6,5,7],
[4,1,8,3]
]
得到的结果为:2+3+5+1 = 11
解法1:递归
当前节点值加上下面两个分支的最小值,加起来总和为最小路径和。
该解法时间复杂度:O(2n),空间复杂度为O(1)
private int getResult(int[][] num) {
return dfsSum(num, 0, 0);
}
private int dfsSum(int[][] num, int m, int n) {
if (m >= num.length || n >= num[0].length) return 0;
int min = Math.min(dfsSum(num, m + 1, n), dfsSum(num, m + 1, n + 1));
return num[m][n] + min;
}
解法2:动态规划
最后一行分别赋值给初始最小路径节点值,然后根据公式,sum[i][j] = Math.min(sum[i+1][j], sum[i+1][j+1]) + num[i][j];
层层倒推,得到sum[0][0]即为当前的解。
private int dpSum(int[][] num) {
int row = num.length;
int col = num[num.length - 1].length;
int[][] sum = new int[row][col];
for (int i = row-1; i >=0; i--) {
for (int j = 0; j < num[i].length; j++) {
if (i == row - 1) {
// 最后一行
sum[i][j] = num[i][j];
continue;
}
// 下一行相邻两个路径的最小值加上当前节点值
sum[i][j] = Math.min(sum[i+1][j], sum[i+1][j+1]) + num[i][j];
}
}
return sum[0][0];
}
上面使用的是二维数组来存放中间计算的值,空间复杂度为O(n2),下面优化为O(n)
private int dpSum2(List> triangle) {
int row = triangle.size();
// 初始化一个一维数组,用来辅助存储单次循环内的路径和
int[] dp = new int[row];
for (int i = 0; i < row; i++) {
// 将最后一行的值赋值为初始值
dp[i] = triangle.get(row - 1).get(i);
}
for (int i = row - 2; i >= 0; i--) {
for (int j = 0; j <= i; j++) {
// 将路径和更新
dp[j] = triangle.get(i).get(j) + Math.min(dp[i], dp[i + 1]);
}
}
return dp[0];
}
给定一个数组nums,找出一个序列中乘积最大的连续子序列(该序列至少包含一位数)
示例:
输入:[2,3,-2,4]
输出:6
解释:子数组[2,3]有最大乘积
解题思路:
动态规划两个核心步骤:
定义状态:使用二维数组存储子乘积的最大和最小值
定义状态方程:如果当前数大于0:取最大数*当前数,得最大子乘积;如果当前数小于0,则取最小数,乘以当前数,得最小数。
private int maxProduct(int[] nums) {
//第二列为了表示是最大还是最小值,第一行为最大值,第二行为最小值
int[][] dp = new int[2][2];
//初始化最大最小值
dp[0][0] = dp[0][1] = nums[0];
int max = 0;
for (int i = 1; i < nums.length; i++) {
int x = i % 2;
int y = (i - 1) % 2;
// 最大值
dp[x][0] = Math.max(dp[y][0] * nums[i], dp[y][1] * nums[i]);
// 最小值
dp[x][1] = Math.min(dp[y][0] * nums[i], dp[y][1] * nums[i]);
max = Math.max(max, dp[x][0]);
}
return max;
}
也可以只定义两个变量代替数组:
private int maxProduct2(int[] nums) {
int icMax = nums[0];
int icMin = nums[0];
for (int i = 1; i < nums.length; i++) {
icMax = max(icMax * nums[i], icMax * nums[i], icMax);
icMin = min(icMax * nums[i], icMax * nums[i], icMin);
}
return icMax;
}
给定一个无序的整数数组,找到其中最长上升子序列的长度。
示例:
输入:[10,9,2,5,3,7,101,18]
输出:4
解释:最长子序列:[2,3,7,101]
解法1: 递归
从头开始递归,如果当前节点比上一个节点值大,那么就 + 1,否则继续递归,直到遍历完成为止。
该解法时间复杂度:O(2n),空间复杂度O(1)
private int lengthOfLIS(int[] nums) {
return lengthofLIS(nums, Integer.MIN_VALUE, 0);
}
private int lengthofLIS(int[] nums, int pre, int cur) {
if (cur == nums.length) return 0;
int taken = 0;
if (pre < nums[cur]) {
//当前节点是有效的上升子序列的一个
taken = 1 + lengthofLIS(nums, nums[cur], cur + 1);
}
// 当前节点不是上升子序列中的节点
int noTaken = lengthofLIS(nums, pre, cur + 1);
return Math.max(taken, noTaken);
}
解法2: 动态规划
动态规划核心步骤:
定义状态:使用一个一位数组来存放当前升序的最大子序列长度
定义状态方程:如果当前值和之前的所有元素遍历对比,当前值较大,且该子序列是前面所有子序列长度的最大值,那么+1就是当前的最大子序列的长度。
private int lengthOfLIS(int[] nums) {
if (nums.length == 0) return 0;
int[] dp = new int[nums.length];
dp[0] = 1;
int maxas = 1;
for (int i = 1; i < nums.length; i++) {
int maxval = 0;
for (int j = 0; j < i; j ++) {
if (nums[i] > nums[j]) {
maxval = Math.max(maxval, dp[j]);
}
}
dp[i] = maxval + 1;
maxas = Math.max(maxas, dp[i]);
}
return maxas;
}