DP-java版本

这篇博客总结了使用Java实现动态规划(DP)解决背包问题和换钱问题。包括0-1背包、完全背包问题,以及不同类型的换钱问题,如无限纸币、单一纸币等。通过优化的状态转移方程减少了重复计算,提高了效率。同时,介绍了暴力搜索、记忆化搜索和最优动态规划的策略,并给出了相应的代码示例。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

常见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];
    }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值