总结动态规划算法

一、前言

1.1、引子

计算机归根结底只会做一件事:穷举。所有的算法都是在让计算机如何聪明地穷举而已,动态规划也是如此。
还是要从一个经典的例子说起——斐波那契数列(详细可见我的另一篇文章 斐波那契数列递归算法优化),我们似乎可以发现动态规划遵循一套固定的流程:递归的暴力解法 -> 带备忘录的递归解法 -> 非递归的动态规划解法。所以从某种程度上来说,所有的动态规划本质都是优化后的暴力求解。


1.2、介绍

DP(动态规划,dynamic programming)
要解决的就是多阶段决策问题;
将待求解的问题分解为若干个子问题(阶段),按顺序求解子阶段,前一子问题的解,为后一子问题的求解提供了有用的信息。在求解任一子问题时,列出各种可能的局部解,通过决策保留那些有可能达到最优的局部解,丢弃其他局部解。依次解决各子问题,最后一个子问题就是初始问题的解。
动态规划解决的问题多数有重叠子问题这个特点,为减少重复计算,动态规划法试图仅仅解决每个子问题一次,从而减少计算量:一旦某个给定子问题的解已经算出,则将其记忆化存储,以便下次需要同一个子问题解之时直接查表。


三种算法区别:

贪心算法(Greed alalgorithm)
是一种在每一步选择中都采取在当前状态下最好或最优(即最有利)的选择,从而希望导致全局结果是最好或最优的算法。但是贪心是一种只考虑眼前情况的策略,一些情况下会导致“鼠目寸光”的问题。

分治算法(Divide and conquer alalgorithm)
字面上的解释是“分而治之”,就是把一个复杂的问题分成两个或更多的相同或相似的子问题,直到最后子问题可以简单的直接求解,原问题的解即子问题的解的合并。

动态规划算法与分治法最大的差别是:适合于用动态规划法求解的问题,经分解后得到的子问题往往不是互相独立的(即下一个子阶段的求解是建立在上一个子阶段的解的基础上,进行进一步的求解)。


二、正文

动态规划算法的核心是记住已经求过的解,记住求解的方式有两种:①自顶向下的备忘录法 ②自底向上。

适用场景:

  1. 满足最优化原理:如果问题的最优解所包含的子问题的解也是最优的,就称该问题具有最优子结构。
  2. 满足无后效性:某阶段状态一旦确定,就不受这个状态以后决策的影响。即某状态以后的过程不会影响以前的状态,只与当前状态有关。
  3. 满足有重叠子问题:即子问题之间是不独立的,一个子问题在下一阶段决策中可能被多次使用到。(该性质并不是动态规划适用的必要条件,但是如果没有这条性质,动态规划算法同其他算法相比就不具备优势)
2.1、方法总结

解决动态规划类问题,分为两步:

  1. 确定状态;
  2. 根据状态列状态转移方程(即从前一个阶段转化到后一个阶段之间的递推关系)

确定该状态上可以执行的操作,然后是该状态和前一个状态或者前多个状态有什么关联,通常该状态下可执行的操作必定是关联到我们之前的几个状态。

2.2、实战分析

实战目录:

  1. 找零钱问题
  2. 数字三角形
  3. 背包问题
  4. 公共子序列,公共子串问题
2.2.1. 找零钱问题

问题:找零所需的最少硬币数

递归:

	public static int coinChange(int[] coins, int amount) {
        return coinChange(0, coins, amount);
    }

    private static int coinChange(int idxCoin, int[] coins, int amount) {
        if (amount == 0)
            return 0;
        if (idxCoin < coins.length && amount > 0) {
        	// 当用最小币值的硬币找零时,所需硬币数量最多
            int maxVal = amount / coins[idxCoin];
            int minCost = Integer.MAX_VALUE;
            for (int x = 0; x <= maxVal; x++) {
                if (amount >= x * coins[idxCoin]) {
                    int res = coinChange(idxCoin + 1, coins, amount - x * coins[idxCoin]);
                    if (res != -1) minCost = Math.min(minCost, res + x);
                }
            }
            return (minCost == Integer.MAX_VALUE)? -1: minCost;
        }
        return -1;
    }

