[java] 动态规划

引言(动态规划之装载问题)

1、问题描述

一艘船可装载重量为12,有6个集装箱,各自的重量为a[n]=[1,2,3,4,5,6],设计一个可以装载的方案,使得船装下集装箱重量最大

2、问题解决

比较有趣的一句话是:每个动态规划都从一个网格开始。 (所以学会网格的推导至关重要,而有些题解之所以写的不好,就是因为没有给出网格的推导过程,或者说,没有说清楚为什么要”这样“设计网格。)

装载问题的网格如下:

m[i][j]j0123456789101112
i
60000006666666
5000005666661111
4000045666991111
3000345678991111
20023456789101111
10123456789101112

为了省事,我一股脑的把所有网格都列出来了,下面将介绍网格是怎么一步步生成的

  1. 首先网格是空的,j表示船的最大装载量,i表示第i个箱子的重量,m[i][j]就表示船可以装下的最大重量。
  2. 这里的网格是从上到下逐渐生成的,先是只有6号箱子,也就代表目前你只能装6号箱子到船上,所以只能是j>=6的时候才能放进去,第一行的值就是这么定下来的。
  3. 现在考虑5号箱子,也就是现在可以在船上放6号和5号箱子。所以j=5的时候可以放5号箱子了,j=11和12的时候可以把5号和6号都放进去。
  4. 同理,一直推理到最后一行最后一列,就得出了最终的结果。

3、状态转移方程

现在我们来提取一下里面的规律,假设现在i+1到n号箱子都已经考虑完了,现在该考虑第i号箱子了,这个时候有两种情况

  • 第i号箱子不放进去:那么 m [ i ] [ j ] = m [ i + 1 ] [ j ] m[i][j]=m[i+1][j] m[i][j]=m[i+1][j]
  • 第i号箱子放进去 m [ i ] [ j ] = m [ i + 1 ] [ j − a [ i ] ] + a [ i ] m[i][j]=m[i+1][j-a[i]]+a[i] m[i][j]=m[i+1][ja[i]]+a[i]
  • 综上 m [ i ] [ j ] = m a x ( m [ i + 1 ] [ j ] , m [ i + 1 ] [ j − a [ i ] ] + a [ i ] ) m[i][j]=max(m[i+1][j],m[i+1][j-a[i]]+a[i]) m[i][j]=max(m[i+1][j],m[i+1][ja[i]]+a[i])
    在这里插入图片描述

例如:求解m[3][9],它表示考虑3-6号箱子,可接受容量为9时的最大装载量

  • 3号箱子不放,那么相当于表示考虑4-6号箱子,可接受容量为9时的最大装载量
    m [ 3 ] [ 9 ] = m [ 4 ] [ 9 ] = 6 m[3][9]=m[4][9]=6 m[3][9]=m[4][9]=6
  • 3号箱子放,那么3号箱子先放进去,然后考虑4-6号箱子,可接受容量为(9-3)时的最大装载量
    m [ 3 ] [ 9 ] = m [ 4 ] [ 6 ] + a [ 3 ] = 9 m[3][9]=m[4][6]+a[3]=9 m[3][9]=m[4][6]+a[3]=9
  • 所以 m [ 3 ] [ 9 ] = 9 m[3][9]=9 m[3][9]=9

经典01背包问题

1、问题描述

给定 3 件物品,物品的重量为 weight[]={1,3,1},对应的价值为 value[]={15,30,20}。现挑选物品放入背包中,假定背包能承受的最大重量 W 为 4,问应该如何选择装入背包中的物品,使得装入背包中物品的总价值最大?

2、问题解决

令 dp[i][w] 表示前 i 件物品放入容量为 w 的背包中可获得的最大价值。为了方便处理,我们约定下标从 1 开始。初始时,网格如下:

dp[i][w]w01234
i
000000
10
20
30

根据状态转移方程 d p [ i ] [ k ] = m a x ( v a l u e [ i ] + d p [ i − 1 ] [ k − w e i g h t [ i ] ] , d p [ i − 1 ] [ k ] ) dp[i][k] = max(value[i] + dp[i-1][k-weight[i]], dp[i-1][k]) dp[i][k]=max(value[i]+dp[i1][kweight[i]],dp[i1][k])

3、代码实现

	public int maxValue(int[] weight, int[] value, int W) {
        int n = weight.length;
        if (n == 0) return 0;
        int[][] dp = new int[n][W + 1];
        // 先初始化第 0 行
        for (int k = 1; k <= W; k++) {
            if (k >= weight[0]) dp[0][k] = value[0];
        }
        for (int i = 1; i < n; i++) {
            for (int k = 1; k <= W; k++) {
                // 存放 i 号物品(前提是放得下这件物品)
                int valueWith_i = (k-weight[i] >= 0) ? (value[i] + dp[i-1][k-weight[i]]) : 0;
                // 不存放 i 号物品
                int valueWithout_i = dp[i-1][k];
                dp[i][k] = Math.max(valueWith_i, valueWithout_i);
            }
        }
        return dp[n-1][W];
    }

观察上面的代码,会发现,当**更新dp[i][…]时,只与dp[i-1][…]**有关,也就是说,我们没有必要使用O(n*W)的空间,而是只使用O(W)的空间即可。也就是说再更新新的一行的时候,直接将新的数值覆盖掉前面一行的内容即可

这里就会存在一个问题,就是我覆盖掉了前面一行的值会不会影响我后面的计算,因为后面的计算可能会用掉覆盖之前的旧值。

