动态规划

简介

动态规划遵循一套固定的流程:递归的暴力解法( 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];
    }
}
原文地址:https://www.cnblogs.com/loveer/p/11786100.html