动态规划:

  1. 首先思考如何设计dp矩阵,这里用一维矩阵就可以满足需求,每个位置代表面值为index的货币找零所需最小硬币数;dp[0]=0
  2. 状态转移方程:dp[cur]=Math.min(dp[cur-coins[i]]+1,min)
	public static void changeMoney(int[] coins,int money){
        int[] dp = new int[money+1];
        dp[0]=0;
        for (int cur = 1;cur<=money;cur++){
            // 当用最小币值的硬币找零时,所需硬币数量最多
            // 这里最小面值为1,那n元就需要n个硬币,所以是最多
            int min = cur;
            // 遍历每一种面值的硬币,看是否可作为找零的其中之一
            for (int i = 0;i<coins.length;i++){
                // 若当前面值的硬币小于当前的cents则分解问题并查表
                if (cur>=coins[i]){
                    int temp = dp[cur-coins[i]]+1;
                    min=Math.min(temp,min);
                }
            }
            // 保存最小硬币数
            dp[cur]=min;
        }
        System.out.println(money+"\t"+dp[money]);
    }

2.2.2. 数字三角形

问题:从上到下选择一条路,使得经过的数字之和最大。路径上的每一步只能往左下或者右下走。
7
3 8
8 1 0
2 7 4 4
4 5 2 6 5

递归:

	public int getMax(){
		int maxSum = getMaxSum(D,n,0,0);
		return maxSum;
	}
	public int getMaxSum(int[][] D,int n,int i,int j){
		//D存储数字三角形,n表示层数
		if(i == n){
			return D[i][j];
		}
		int x = getMaxSum(D,n,i+1,j);
		int y = getMaxSum(D,n,i+1,j+1);
		return Math.max(x,y)+D[i][j];
	}

动态规划:

  1. 首先思考如何设计dp矩阵,这里通过一个二维数组来实现,每个位置代表到达值为index的位置路过的最大数字之和;cost[0][0]=triangle[0][0]
  2. 状态转移方程:cost[i][j]= min(cost[i-1][j],cost[i-1][j-1])+triangle[i][j]
	public int minimumTotal(int[][] triangle) {
        if(triangle==null||triangle.length==0)
            return 0;
        int len = triangle.length;
        //用来记录每一步的状态
        int [][] cost = new int[len][len];
        cost[0][0]=triangle[0][0];
        for(int i=1; i<len; i++){
            for(int j=0; j<triangle[i].length; j++){
            	//计算上一个状态的时候,防止出现越界问题
                int lower = max(0,j-1);
                int upper = min(j,triangle[i-1].length-1);
            	//状态转移方程
                cost[i][j]= min(cost[i-1][lower],cost[i-1][upper])+triangle[i][j];
            }
        }
        int minCost = Integer.MAX_VALUE;
        for(int k=0; k<triangle[len-1].length; k++){
            minCost = min(minCost,cost[len-1][k]);
        }

       return minCost;
    }
2.2.3. 背包问题

01背包: 有n种物品与承重为m的背包。每种物品只有一件,每个物品都有对应的重量weight[i]与价值value[i],求解如何装包使得价值最大。

动态规划:

  1. 首先思考如何设计dp矩阵,这里通过一个二维数组来实现,dp[i][j]表示只考虑前i件物品的时候,背包可用容量为j时的最大价值
  2. 状态转移方程:dp[i][j]=max{dp[i-1][j-w[i]]+v[i],dp[i-1][j]};这里我们从j=V倒推回来的话可以优化成 dp[j]=max{dp[j],dp[j-w[i]]+v[i]};
	public static int maxValue(int[] w, int[] v, int cap) {
        int[] dp = new int[cap + 1];

        for (int i = 0; i < w.length; i++) {
            for (int j = cap; j >= w[i]; j--) {// 倒序遍历
                dp[j] = Math.max(dp[j], dp[j - w[i]] + v[i]);
            }
        }

        int maxValue = dp[cap];// 获取的最大价值
        System.out.println(Arrays.toString(dp));

        return maxValue;
    }

完全背包: 有n种物品与承重为m的背包。每种物品有无限多件,每个物品都有对应的重量weight[i]与价值value[i],求解如何装包使得价值最大。

动态规划:

  1. 首先思考如何设计dp矩阵,这里通过一个二维数组来实现,dp[i][j]表示只考虑前i件物品的时候,背包可用容量为j时的最大价值
  2. 状态转移方程:dp[i][j] = max ( dp[i-1][j],dp[i-1][j - kw[i]] +kv[i] )(k表示第i种物品放入k件),优化后:dp[j]=max{dp[j],dp[j-w[i]]+v[i]} (注意这里和01背包一样但是求解的过程不同)
	public static int maxValue(int[] w, int[] v, int cap) {
        int[] dp = new int[cap + 1];

        for (int i = 0; i < w.length; i++) {
            for (int j = w[i]; j <= cap; j++) {// 正序遍历
                dp[j] = Math.max(dp[j], dp[j - w[i]] + v[i]);
            }
        }

        int maxValue = dp[cap];// 获取的最大价值
        System.out.println(Arrays.toString(dp));

        return maxValue;
    }