巧妙的是,在完全背包问题(物品可以重复取)中,正好可以利用这一点,正向遍历

观察状态转移方程就知道,我在计算dp[i][k]的时候我会用到上一行的dp[i-1][k]和dp[i-1][k-weight[i]],如果我是从左往右也就是k递增的顺序去覆盖的话,在计算到第k列的时候,我第k-weight[i]列的值已经覆盖掉了,所以这种顺序不行,只能从右往左也就是k递减的顺序去覆盖。

代码

    public int maxValue(int[] weight, int[] value, int W) {
        int n = weight.length;
        if (n == 0) return 0;int[] dp = new int[W + 1];
        for (int i = 0; i < n; i++) {
          //做了个优化:只要确保 k>=weight[i] 即可,而不是 k>=1,从而减少遍历的次数
            for (int k = W; k >= weight[i]; k--) {
                dp[k] = Math.max(dp[k - weight[i]] + value[i], dp[k]);
            }
        }
        return dp[W];
    }

其他案例

1、飞机座位分配概率(01背包问题)

题目

有 n 位乘客即将登机,飞机正好有 n 个座位。第一位乘客的票丢了,他随便选了一个座位坐下。

剩下的乘客将会:

  • 如果他们自己的座位还空着,就坐到自己的座位上,
  • 当他们自己的座位被占用时,随机选择其他座位

第 n 位乘客坐在自己的座位上的概率是多少?

示例 1:

输入:n = 1
输出:1.00000
解释:第一个人只会坐在自己的位置上。

示例 2:

输入: n = 2
输出: 0.50000
解释:在第一个人选好座位坐下后,第二个人坐在自己的座位上的概率是 0.5。

思路

我们定义原问题为 f(n)。对于第一个人来说,他有 n 中选择,就是分别选择 n 个座位中的一个。由于选择每个位置的概率是相同的,那么选择每个位置的概率应该都是 1 / n。

我们分三种情况来讨论:

  • 如果第一个人选择了第一个人的位置(也就是选择了自己的位置),那么剩下的人按照票上的座位做就好了,这种情况第 n 个人一定能做到自己的位置,这时答案是1
  • 如果第一个人选择了第 n 个人的位置,那么第 n 个人肯定坐不到自己的位置,这时答案是0
  • 如果第一个人选择了第 i (1 < i < n)个人的位置,那么第 i 个人就相当于变成了“票丢的人”,此时问题转化为 f(n - i + 1)。

运用动态规划的思想,假设现在前n-1个人的概率都已经算出来了,现在来算第n个人的概率,状态转移方程为 d p [ n ] = ( 1 + d p [ 2 ] + d p [ 3 ] + . . . + d p [ n − 1 ] + 0 ) / n dp[n]=(1+dp[2]+dp[3]+...+dp[n-1]+0)/n dp[n]=(1+dp[2]+dp[3]+...+dp[n1]+0)/n

代码

class Solution {
    public double nthPersonGetsNthSeat(int n) {
         if (n == 1) return 1.0;
        double [] dp=new double[n+1];//从下标1开始使用
        dp[2]=0.5;
        double sum_dp=1.5;
        for(int i=3;i<=n;i++){
            dp[i]=(1.0/(double)i)*sum_dp;
            sum_dp+=dp[i];
        }
        return dp[n];
    }
}

2、零钱兑换(完全背包问题)

题目

给定不同面额的硬币 coins 和一个总金额 amount。编写一个函数来计算可以凑成总金额所需的最少的硬币个数。如果没有任何一种硬币组合能组成总金额,返回 -1。

你可以认为每种硬币的数量是无限的。

示例 1:

输入:coins = [1, 2, 5], amount = 11
输出:3 
解释:11 = 5 + 5 + 1

示例 2:

输入:coins = [2], amount = 3
输出:-1

思路

首先,我们定义:

  • F ( S ) F(S) F(S):组成金额 SS 所需的最少硬币数量
  • [ c 0 … c n − 1 ] [c_{0} \ldots c_{n-1}] [c0cn1] :可选的 n 枚硬币面额值

假设我们知道 F(S) ,即组成金额 S 最少的硬币数,最后一枚硬币的面值是 C。那么由于问题的最优子结构,转移方程应为:

F ( S ) = F ( S − C ) + 1 F(S) = F(S - C) + 1 F(S)=F(SC)+1

但我们不知道最后一枚硬币的面值是多少,所以我们需要枚举每个硬币面额值 c 0 , c 1 … c n − 1 c_{0},c_{1} \ldots c_{n-1} c0,c1cn1,并选择其中的最小值。

假设在计算 F(i)之前,我们已经计算出 F(0)-F(i-1)的答案。 则 F(i)对应的转移方程应为
F ( i ) = m i n j ∈ [ 0 , n ] F ( i − c j ) + 1 F(i)=min_{j \in [0,n]} F(i-c_j)+1 F(i)=minj[0,n]F(icj)+1

代码

	public class Solution {
	    public int coinChange(int[] coins, int amount) {
	        int max = amount + 1;
	        int[] dp = new int[amount + 1];
	        Arrays.fill(dp, max);
	        dp[0] = 0;
	        for (int i = 1; i <= amount; i++) {
	            for (int j = 0; j < coins.length; j++) {
	                if (coins[j] <= i) {
	                    dp[i] = Math.min(dp[i], dp[i - coins[j]] + 1);
	                }
	            }
	        }
	        return dp[amount] > amount ? -1 : dp[amount];
	    }
	}
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值