DP-java版本

来源:互联网 发布:淘宝店铺如何上传宝贝 编辑:程序博客网 时间:2024/05/16 14:15

常见dp问题的java叙述总结。

背包问题

0-1背包

对于0-1背包问题,运用dp的思想可以有两种常见状态转移:认为物品下标从0开始

  • dp[i][j]表示从前i-1个物品选,在重量不超过j的情况下的最大价值。(正向)
  • dp[i][j]表示从第i个物品开始选,在重量不超过j的情况下的最大价值。(反向)

状态转移方程分别如下:

#初始化dp[0][j]=0if j<w[i]:    dp[i+1][j]=dp[i][j]else:    dp[i+1][j]=max(dp[i][j],dp[i][j-w[i]]+v[i])
#初始化dp[n][j]=0if j<w[i]:    dp[i][j]=dp[i+1][j]else:    dp[i][j]=max(dp[i+1][j],dp[i+1][j-w[i]]+v[i])

完全背包

这里运用正向dp思路:

dp[i][j]表示从前i-1个物品选,在重量不超过j的情况下的最大价值。

转移方程如下:

#初始化dp[0][j]=0dp[i+1][j]=max(dp[i][j],dp[i][j-k*w[i]]+k*v[i])  #(k>=0且k*w[i]<=j)

这是最容易想到的思路,但是实际上还可以优化,因为其中有重复计算:

比如在计算dp[i+1][j]的计算中(k>=1),与在d[i+1][j-w[i]]的计算中选择k-1的情况相同。

即有以下变形:

 dp[i+1][j]=max(dp[i][j-k*w[i]]+j*v[i])(k>=0)=max(dp[i][j],max(dp[i][j-k*w[i]]+k*v[i]))(k>=1)=max(dp[i][j],max(dp[i][j-w[i]-k*w[i]]+k*v[i]+v[i]))(k>=0)=max(dp[i][j],dp[i+1][j-w[i]]+v[i])

所以时间复杂度降为平方级。

