暴力递归到动态规划

文章介绍了如何将暴力递归优化为动态规划来解决一个机器人在指定步数内到达特定位置的问题。通过分析递归过程中的重复计算,使用缓存矩阵存储中间结果,减少了计算量。最终进一步优化为纯粹的动态规划方法,通过填表方式避免了递归,提高了效率。

暴力递归到动态规划

假设有排成一行的n个位置, 记为1~n,n-定大于或等于2。开始时机器人在其中的m位置上(m 一定是1~n中的一个)。如果机器人来到1位置,那么下一步只能往右来到2位置;如果机器人来到n位置, 那么下一步只能往左来到n-1位置;如果机器人来到中间位置,那么下一步可以往左走或者往右走;规定机器人必须走k步,最终能来到p位置(p也是1~n中的一个)的方法有多少种?给定四个参数n、m、k、p,返回方法数。

暴力递归
public static int robot(int n, int m, int k, int p){
   // 无效参数的情况
   if (n < 2 || m < 1 || m > n || k < 1 || p < 1 || p > n)
      return 0;
   return walk(n, m, k, p);
}

// n 还是表示一共n个位置,p 还是表示目标位置
// cur 表示当前位置,rest表示还能走几步
private static int walk(int n, int cur, int rest, int p) {
   // 如果没有剩余步数了,当前的cur位置就是最后的位置
   // 如果最后的位置停在P上,那么之前做的移动是有效的
   // 如果最后的位置没在P上,那么之前做的移动是无效的
   if (rest == 0)
      return cur == p ? 1 : 0;
   if (cur == 1)
      return walk(n, cur + 1, rest - 1, p);
   if (cur == n)
      return walk(n, cur - 1, rest - 1, p);
   // 如果还有rest步要走,而当前的cur位置在中间位置上,那么当前这步可以走向左,也可以走右
   // 走向左之后,后续的过程就是,来到cur-1位置 上,还剩rest-1步要走
   // 走向右之后,后续的过程就是,来到cur+1位置. 上,还剩rest-1步要走
   // 走向左、走向右是截然不同的方法,所以总方法数要都算上
   return walk(n, cur - 1, rest - 1, p) + walk(n, cur + 1, rest - 1, p);
}

这种解法是最纯粹的暴力递归,有一些是重复计算。可以发现递归时只有两个参数对结果有实际影响

当前位置 剩余步数 ,如果将这两个参数的取值组成一张矩阵,计算好的数据存在矩阵中,当碰到有重复计算时只需要取值即可。

半动态规划
// 上述的这种暴力递归方法是有重复计算的。可以看出递归中n、p两个参数是固定不变的,结果只取决于(m,k)的组合,如果有
// 一个cache存放各种组合的结果,当重复计算时只需要从cache中返回结果。
public static int robotCache(int n, int m, int k, int p){
   // 无效参数的情况
   if (n < 2 || m < 1 || m > n || k < 1 || p < 1 || p > n)
      return 0;
   int[][] cache = new int[n + 1][k + 1];
   // 默认将cache所有元素都设为-1,表示从来没计算过,当递归访问某个元素时发现不是-1时说明已经计算过了,直接取值即可
   for (int[] ints : cache) Arrays.fill(ints, -1);

   return walkCache(n, m, k, p, cache);
}

// 此时,所有的递归都要带上cache这张表一起玩
private static int walkCache(int n, int cur, int rest, int p, int[][] cache) {
   if (cache[cur][rest] != -1)
      return cache[cur][rest];
   if (rest == 0){
      cache[cur][rest] = cur == p ? 1 : 0;
      return cache[cur][rest];
   }
   if (cur == 1){
      cache[cur][rest] = walkCache(n, cur + 1, rest - 1, p, cache);
      return cache[cur][rest];
   }
   if (cur == n){
      cache[cur][rest] = walkCache(n, cur - 1, rest - 1, p, cache);
      return cache[cur][rest];
   }
   // 在中间位置
   cache[cur][rest] = walkCache(n, cur - 1, rest - 1, p, cache) +
      walkCache(n, cur + 1, rest - 1, p, cache);
   return cache[cur][rest];
}

