欢迎光临我的博客[http://poetize.cn],前端使用Vue2,聊天室使用Vue3,后台使用Spring Boot
简介
动态规划遵循一套固定的流程:递归的暴力解法( O(2^n) ) -> 带备忘录的递归解法( O(n) ) -> 非递归的动态规划解法( O(n) )。
「自顶向下」:
是从上向下延伸,
都是从一个规模较大的原问题比如说 f(20),向下逐渐分解规模,直到 f(1) 和 f(2) 触底,然后逐层返回答案。
「自底向上」:
直接从最底下,最简单,问题规模最小的 f(1) 和 f(2) 开始往上推,直到推到我们想要的答案 f(20),
动态规划解法
1. 将原问题分解为子问题
f(0),f(1),f(2)...f(n)
2. 确定状态
f(n)
3. 确定一些初始状态(边界条件)的值
f(n)=0
4. 确定状态转移方程(当前子问题值与前一个子问题值的关系)
f(n) 是一个状态 n,这个状态 n 是由状态 n - 1 和状态 n - 2 相加转移而来,这就是状态转移
动态规划问题最困难的就是写出状态转移方程。
适合使用动规求解的问题
1. 问题具有最优子结构(问题的最优解所包含的子问题的解也是最优的)
2. 求最优解问题
动态规划1:爬楼梯,求共多少爬法(n为正整数)
/**
* 方法1:递归(超时):太多冗余
*/
class Solution1 {
public static int climbStairs(int n) {
if(n==1 || n==2)return n;
return climbStairs(n-1)+climbStairs(n-2);
}
}
/**
* 方法2:记忆化递归递归
* 把每一步的结果存储在 memo 数组之中,每当函数再次被调用,我们就直接从 memo 数组返回结果。
*
* 时间复杂度:O(n),树形递归的大小可以达到 n。
* 空间复杂度:O(n),递归树的深度可以达到 n。
*/
class Solution2 {
public static int climbStairs(int n) {
int memo[] = new int[n + 1];
return climb_Stairs(0, n, memo);
}
public static int climb_Stairs(int i, int n, int memo[]) {
if (i > n) {
return 0;
}
if (i == n) {
return 1;
}
if (memo[i] > 0) {
return memo[i];
}
memo[i] = climb_Stairs(i + 1, n, memo) + climb_Stairs(i + 2, n, memo);
return memo[i];
}
}
class Solution2 {
public static int climbStairs(int n) {
int memo[] = new int[n + 1];
return climb_Stairs(n, memo);
}
public static int climb_Stairs(int n, int memo[]) {
if (n == 1 || n == 2) return memo[n] = n;
if (memo[n] > 0) {
return memo[n];
}
return memo[n] = climb_Stairs(n - 1, memo) + climb_Stairs(n - 2, memo);
}
}
/**
* 方法3:动态规划
*
* 思路(状态转移方程):
* 第n阶爬法的数量=第n-1阶爬法的数量+第n-2阶爬法的数量
*
* 时间复杂度:O(n)
* 空间复杂度:O(1)
*/
class Solution3 {
public static int climbStairs(int n) {
if(n==1 || n==2)return n;
int xxx =1;
int xx =2;
int x = 0;
int i=3;
while(i++<=n){
x=xxx+xx;
xxx=xx;
xx=x;
}
return x;
}
}
class aaa {
public static void main(String[] args) {
long l = System.currentTimeMillis();
System.out.println(Solution1.climbStairs(45));
System.out.println(System.currentTimeMillis()-l);
}
}
动态规划2:打家劫舍:求可以盗取的最大数(不能盗取相邻元素)
/**
* 思路(状态转移方程):
* n个元素最大可以盗取数值=
* n-2个元素最大可以盗取数值+第n个元素数值
* 与
* n-1个元素最大可以盗取数值
* 取最大值
*
* 时间复杂度:O(n)
* 空间复杂度:O(1)
*/
class Solution {
public int rob(int[] nums) {
if (nums.length == 0) return 0;
if (nums.length == 1) return nums[0];
if (nums.length == 2) return nums[0] > nums[1] ? nums[0] : nums[1];
int xxx = nums[0]; //前前一个最大
int xx = nums[0] > nums[1] ? nums[0] : nums[1]; //前一个最大
int x = nums[2]; //当前数
int sum = 0;
for (int i = 2; i < nums.length; i++) {
sum = xx > xxx + x ? xx : xxx + x;
if (i < nums.length - 1) {
xxx = xx;
xx = sum;
x = nums[i + 1];
}
}
return sum;
}
}
动态规划3:给定一个整数数组 nums ,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和
/**
* 思路:
* 前一个子数组和是正数,则说明有增益,与当前元素相加(前面的子序列对自己有力,保留前面,团结互助)
* 前一个数组是负数,则说明无增益,子数组从当前数组开始(前面的子序列对自己不利,丢弃前面,自立门户)
*
* 状态:dp[i]:表示以 nums[i] 结尾的连续子数组的最大和(如果数组长度为n,则有n种情况)
*
* 状态转移方程:dp[i]=max{nums[i],dp[i−1]+nums[i]}
*
* 时间复杂度:O(N)。
* 空间复杂度:O(1)。
*/
class Solution {
public int maxSubArray(int[] nums) {
int max= nums[0];
int sum= 0;
for (int i = 0; i <nums.length ; i++) {
if(sum<0)sum=nums[i];
else sum+=nums[i];
max=Math.max(max,sum);
}
return max;
}
}
动态规划4:最少的硬币个数。如果没有任何一种硬币组合能组成总金额,返回 -1
/**
* 思路:动态规划
*
* 动态转移方程:金额n的最少硬币数=(金额n-各种硬币面值)+1 中所需硬币数最小的元素
*
* 时间复杂度:O(Sn) S是金额,n是硬币面值数
* 空间复杂度:O(S)
*/
class Solution {
public int coinChange(int[] coins, int amount) {
int[] f = new int[amount + 1];
f[0] = 0;
for (int i = 1; i <= amount; i++) {
int cast = Integer.MAX_VALUE;
for (int j = 0; j < coins.length; j++) {
if (i - coins[j] >= 0) {
if (f[i - coins[j]] != Integer.MAX_VALUE) cast = Math.min(cast, f[i - coins[j]] + 1);
}
}
f[i] = cast;
}
return f[amount] == Integer.MAX_VALUE ? -1 : f[amount];
}
}
动态规划5:三角形最小路径和(给定一个三角形,找出最小路径和。每一步只能移动到下一行中相邻的结点上)
/**
* 方法1:记忆化递归法
*
* 思路:
* 当前元素的最小值=下一层元素两个元素的最小值+当前元素
*/
class Solution2 {
private Integer[][] arr = null;
public int minimumTotal(List<List<Integer>> triangle) {
arr = new Integer[triangle.size()][triangle.size()];
return helper(0, 0, triangle);
}
private int helper(int row, int col, List<List<Integer>> triangle) {
if (row == triangle.size() - 1) return arr[row][col] = triangle.get(row).get(col);
if (arr[row][col] != null) return arr[row][col];
int left = helper(row + 1, col, triangle);
int right = helper(row + 1, col + 1, triangle);
return arr[row][col] = Math.min(left, right) + triangle.get(row).get(col);
}
}
/**
* 思路(状态转移方程):自底向上
* 当前元素的最小值=下一层元素两个元素的最小值+当前元素
*/
class Solution1 {
public int minimumTotal(List<List<Integer>> triangle) {
int[] ints = new int[triangle.size() + 1];
for (int i = triangle.size() - 1; i <= 0; i--) {
for (int j = 0; j < triangle.get(i).size(); j++) {
ints[j] = Math.min(ints[j], ints[j + 1]) + triangle.get(i).get(j);
}
}
return ints[0];
}
}
动态规划6:给定一个无序的整数数组,找到其中最长上升子序列的长度
/**
* 示例:
* 输入: [10,9,2,5,3,7,101,18]
* 输出: 4
*
* 思路(状态转移方程):
* dp[i] 表示以 nums[i] 这个数结尾的最长递增子序列的长度。
*
* 设计动态规划算法,需要一个 dp 数组。
* 我们可以假设 dp[0...i-1]dp[0...i−1] 都已经被算出来了,然后通过这些结果算出 dp[i]的最大值。
*
* 时间复杂度 O(N^2)
* 空间复杂度 O(N)
*/
class Solution {
public int lengthOfLIS(int[] nums) {
if(nums.length<2)return nums.length;
int max=0;
int[] ints = new int[nums.length];
ints[0]=1;
for (int i = 1; i < nums.length; i++) {
ints[i]=1;
for (int j = 0; j <i ; j++) {
if(nums[j]<nums[i]){
ints[i]=Math.max(ints[i],ints[j]+1);
}
}
max=Math.max(max,ints[i]);
}
return max;
}
}
动态规划7:给定一个包含非负整数的网格,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小(只能向下或者向右移动一步)
/**
* 思路(状态转移方程):
* dp[i][j]=min(dp[i-1][j],dp[i][j-1])+dp[i][j]
*
* 时间复杂度 :O(mn)。遍历整个矩阵恰好一次。
* 空间复杂度 :O(mn)。额外的一个同大小矩阵。
*
* 注:在原数组上存储,这样就不需要额外的存储空间
*/
class Solution {
public static int minPathSum(int[][] grid) {
int row = grid.length;
int col = grid[0].length;
int[][] ints = new int[row][col];
ints[0][0] = grid[0][0];
for (int i = 1; i < row; i++) {
ints[i][0] = ints[i - 1][0] + grid[i][0];
}
for (int i = 1; i < col; i++) {
ints[0][i] = ints[0][i - 1] + grid[0][i];
}
for (int i = 1; i < row; i++) {
for (int j = 1; j < col; j++) {
ints[i][j] = Math.min(ints[i - 1][j], ints[i][j - 1]) + grid[i][j];
}
}
return ints[row - 1][col - 1];
}
}