package DP;/** * @author prime  on 2017/5/1. */import java.util.Arrays;public class KnapSack{//各种背包    private static int solve0_1(int[] v,int[] w,int c)    {//正向dp解0-1背包        int n=v.length;        int[][] dp=new int[n+1][c+1];//从前i个物品中选出总重量不超过j的物品时总价值的最大值        Arrays.fill(dp[0],0);//dp[0][j]置0        for (int i=0;i<n;i++)            for (int j=0;j<=c;j++)            {                if (j<w[i])                    dp[i+1][j]=dp[i][j];                else                    dp[i+1][j]=Math.max(dp[i][j],dp[i][j-w[i]]+v[i]);            }        return dp[n][c];    }    private static int solve0_1_reverse(int[] v,int[] w,int c)    {//反向dp解0-1背包        int n=v.length;        int[][] dp=new int[n+1][c+1];//从第i个物品开始选,在不超过j的条件下的最大价值(i从0开始)        Arrays.fill(dp[n],0);//dp[n][j]置0        for (int i=n-1;i>=0;i--)            for (int j=0;j<=c;j++)            {                if (j<w[i])                    dp[i][j]=dp[i+1][j];                else                    dp[i][j]=Math.max(dp[i+1][j],dp[i+1][j-w[i]]+v[i]);            }        return dp[0][c];    }    private static int[][] dp=new int[6][101];    private static int[] w={10,20,30,40,50};    private static int[] v={20,30,65,40,60};    private static int n=5;    private static int solve0_1_rec(int i,int j)//从第i个物品开始选,在不超过j的条件下的最大价值(i从0开始)    {//反向dp的递归版本        int res;        if (dp[i][j]>0)//备忘录,没有这个就是BS            return dp[i][j];        if (i==n)            return 0;        else if (j<w[i])            res=solve0_1_rec(i+1,j);        else            res=Math.max(solve0_1_rec(i+1,j),solve0_1_rec(i+1,j-w[i])+v[i]);        return dp[i][j]=res;    }    private static int solve(int[] v,int[] w,int c)    {//完全背包问题,正向dp思考,优化后        int n=v.length;        int[][] dp=new int[n+1][c+1];        Arrays.fill(dp[0],0);//dp[0][j]置0        for (int i=0;i<n;i++)            for (int j=0;j<=c;j++)            {                if (j<w[i])                    dp[i+1][j]=dp[i][j];                else                    dp[i+1][j]=Math.max(dp[i][j],dp[i+1][j-w[i]]+v[i]);            }        return dp[n][c];    }    private static int solve_bad(int[] v,int[] w,int c)    {        int n=v.length;        int[][] dp=new int[n+1][c+1];        Arrays.fill(dp[0],0);//dp[0][j]置0        for (int i=0;i<n;i++)            for (int j=0;j<=c;j++)                for (int k=0;k*w[i]<=j;k++)                {                    dp[i+1][j]=Math.max(dp[i+1][j],dp[i][j-k*w[i]]+k*v[i]);                }        return dp[n][c];    }    public static void main(String[] args)    {        int[] w={10,20,30,40,50};        int[] v={20,30,65,40,60};        System.out.println(solve0_1(v,w,100));        System.out.println(solve0_1_reverse(v,w,100));        System.out.println(solve0_1_rec(0,100));        System.out.println(solve(v,w,100));        System.out.println(solve_bad(v,w,100));    }}

换钱问题

dp[i][j]表示可以随便使用0..i的纸币的情况下,组成j元所需要的最少纸币数。

初始化都是相同的,即第一列dp[i][0]都是0;而第一行dp[0][j],如果可以被第一个货币整除就填入结果即货币数(对于第二种纸币只有一张的情况下只有等于第一个纸币才填1),否则指定一个非常大的数(最好不要用整数最大值,可能会溢出)

无限纸币

这种情况下和完全背包问题很相似:

if arr[i]>j: #当前货币太大了    dp[i][j]=dp[i-1][j]else:    dp[i][j]=min(dp[i-1][j],dp[i][j-arr[i]]+1)

这里的也是像上面完全背包问题一样,化简后的结果。原始递推公式是:

if arr[i]>j: #当前货币太大了    dp[i][j]=dp[i-1][j]else:    dp[i][j]=min(dp[i][j*k*arr[i]]+k)(k>=0)

这里对它用上面一样的方法化简即可。

单一纸币

这种就相对容易些:

if arr[i]>j: #当前货币太大了    dp[i][j]=dp[i-1][j]else:    dp[i][j]=min(dp[i-1][j],dp[i-1][j-arr[i]]+1)

代码如下:

package DP;/** * 换钱问题的DP求解 */import java.util.Arrays;import java.util.Scanner;public class money{    private static int minCoin1(int[] arr,int aim)    {//每种纸币无限制的换钱问题        int n=arr.length;        int[][] dp=new int[n][aim+1];//dp[i][j]表示在可以任意使用0..i的货币的情况下,组成j所需要的最小张数        for (int i=0;i<n;i++)//第一列显然是0            dp[i][0]=0;        for (int j=1;j<=aim;j++)        {            if (j%arr[0]==0)                dp[0][j]=j/arr[0];            else                dp[0][j]=20000;//不用整数最大值是为了防止溢出。        }        /*以上是初始化部分*/        for (int i=1;i<n;i++)            for (int j=0;j<=aim;j++)            {                if (j<arr[i])//第i个钱太大了                    dp[i][j]=dp[i-1][j];                else                    dp[i][j]=Math.min(dp[i-1][j],dp[i][j-arr[i]]+1);            }        return dp[n-1][aim]!=20000?dp[n-1][aim]:-1;    }    private static int minCoin2(int[] arr,int aim)    {//每种纸币只能用一次        int n=arr.length;        int[][] dp=new int[n][aim+1];//dp[i][j]表示任意使用0..i的纸币,组成j元的最小张数        for (int i=0;i<n;i++)            dp[i][0]=0;        for (int j=1;j<=aim;j++)        {            if (j==arr[0])                dp[0][j]=1;            else                dp[0][j]=20000;        }        /*初始化完成*/        for (int i=1;i<n;i++)            for (int j=0;j<=aim;j++)            {                if (arr[i]>j)                    dp[i][j]=dp[i-1][j];                else                    dp[i][j]=Math.min(dp[i-1][j],dp[i-1][j-arr[i]]+1);            }        return dp[n-1][aim]!=20000?dp[n-1][aim]:-1;    }    public static void main(String[] args)    {        int[] a={5,7,25,50};        System.out.println(minCoin1(a,15));        System.out.println(minCoin2(a,15));    }}

换钱的方法数问题(每种货币无限)

三种方法:暴力搜索、记忆化搜索、DP

暴力搜索

暴力搜索基于以下事实:

假设货币数组arr[0..n-1],目标aim,则过程如下:

  • 不用arr[0],用剩下的arr[1..n-1]组成aim
  • 用一张arr[0],用剩下的arr[1..n-1]组成aim-arr[0]
  • 用两张arr[0],用剩下的arr[1..n-1]组成aim-2*arr[0]
  • ……

所以,就可以写出一个递归的暴力搜索方法:

private static int coin1(int[] arr,int aim)    {//暴力搜索        if (arr==null||aim<0||arr.length==0)            return 0;        return BS1(arr,0,aim);    }    private static int BS1(int[] arr,int index,int aim)    {/*BS1表示如果用arr[index..]组成aim元的方法数*/        int res=0;        if (index==arr.length)            res=aim==0?1:0;        else        {            for (int i=0;i*arr[index]<=aim;i++)                res+=BS1(arr,index+1,aim-i*arr[index]);        }        return res;    }

暴力搜索最大的弊端在于有很多重复计算,比如用2张5元和一张10元而言,后序的递归都是一样的情形。为此想保留每次计算的结果,有了下面的优化:

记忆化的暴力搜索

把上述BS1的参数和返回值对应起来,并保存到二维数组中:

private static int coin2(int[] arr,int aim)    {/*记忆后的暴力搜索*/        if (arr==null||arr.length==0||aim<0)            return 0;        int[][] memo=new int[arr.length+1][aim+1];        for (int[] e:memo)            Arrays.fill(e,-1);        return BS2(arr,0,aim,memo);    }    private static int BS2(int[] arr,int index,int aim,int[][] memo)    {/*基本参数都和上面一样,memo[i][j]表示index为i,aim为j时的计算结果*/        int res=0;        if (index==arr.length)            res=aim==0?1:0;        else        {            if (memo[index][aim]!=-1)                return memo[index][aim];            for (int i=0;i*arr[index]<=aim;i++)                res+=BS2(arr,index+1,aim-i*arr[index],memo);        }        return memo[index][aim]=res;    }

初始化memo为-1表示还没被计算,当后序程序递归时,查表即可。

非最优的动态规划

dp和记忆化搜索本质上是一样的,都是空间换时间,以某种方式 进行记录。不同的地方在于dp规定计算顺序,后面的依赖前面的结果;而记忆化搜索只是单纯记录中间结果,对顺序没有规定。

private static int coin3(int[] arr,int aim)    {//dp法        int n=arr.length;        /*dp[i][j]表示使用0..i的货币组成j元的方法数*/        int[][] dp=new int[n][aim+1];        /*第一列显然都是1*/        for (int i=0;i<n;i++)            dp[i][0]=1;        for (int j=0;j<=aim;j++)        {            if (j%arr[0]==0)                dp[0][j]=1;//能被找开就说明是一种方法        }        for (int i=1;i<n;i++)            for (int j=0;j<=aim;j++)            {                int count=0;                for (int k=0;k*arr[i]<=j;k++)                    count+=dp[i-1][j-k*arr[i]];                dp[i][j]=count;            }        return dp[n-1][aim];    }

最优DP

上面的dp之中还有很多重复计算,利用推导可以化简如下:

 dp[i][j]=dp[i-1][j-arr[i]*k] (k>=0)=dp[i-1][j]+dp[i-1][j-arr[i]-arr[i]*k](k>=0)=dp[i-1][j]+dp[i][j-arr[i]]

由此有以下代码:

private static int coin4(int[] arr,int aim)    {//最优dp        int n=arr.length;        /*dp[i][j]表示使用0..i的货币组成j元的方法数*/        int[][] dp=new int[n][aim+1];        /*第一列显然都是1,组成0元只有一种方法*/        for (int i=0;i<n;i++)            dp[i][0]=1;        /*第一行如果可以被找开就是1*/        for (int j=1;j<=aim;j++)            if (j%arr[0]==0)                dp[0][j]=1;        for (int i=1;i<n;i++)            for (int j=0;j<=aim;j++)            {                if (j<arr[i])                    dp[i][j]=dp[i-1][j];                else                    dp[i][j]=dp[i-1][j]+dp[i][j-arr[i]];            }        return dp[n-1][aim];    }
0 0
原创粉丝点击