常见dp问题的java叙述总结。
背包问题
0-1背包
对于0-1背包问题,运用dp的思想可以有两种常见状态转移:认为物品下标从0开始
dp[i][j]
表示从前i-1个物品选,在重量不超过j的情况下的最大价值。(正向)dp[i][j]
表示从第i个物品开始选,在重量不超过j的情况下的最大价值。(反向)
状态转移方程分别如下:
#初始化dp[0][j]=0
if 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]=0
if 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]=0
dp[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];
}