通过分析发现,当cur=1cur=1cur=1 时,依赖 cache[cur+1][rest−1]cache[cur+1][rest-1]cache[cur+1][rest1] 的值;当 cur=ncur=ncur=n 时,依赖 cache[cur−1][rest−1]cache[cur-1][rest-1]cache[cur1][rest1] 的值;当cur不在首尾时,依赖 cache[cur−1][rest−1]cache[cur-1][rest-1]cache[cur1][rest1]cache[cur+1][rest−1]cache[cur+1][rest-1]cache[cur+1][rest1] 。没有cur=0cur=0cur=0 的情况,虽然cache容量为(N+1)×(K+1)(N+1) \times (K+1)(N+1)×(K+1) ,但那是为了方便运算而已。初始情况下,rest=0rest=0rest=0,如果cur≠pcur \neq pcur=p 则为0,否则为1。假设目标位置p=3p=3p=3 如下图所示:

在这里插入图片描述

如果确定了这种依赖关系后,直接填表就好了,连递归都省了。

纯粹动态规划
public static int dp(int n, int m, int k, int p){
   // 无效参数的情况
   if (n < 2 || m < 1 || m > n || k < 1 || p < 1 || p > n)
      return 0;
   int[][] cache = new int[n + 1][k + 1];

   cache[p][0] = 1;
   // 先填列再填行
   for (int col = 1; col < cache[0].length; col++) {
      for (int row = 1; row < cache.length; row++) {
         if (row == 1)
            cache[row][col] = cache[row + 1][col - 1];
         else if (row == cache.length - 1)
            cache[row][col] = cache[row - 1][col - 1];
         else
            cache[row][col] = cache[row - 1][col - 1] + cache[row + 1][col - 1];
      }
   }
   return cache[m][p];
}
### Java 实现从暴力递归动态规划的优化 #### 暴力递归实现 对于某些问题,如零钱兑换问题,在最初阶段可以采用暴力递归的方法解决。然而这种方法存在大量重复计算的问题,效率低下。 ```java public int coinChange(int[] coins, int amount) { if (amount < 0) { return -1; } if (amount == 0) { return 0; } int minCount = Integer.MAX_VALUE; for (int i = 0; i < coins.length; i++) { int res = coinChange(coins, amount - coins[i]); if (res >= 0 && res < minCount) { minCount = 1 + res; } } return minCount == Integer.MAX_VALUE ? -1 : minCount; } ``` 此代码展示了最基础的递归逻辑[^1]。当金额 `amount` 小于零返回 `-1` 表示无法组合;等于零表示正好匹配成功,不需要任何硬币参与运算。遍历每种面额尝试减少当前总金额并递归求解剩余部分所需的最少数量直到找到最小值或者确认无解为止。 #### 添加记忆化搜索(Memoization) 为了改善上述算法性能,可以在原有基础上引入缓存机制保存已经计算过的结果防止不必要的重复工作: ```java private static final Map<Integer, Integer> memo = new HashMap<>(); public int coinChangeWithMemo(int[] coins, int amount) { if (amount < 0) { return -1; } if (amount == 0) { return 0; } // Check cache first before computing. if (!memo.containsKey(amount)) { int minCount = Integer.MAX_VALUE; for (int i = 0; i < coins.length; ++i) { int res = coinChangeWithMemo(coins, amount - coins[i]); if (res >= 0 && res < minCount) { minCount = 1 + res; } } memo.put(amount, (minCount == Integer.MAX_VALUE) ? -1 : minCount); } return memo.get(amount); } ``` 这段改进后的版本利用哈希表作为外部存储器记录之前遇到过的输入及其对应输出以便快速检索已知答案从而大大减少了时间复杂度[^3]。 #### 完全转换成自底向上的动态规划方案 最终目标是从完全依赖函数调用来构建表格形式的数据结构逐步填充直至获得全局最优解的过程称为“自底向上”。这种方式不仅消除了显式的栈空间消耗还进一步提高了程序运行速度。 ```java public int dpCoinChange(int[] coins, int amount) { int max = amount + 1; int[] dp = new int[max]; 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]; } ``` 这里定义了一个长度为 `max` 的数组 `dp` 来保存不同数额所需最少硬币数目,并初始化所有元素为最大整数值代表未知状态。接着按照从小到大顺序更新每一个位置处的最佳选择结果直到处理完毕整个范围内的数据项。
评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值