动态规划示例汇总-Java版(组合硬币、跳台阶、最小路径和、最长递增子序列、最长公共子序列、01背包问题、最小编辑代价)

动态规划算法示例汇总-Java版

  • 组合硬币
    • Java解题—暴力搜索
    • Java解题—记忆搜索
    • Java解题—动态规划(两种写法)
  • 跳台阶
    • Java解题—暴力递归
    • Java解题—动态规划
  • 矩阵最小路径和
    • Java解题—动态规划
  • 最长递增子序列
    • Java解题—动态规划
  • 字符串最长公共子序列
    • Java解题—动态规划
  • 0-1背包问题
    • Java解题—动态规划
  • 最小编辑代价
    • Java解题—动态规划

组合硬币

给定数组arr,arr中所有的值都为正数且不重复。每个值代表一种面值的货币,每种面值的货币可以使用任意张,再给定一个整数aim代表要找的钱数,求换钱有多少种方法。

下面的解题思路是具有递进关系的!

解题思路—暴力搜索:从arr[0]为0张开始遍历,让arr中剩余的值组成aim;接着arr[0]为一张…直至arr[0]的张数为aim/arr[0]。使用递归求解。暴力搜索存在大量重复计算,例如:如果已经使用0张5元和1张10元的情况的后续解法与已经使用2张5元和0张10元的情况一模一样,不推荐!
解题思路—记忆搜索:为了避免结果的重复计算,所以使用建议哈希map来保存计算的结果,index表示已经遍历到数组的第几位,使用index和aim组成共同的key,返回结果作为value。依旧使用递归求解。在进入递归前,先在哈希map中查询有没有对应的index和aim,如果存在,则直接取值;如果不存在,继续进行递归。 需要注意的是map的大小, index要多加一维用来避免数组越界。
解题思路—动态规划(两种写法)

如果arr长度为N,生成行数为N+1(为什么会多加一维?第一维是任何货币都不使用的结果,即arr[0]=0,方便后续递归!原arr值为{5,10,25,1},现arr值为{0,5,10,25,1}),列数为aim+1的矩阵dp。dp[i][j]的含义是在使用arr[0…i]货币情况下,组成钱数有多少种方法。
先要初始化一下dp数组,当需要组成钱数0时,每一种货币只有一种方法,就是一张不出。这在数组dp中的体现为:dp[i][0] = 1,即dp的第一列全置1。

  1. 接下来,从dp[1][1]开始枚举:
    dp[i][j] = dp[i-1][j] + dp[i-1][j-1×arr[i-1]] + dp[i-1][j-2×arr[i-1]] + …
    这样写的时间复杂度为O(N×aim2)。
  2. 第二种写法,就是将上述的枚举过程简化:
    dp[i][j] = dp[i-1][j-arr[i-1]] + dp[i-1][j] ,通过下图可以帮助理解
    这样写的时间复杂度缩小至O(N×aim)。

动态规划示例汇总-Java版(组合硬币、跳台阶、最小路径和、最长递增子序列、最长公共子序列、01背包问题、最小编辑代价)_第1张图片

Java解题—暴力搜索

public int coins1(int[] arr, int aim){
   if(arr==null || arr.length==0 || aim<0)
        return 0;
    return process1(arr, 0, aim);
}

public int process1(int[] arr, int index, int aim){
    int res = 0;
    if(index==arr.length)
        res = aim==0?1:0;
    else{
        for(int i=0;arr[index]*i<=aim;i++){
            res += process1(arr, index+1, aim-arr[index]*i);
        }
    }
    return res;
}

Java解题—记忆搜索

public int coins2(int[] arr, int aim){
    if(arr==null || arr.length==0 || aim<0)
        return 0;
    int[][] map = new int[arr.length+1][aim+1];
    return process2(arr, 0, aim, map);
}