我们想一下为什么?
01背包问题为何要逆序循环?因为ans[x][y]是由ans[x-1][y-weight]推导的,我们逆序循环正好保证了每个物品只被运用一次,但是如果我们正序循环,说明我们的ans[x][y]是由ans[x][y-weight]推导出的,每个物品可以被运用多次。这正是我们在完全背包里面想要的。

多重背包: 有n种物品与承重为m的背包。每种物品有有限件num[i],每个物品都有对应的重量weight[i]与价值value[i],求解如何装包使得价值最大。

动态规划:

  1. 首先思考如何设计dp矩阵,这里通过一个二维数组来实现,dp[i][j]表示只考虑前i件物品的时候,背包可用容量为j时的最大价值
  2. 状态转移方程:dp[i][j] = max (dp[i-1][j], dp[i-1][j - kw[i]] +kv[i] ) 0<=k<=num[i] (k表示第i种物品放入k件),优化后:dp[j] = Math.max(dp[j], dp[j - k * w[i]] + k * v[i])
	public static int maxValue(int[] w, int[] v,int[] n, int cap) {
        int[] dp = new int[cap + 1];

        for (int i = 0; i < w.length; i++) {
            for (int k = 0; k <= n[i]; k++) {
                for (int j = cap; j >= k * w[i]; j--) {// 正序遍历
                    dp[j] = Math.max(dp[j], dp[j - k * w[i]] + k * v[i]);
                }
            }
        }

        int maxValue = dp[cap];// 获取的最大价值
        System.out.println(Arrays.toString(dp));

        return maxValue;
    }
2.2.4. 公共子序列,公共子串问题

公共子串:
给出两个字符串,找到最长公共子串,并返回其长度

状态,字符串的每一位对应另一个字符串的每一个位置,因此通过一个二维数组来表示这每一个状态位,然后是找状态转移方程,转移方程即为:

  1. 如果两个字母不相同,值为0
  2. 如果两个字母相同,值为左上角的值加1
	public int longestCommonSubstring(String s1, String s2) {
        char[] a = s1.toCharArray();
        char[] b = s2.toCharArray();
        // a.length行,b.length列
        int[][] result = new int[a.length + 1][b.length + 1];
        int max = 0;
        for (int i = 0; i < a.length; i++) {
            for (int j = 0; j < b.length; j++) {
                if (a[i] == b[j]) {
                    result[i + 1][j + 1] = result[i][j] + 1;
                    max = Math.max(max, result[i + 1][j + 1]);
                }
            }
        }
      return max;
    }

公共子序列:
给出两个字符串,找到最长公共子序列(LCS),返回LCS的长度。和最长公共子串类似,只不过子序列是可以不连续的。

子序列和子串的区别在于,其值不是仅仅取决于其上一个位置的对应于比对的位置的状态,而是要寻找最大的前面的状态值中最大的一个。

  1. 如果两个字母不同,就选择上方和左方邻居中较大的那个
  2. 如果两个字母相同,就将当前单元格的值设置为左上方的值加一
 	public int longestCommonSubsequence(String s1, String s2) {
        char[] a = s1.toCharArray();
        char[] b = s2.toCharArray();
        // a.length行,b.length列
        int[][] result = new int[a.length + 1][b.length + 1];
        for (int i = 0; i < a.length; i++) {
            for (int j = 0; j < b.length; j++) {
                if (a[i] == b[j]) {
                    result[i + 1][j + 1] = result[i][j] + 1;
                } else {
                    result[i + 1][j + 1] = Math.max(result[i][j + 1], result[i + 1][j]);
                }
            }
        }
        return result[a.length][b.length];
    }

三、尾声

总结一下可以用动态规划的经典算法(待更新…):


  1. 斐波那契数列
  2. 剪绳子
  3. 矩阵最小路径
  4. 最长上升子序列
  5. 最长回文子字符串
  6. 最长整除子序列
  7. 寻找和为定值的多个数
  8. 单词转换的最少操作次数
  9. 股票问题
  10. N皇后问题

对于动态规划的更多问题,将会继续更新,陆续也会写一些贪心算法等常见的算法类型。

总结:
动态规划一般可分为线性动规,区域动规,树形动规,背包动规四类。
动态规划最重要其实就是状态转移方程,另外就是如何设计dp矩阵,她每个位置代表的含义以及他的初始值,多做题目熟练了就会比较容易想到

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值