【算法】动态规划(二)

前言

上一篇,介绍了动态规划DP的概念,以及暴力递归和动态规划的关系。这一篇,将介绍几道经典的动态规划题

1、台阶问题

题目

有n级台阶,一个人每次上一级或者两级,问有多少种可以走完n级台阶的方法

思路

想走到第n级台阶,有两种途径
1、在n-1级上一级
2、在n-2级上两级
因此,我们可以知道f(n) = f(n - 1) + f(n - 2) ,其实答案就是斐波那契数列

代码实现

递归版本

    public static int step1(int n) {
        // 对于第n级台阶方法 = f(n - 1) + f(n - 2)
        if(n == 1 || n == 2) {
            return n;
        }
        return step1(n - 1) + step1(n - 2);
    }

动态规划版本

    public static int step2(int n) {
        if(n == 1 || n == 2) {
            return n;
        }
        int[] dp = new int[1000];
        dp[1] = 1;
        dp[2] = 2;
        for(int i = 3; i <= n; i++) {
            dp[i] = dp[i - 1] + dp[i - 2];
        }
        return dp[n];
    }

2、最长递增子序列长度

题目

给定数组arr,返回arr的最长递增子序列长度。返回arr的最长递增子序列长度。
比如arr=[2,1,5,3,6,4,8,9,7],最长递增子序列为[1,3,4,8,9],
所以返回这个子序列的长度5

算法实现

我们构建一个以为数组dp,dp[i]的表示以arr[i]结尾时,最长递增子序列的长度。计算dp[i]时,先判断arr[i]是否大于arr[0..i],若大于,则找到dp[i..1]中最大的值+1,否则dp[i]置为1。最后返回dp数组中最大的数,即为最长递增子序列长度

    public static int commonLongestAscSeq(int arr[]) {
        if (arr == null) {
            return 0;
        }
        if (arr.length == 1) {
            return 1;
        }
        int max = 0;
        // dp[i]代表以arr[i]为结尾的情况下,arr[0..i]的最大递增子序列长度
        int[] dp = new int[arr.length];
        dp[0] = 1;
        for(int i = 1; i < arr.length; i++) {
            // 用于标记arr[i]是否比arr[0~i-1]的数都小
            boolean isSmallThanPre = true;
            // 对于arr[i],我们要找0~i-1的最大递增子序列,然后在其基础上+1
            for(int j = 0; j < i; j++) {
                if(arr[i] > arr[j]) {
                    dp[i] = Math.max(dp[j] + 1, dp[i]);
                    isSmallThanPre = false;
                }
            }
            // 如果都比i~1的数小,那么就置为1
            if (isSmallThanPre) {
                dp[i] = 1;
            }
            max = Math.max(dp[i], max);
        }
        return max;
    }

3、01背包问题

题目

给定 n 种物品和一个容量为 bag的背包,物品 i 的重量是 weights[i],其价值为 values[i]
问:应该如何选择装入背包的物品,使得装入背包中的物品的总价值最大?
例如weights = { 2, 2, 6, 5, 4 },values = { 6, 3, 5, 4, 6 },bag=10,那么总价值最大为15,当选择v[0],v[1],v[4]时,6 + 3 + 6价值最大,重量为2 + 2 + 4 = 8。01背包问题就是每件物品最多拿一件,还有其延伸版本,完全背包问题等等,这里就不描述了。

思路

创建一个二维数组[物品的数量 + 1][背包的大小 + 1],dp[i][j]表示,在拿第i件物品时,j背包容量为j时,所能获得最大价值。那么对于dp[i][j],有两种情况
1、不拿这件物品时,dp[i][j]的价值更高 -> dp[i][j] = dp[i - 1][j]
2、拿这件物品时,dp[i][j]的价值更高,因此,我们在不拿i这件物品时(i - 1),且重量减去i物品重量( j - w[i])的最优解基础上 加上该物品的价值 -> dp[i][j] = v[i] + dp[i - 1][j - w[i - 1]]
得到了以上递推式后,我们就可以进行动态规划了

整个填表的过程如下所示:


【算法】动态规划(二)_第1张图片

算法实现

    public static int maxValue2(int[] weights, int[] values, int bag) {
        // dp[i][j]表示,在0~i件物品选择,j背包容量为j时,所能获得最大价值
        int[][] dp = new int[weights.length + 1][bag + 1];
        for(int i = 1; i < dp.length; i++) {
            for(int j = 1; j <= bag; j++) {
                // 容量不够放下i,所以不拿
                if (j < weights[i - 1]) {
                    dp[i][j] = dp[i - 1][j];
                }else {
                    int max1 = dp[i - 1][j];
                    int max2 = values[i - 1] + dp[i - 1][j - weights[i - 1]];
                    dp[i][j] = Math.max(max1, max2);
                }
            }
        }
        return dp[weights.length][bag];
    }