public int process2(int[] arr, int index, int aim, int[][] map){
    int res = 0;
    if(index==arr.length)
        res = aim==0?1:0;
    else{
        int mapValue = 0;
        for(int i=0;arr[index]*i<=aim;i++){
            mapValue = map[index+1][aim-arr[index]*i];
            if(mapValue!=0)
                res += mapValue;
            else
                res += process2(arr, index+1, aim-arr[index]*i, map);
        }
    }
    map[index][aim] = res;
    return res;
}

Java解题—动态规划(两种写法)

public int coins3(int[] arr, int aim){
    if(arr==null || arr.length==0 || aim<=0)
        return 0;
    // dp的维度 arr.length+1,多了一维不使用任何币种
    int[][] dp = new int[arr.length+1][aim+1];
    // aim=0的时候
    for(int i=0;i<=arr.length;i++)
        dp[i][0] = 1;

// 下面有两种写法
    for(int i=1;i<=arr.length;i++){
        for(int j=1;j<=aim;j++){
        
            // k为arr[i]有多少张
            for(int k=0;k<=j/arr[i-1];k++){
                dp[i][j] += dp[i-1][j - k*arr[i-1]];
            }
            
//            if((j-arr[i-1])>=0)
//                dp[i][j] = dp[i][j-arr[i-1]] + dp[i-1][j];
//            else // 当前货币为0张
//                dp[i][j] += dp[i-1][j];
        }
    }
    return dp[arr.length][aim];
}

跳台阶

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

解题思路—暴力递归:想要走到第i级台阶,只能从第i-1级台阶迈一步到第i级台阶,或者从第i-2级台阶迈两步到第i级台阶。即 f(i) = f(i-1) + f(i-2) 。设置好初始化f(1)=1, f(2)=2即可。
解题思路—动态规划递归会造成栈溢出,所以进一步我们选择使用动态规划,自底向上,保存先计算的值

Java解题—暴力递归

public int s1(int target){
    if(target<1) return 0;
    if(target<3) return n;
    return s1(target-1)+s1(target-2);
}

Java解题—动态规划

public int s2(int target) {
    if(target<=2)
        return target;
    int f1 = 1;
    int f2 = 2;
    int f3 = 3;
    for(int i=3;i<=target;i++){
        f3 = f1 + f2;
        f1 = f2;
        f2 = f3;
    }
    return f3;
}

矩阵最小路径和

给定一个矩阵m,从左上角开始每次只能向右或者向下走,最后达到右下角的位置,路径上所有的数字累加起来就是路径和,返回所有的路径中最小的路径和。如果给定的m如大小看到的样子,路径1,3,1,0,6,1,0是所有路径中路径和最小的,所以返回12。
1 3 5 9
8 1 3 4
5 0 6 1
8 8 4 0

解题思想—动态规划

假设矩阵m的大小为M×N,行数为M,列数为N。生成大小和m一样的矩阵dp,行数为M, 列数为N。dp[i][j]的值表示从左上角,也就是(0,0)位置,走到(i,j)位置的最小路径和。

  1. 因为第一行与第一列只能从(0,0)开始走,所以dp初始化为:
    动态规划示例汇总-Java版(组合硬币、跳台阶、最小路径和、最长递增子序列、最长公共子序列、01背包问题、最小编辑代价)_第2张图片
  2. 接下来,从dp[1][1]开始遍历:
    dp[i][j] = m[i][j] + min{dp[i-1][j], dp[i][j-1]} // 取dp[i-1][j] 和 dp[i][j-1] 中最小的一个
    这就是动态规划的核心代码

Java解题—动态规划

