动态规划-走台阶和国王与金矿问题

本文深入探讨了动态规划解决经典问题的方法,包括走台阶问题和国王与金矿问题。通过分析最优子结构、边界条件和状态转移公式,文章提供了备忘录算法和迭代算法的实现代码,帮助读者理解动态规划的精髓。

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

(1)、走台阶

有一座N级的台阶,从下往上走,每次走一个或两个台阶,那么走上N阶总共需有多少种方法?

考虑:假设台阶一共10级,那么只差最后一步就走到10级台阶,这时有几种情况?

有两种:第一种,从第9级到第10级;第二种从第8级到第10级。那么现在想想,不管之前0级-9级或者0级-8级走台阶的过程,最后一步肯定是从第9级或者第8级台阶走,假设从0级-8级台阶的走法有X种,0级-9级台阶的走法有Y种,那么从0级-10级台阶有多少种?

分析:10级的台阶走法可以根据最后一步的不同而分为两个部分:第一部分就是最后一步从第9级到第10级,这一部分的走法数量和0级-9级的走法数量是一样的;第二部分最后一步就是从第8级到第10级,它的走法数量和0级-8级是一样的,那么就有如下的关系:X+Y  => F(10) = F(9) + F(8)。将思路画出来,如下图(借鉴小灰图):

                          

那么我们可以推出递归公式:

F(n) = F(n-1) + F(n-2),边界条件:F(1) = 1; F(2) = 2

动态规划中包含三个重要的概念:最优子结构、边界和状态转移公式。

下面使用两种方式来实现这个动态规划问题,(由于递归比较简单,这里就不实现了)

1、使用备忘录算法,将计算的中间结果保存在一个HashMap中。若第一次出现的计算值,写入HashMap中;若出现多次,那么直接从map中取出即可,避免重复计算,代码如下:

/**
 *  使用备忘录算法求解
 *  为了避免重复运算,使用一个Map来计算中间结果,若第一次出现则存入Map;
 *  若后面多次出现,那么就直接从Map中取出来,避免重复计算。
 * @param n
 * @return
*/
public static int getClimbingWays(int n, HashMap<Integer, Integer> recordMap){
   // 判断边界条件
   if(n < 0){
      return 0;
   }
   if(n == 1){
      return 1;
   }
   if(n == 2){
      return 2;
   }

   if(recordMap.containsKey(n)){
       return recordMap.get(n);
   }else{
       int value = getClimbingWays(n - 1, recordMap) + getClimbingWays(n - 2, recordMap);
       recordMap.put(n , value);
       return value;
   }
}

2、使用迭代的方式

通过分析我们发现,F(n)的值只和前两个值有关,F(n-1)和F(n-2),那么我们只需要保存前两个值就可以得到最新的值。代码如下:

/**
 *   分析:
 *   台阶数: 1    2    3    4   ...
 *   走法数: 1    2    3    5   ...
 *   台阶个数1和2的走法数是边界值,台阶数3的走法数,是1和2的和;
 *   而台阶数4的走法数是2和3的和,台阶数3的走法数只依赖1和2;
 *   台阶数4的走法数只依赖2和3。每次迭代的过程中,只依赖前两个状态,
 *   就可以得到最新的状态,因此只需保留前两次的状态即可。
 */
 public static int getClimbingWays(int n){
    if(n < 0 ) {
        return 0;
    }
    if(n == 1){
        return 1;
    }
    if(n == 2){
        return 2;
    }
    // 迭代
    // 前两个值
    int a = 1;
    int b = 2;
    // 当前值
    int current = 0;
    for (int i = 3; i <= n ; i++) {
        current = a + b;
        a = b;
        b = current ;
    }
    return current;
 }

(2)、国王和金矿

题目:有一个国家发现了5座金矿,每座金矿的黄金储备量不同,需要参与的挖掘的工人数也不同。参与挖掘工人总数是10人。每座金矿要么全挖要么不挖,不能派出一半人挖取一半金矿。求解要得到尽可能多的黄金,应该挖取哪几座金矿?比如5座金矿总量和用工数如下:400金/5人,500金/5人,200金/3人,300金/4人,350金/3人。

分析:10人5金矿的最优子结构是10人4金矿时对应的最优子结构。10人4金矿最优结果有两种可能,一种是:第5座金矿不挖,只挖前4座金矿;还有就是第5座金矿挖,那么前4座金矿所需的人数就是10-第5座金矿的人数。那么4座金矿的最优选择对应5座金矿的最优选择就是:前4座金矿10人挖金数量与前4座挖金数量+第5座金矿挖金数量中的最大值,如下图所示(借鉴小灰图):

              

假设金矿数量N,工人数W,金矿的黄金量为G[],金矿用工数P[],通过上述分析,我们可以得知5金矿和4金矿的最优选择存在如下关系:

                                 F(5, 10) = MAX(F(4, 10) , F(4, 10 - P[4]) + G[4])

上述就是状态转移方程,那么边界就是:

当 N = 1,W >= P[0] 时, F(N, W) = G[0];

当 N=1, W >= P[0] 时, F(N, W) = G[0] 当N=1,W < P[0]时,F(N, W) = 0 N = 1,W < P[0] 时, F(N, W) = 0

通过整理可得如下状态转移方程:

                    \\ F(n, w) = 0, (n <= 1, w < p[0]); \\ F(n,w) = g[0],(n == 1, w >= p[0]); \\ F(n,w) = F(n-1, w),(n > 1, w < p[n-1]); \\ F(n, w) = max(F(n-1, w), F(n-1, w - p[n-1]) + g[n-1]), (n > 1, w >= p[n-1])

借助如下表格帮助写出迭代方式的代码(拷贝自小灰图):

表格第一行是工人数,第二行是金矿数。第一座金矿数是400金/5人,那么可以填写第一行的值:

同理第二行,注意最后一个值的填写, F(2,10) = max(F(1, 10),   F(1, 10 - 5) + G[1])  => F(2,10) = max(400,  400 + 500) = 900,因此是900。

同理可以得到如下所以的数量信息:

找规律:比如第3座金矿8工人的结果,就是2金矿5工人和 2金矿8工人来的 F(3, 8) = max( F(2, 8), F(2, 5) + G[2])

可以看出我们只需要存储前一行的结果就可以得到下一行的结果,而不需要存储前面的所有表格。

    /**
     * @param n   金矿数
     * @param w   工人数
     * @param g   每个金矿对应的黄金数
     * @param p   每个金矿对应的用工数
     * @return
     */
    public static int getMostGold(int n, int w, int[] g, int[] p){
        // 存储前一次的结果
        int[] preResult = new int[p.length];
        // 存储当前结果
        int[] results = new int[p.length];
        // 填充边界,表格第一行结果
        for (int i = 0; i <= w ; i++) {
            if(i < p[0]){
                preResult[i] = 0;
            }else{
                preResult[i] = p[0];
            }
        }

        // 填充其余的格子
        // 外层循环是金矿数量,内存循环是工人数
        for (int i = 0; i < n; i++) {
            for (int j = 0; j <= w ; j++) {
                if(j < p[i]){
                    results[j] = preResult[j];
                }else{
                    // 根据迭代公式
                    results[j] = Math.max(preResult[j], preResult[j-p[i]] + g[i]);
                }
            }
            preResult = results;
        }
        return results[n];
    }

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值