4、最小代价

题目

给定两个字符str1和str2,再给定三个整数ic,dc和rc,分别代表插入、删除和替换一个
字符的代价。返回将str1编辑成str2的最小代价。
例如,str1="abc", str2="adc", ic=5,dc=3, rc=2。从"abc"->"adc",把"b"替换
成“d”的代价最小,所以返回2.
再比如str1="abc", str2="adc", ic=5,dc=3, rc=100。从"abc"->"adc",先删除"b",
然后插入"d"是代价最小的,所以返回8.

思路

创建一个二维数组dp[str1.len + 1][str2.len + 1],dp[i][j]代表str1[0..i] -> str2[0..j]的最小代价。
那么对于第一列代价都是删除代价

// 填写第一列,那么就是str1[0~i] -> ""
for(int i = 1; i <= chs1.length; i++) {
    dp[i][0] = i * dc;
}

同理,对于第一行都是插入代价,那么就是"" -> str2[0~j]

// 填写第一行,那么就是"" -> str2[0~j]
for(int j = 1; j <= chs2.length; j++) {
    dp[0][j] = j * ic;
}

那么而对于非第一行,第一列,dp[i][j]的值可能来自以下4中情况,注意,我们创建的dp数组是[str1.len + 1][str2.len + 1]的,因此求i,j的问题,我们需要回原数组,就需要i-1,j-1
1、【不操作】当str1[i - 1] == str2[i - 1],说明str1[0..i-1] -> str2[0..j-1]只需要str1[0..i-2] 编辑成 str2[0..j-2]的代价,也就是dp[i - 1][j - 1]
2、【删除】先把str1[0~i-2] 编辑成 str2[0~j-1],那么就删除str[i-1],因此此时花销为dp[i - 1][j] + dc
3、【插入】先把str1[0~i-1] 编辑成 str2[0~j-2],那么插入一个字符,使之相等,因此此时花销为ic + dp[i][j - 1]
4、【替换】先把str1[i - 2] 编辑成 str2[j - 2],那么直接替换str1[i - 1],使之相等,因此此时花销为dp[i - 1][j - 1] + rc

以str1 = "ab12cd3",str2 = "abcdf",ic=5,dc=3,rc=2为例,蓝色部分为初始的第一行,第一列的数字,整个填表的过程如下:


【算法】动态规划(二)_第2张图片

算法实现

    public static int convertFirstToSecond(String str1, String str2, int ic, int dc, int rc) {
        char[] chs1 = str1.toCharArray();
        char[] chs2 = str2.toCharArray();
        // dp[i][j]代表str1[0..i] -> str2[0..j]的最小代价
        int[][] dp = new int[chs1.length + 1][chs2.length + 1];
        // 填写第一列,那么就是str1[0~i] -> ""
        for(int i = 1; i <= chs1.length; i++) {
            dp[i][0] = i * dc;
        }
        // 填写第一行,那么就是"" -> str2[0~j]
        for(int j = 1; j <= chs2.length; j++) {
            dp[0][j] = j * ic;
        }
        for(int i = 1; i <= chs1.length; i++) {
            for(int j = 1; j <= chs2.length; j++) {
                if(chs1[i - 1] == chs2[j - 1]) {
                    // 说明str1[0~i-1] -> str2[0~j-1]只需要
                    // str1[0~i-2] -> str2[0~j-2]的代价,也就是dp[i - 1][j - 1]
                    dp[i][j] = dp[i - 1][j - 1];
                }else {
                    // 若当前字符不等,则有三种情况
                    // 1、先把str1[0~i-2] 编辑成 str2[0~j-1],那么就删除str[i-1]
                    // 因此此时花销为dp[i - 1][j] + dc
                    int min1 = dc + dp[i - 1][j];
                    // 2、先把str1[0~i-1] 编辑成 str2[0~j-2],那么插入一个字符,使之相等
                    // 因此此时花销为ic + dp[i][j - 1]
                    int min2 = ic + dp[i][j - 1];
                    dp[i][j] = Math.min(min1, min2);
                    // 3、先把str1[i - 2] 编辑成 str2[j - 2],那么直接替换str1[i - 1],使之相等
                    // 因此此时花销为dp[i - 1][j - 1] + rc
                    int min3 = dp[i - 1][j - 1] + rc;
                    dp[i][j] = Math.min(dp[i][j], min3);
                }
            }
        }
        return dp[chs1.length][chs2.length];
    }

总结

其实动态规划,也可以简单的认为是一个填表的过程,找出转换方程后,我们先填写显而易见的初始数值,然后再根据初始数字推出下一行的数据,直至整张表填完。然而有些动态规划的题目,不是那么直接的填表,例如,零钱问题的最优解。因此,下一篇就动态规划的读表展开~

你可能感兴趣的:(【算法】动态规划(二))