public int pathSum(int[][] m){
    if(m==null || m.length==0 || m[0].length==0)
        return 0;

    int[][] dp = new int[m.length][m[0].length];
    dp[0][0] = m[0][0];
    for(int i=1;i

最长递增子序列

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

解题思路—动态规划

生成一个与arr长度相同的数组dp,dp[i]表示在必须以arr[i]这个数结尾的情况下,arr[0…i]中最大递增子序列长度。
dp[i] = max{dp[j] + 1(0<=j
看图更易理解
动态规划示例汇总-Java版(组合硬币、跳台阶、最小路径和、最长递增子序列、最长公共子序列、01背包问题、最小编辑代价)_第3张图片
动态规划示例汇总-Java版(组合硬币、跳台阶、最小路径和、最长递增子序列、最长公共子序列、01背包问题、最小编辑代价)_第4张图片

Java解题—动态规划

public int longestSub(int[] arr){
    if(arr==null || arr.length==0)
        return 0;

    int[] dp = new int[arr.length];
    dp[0] = 1;

    for(int i=0;imax)
                max = dp[j];
        }
        dp[i] = max + 1;
    }

    Arrays.sort(dp);
    return dp[dp.length-1];
}

字符串最长公共子序列

给定两个字符串str1和str2,返回两个字符串的最长公共子序列。例如:str1=“1A2C3D4B56”,str2=“B1D23CA45B6A”,"123456"或者"12C4B6"都是最长公共子序列,返回哪一个都行。

解题思路—动态规划

假设str1的长度为M,str2的长度为N,生成大小为(M+1)×(N+1)的矩阵dp。dp[i][j]的含义是str1[0…i]与str2[0…j]的最长公共子序列的长度。
接着,从dp[1][1]开始枚举:有2种可能

  1. str1[i]==str2[j],则
    dp[i][j] = dp[i-1][j-1]+1
  2. str1[i]!=str2[j],则
    dp[i][j] = max{dp[i-1][j], dp[i][j-1]}

Java解题—动态规划

public StringBuilder str = new StringBuilder();
public String sameSub(String str1, String str2){
    if(str1==null || str1.length()==0 || str2==null || str2.length()==0)
        return null;

    int[][] dp = new int[str1.length()+1][str2.length()+1];

    for(int i=1;i<=str1.length();i++)
        for(int j=1;j<=str2.length();j++)
            if (str1.charAt(i-1)==str2.charAt(j-1))
                dp[i][j] = dp[i-1][j-1]+1;
            else
                dp[i][j] = Math.max(dp[i-1][j], dp[i][j-1]);

    printSub(dp, str1, str2, str1.length(), str2.length());
    return str.toString();
}

public void printSub(int[][] dp, String str1, String str2, int i, int j){
    if(i==0 || j==0)
        return;

    if(str1.charAt(i-1)==str2.charAt(j-1)){
        printSub(dp, str1, str2, i-1, j-1);
        str.append(str1.charAt(i-1));
    }else if(dp[i-1][j]>=dp[i][j])
        printSub(dp, str1, str2, i-1, j);
    else
        printSub(dp, str1, str2, i, j-1);
}

0-1背包问题

一个背包有一定的承重W,有N件物品,每件都有自己的价值,记录在数组v中,也都有自己的重量,记录在数组w中,每件物品只能选择要装入背包还是不装入背包,要求在不超过背包承重的前提下,选出物品的总价值最大。

解题思路—动态规划

假设物品编号从1到n,一件一件物品考虑是否加入背包。
假设dp[x][y]表示前x件物品,不超过重量y的时候的最大价值,枚举一下第x间物品的情况:

  1. 如果选择第x件物品,则前 x-1 件物品得到的重量不能超过 y-w[x] 。
  2. 如果不选第x件物品,则前 x-1 件物品得到的重量不能超过y。

所以, dp[x][y] = max{dp[x-1][y], dp[x-1][y-w[x]]+v[x]}

Java解题—动态规划

// 写法一:使用二维数组,缺点:若物品数量过多,内存消耗过大
public int knapsack(int W, int[] w, int[] v){
    if(W<=0 || w==null || w.length==0 || v==null || v.length==0)
        return 0;
    int n = w.length;
    int[][] dp = new int[n+1][W+1];
    for(int i=1;i<=n;i++)
        for(int j=1;j<=W;j++)
            if (j-w[i-1]>=0)
                dp[i][j] = Math.max(dp[i-1][j], dp[i-1][j-w[i-1]]+v[i-1]);
            else
                dp[i][j] = dp[i-1][j];
    return dp[n][W];
}

// 写法二:使用一维数组,减小内存消耗
public static int knapsack(int W, int[] w, int[] v){
    if(W<=0 || w==null || w.length==0 || v==null || v.length==0)
        return 0;
    int n = w.length;
    int[] dp = new int[W+1];
    for(int i=1;i<=n;i++)
        for(int j=W;j>=w[i-1];j--)
            if(dp[j-w[i-1]]+v[i-1]>dp[j])
                dp[j] = dp[j-w[i-1]]+v[i-1];

    return dp[W];
}

最小编辑代价

给定两个字符串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。

解题思路—动态规划

假设str1的长度为M,str2的长度为N,首先生成大小为(M+1)×(N+1)的矩阵dp,dp[i][j]的值代表str1[0…i-1]编辑成str2[0…j-1]的最小代价。比如,str1=“ab12cd3”,str2=“abcdf”,ic=5,dc=3,rc=2。
dp矩阵如下图所示;
动态规划示例汇总-Java版(组合硬币、跳台阶、最小路径和、最长递增子序列、最长公共子序列、01背包问题、最小编辑代价)_第5张图片

  1. dp[0][0] 设置为0,表示str1空的子串编辑成str2空的子串,故代价为0。
  2. 矩阵dp第一列即dp[0…M][0],dp[i][0]表示str1[0…i-1]编辑成空串的最小代价,即把str1[0…i-1]所有字母都删掉的代价,故dp[i][0]=dc×i
  3. 矩阵dp第一行即dp[0][0…N],dp[0][j]表示空串编辑成str2[0…j-1]的最小代价,即在空串里插入str2[0…j-1]的所有字符的代价,故dp[0][j]=ic×j
  4. 从dp[1][1]开始遍历:
    (1)str1[0…i-1]可以先编辑成str1[0…i-2],即删除str1[i-1],然后由str1[0…i-2]编辑成str2[0…j-1],dp[i-1][j]就表示str1[0…i-2]编辑成str2[0…j-1]的最小代价,那么dp[i][j]=dc+dp[i-1][j]
    (2)str1[0…i-1]可以先编辑成str2[0…j-2],然后将str2[0…j-2]插入字符str2[j-1],编辑成str2[0…j-1],dp[i][j-1]表示str1[0…i-1]编辑成str2[0…j-2]的最小代价,那么dp[i][j]=ic+dp[i][j-1]
    (3)str1[i-1]!=str2[j-1],先把str1[0…i-1]中str1[0…i-2]的部分变成str2[0…j-2],然后把字符str1[i-1]替换成str2[j-1],那么dp[i][j]=rc+dp[i-1][j-1]
    (4)str1[i-1]==str2[j-1],那么dp[i][j]=dp[i-1][j-1]
    dp = 四种可能中的最小值。

Java解题—动态规划

public int findMinCost(String str1, String str2, int ic, int dc, int rc){
    if(str1==null || str2==null)
        return 0;
    if(str1.length()==0) return str2.length()*ic;
    if(str2.length()==0) return str1.length()*ic;

    int[][] dp = new int[str1.length()+1][str2.length()+1];
    for(int i=0;i<=str1.length();i++)
        dp[i][0] = dc * i;
    for(int i=0;i<=str2.length();i++)
        dp[0][i] = ic * i;

    for(int i=1;i<= str1.length();i++)
        for(int j=1;j<= str2.length();j++){
            if(str1.charAt(i-1)==str2.charAt(j-1))
                dp[i][j] = dp[i-1][j-1];
            else
                dp[i][j] = dp[i-1][j-1] + rc;

            dp[i][j] = Math.min(dp[i][j], Math.min(dp[i-1][j]+dc, dp[i][j-1]+ic));
        }

    return dp[str1.length()][str2.length()];
}

你可能感兴趣的:(Java算法与数据结构)