算法必备—动态规划详解,从递归到动态规划

动态规划

动态规划作为经典的算法,在使用上现在十分广泛,机器人走路问题、背包问题、八皇后问题,可以说是用的地方十分广泛。

动态规划从简单的地方说起,最初的时候可以理解为循环问题,就是常说的递归,我们仔细的想一想,如果一个问题能够使用递归得到解,那么只要在递归过程中有重复的计算,都可以换做动态规划来解决。

首先了解动态规划是什么?

就是将大问题,分解为小问题,然后一步步的解决它,很类似于分治算法,但是用很不相同。

再次了解递归的要点?

递归必须右函数出口,否则会死递归下去

递归过程必须是要向着出口的条件逼近,否则也会是死递归过程

可以将递归理解为栈结构,当主递归进去后,就是方法入栈,一直递归调用,知道最后一个递归方法全部进栈,开始出栈回溯。

例子:

1.机器人走路问题

假设有一个直线,可以使用一个数组表示,机器人每次可以向左或者向右移动一个位置,在规定的移动次数下,有多少种方法可以到达指定位置?

输入:N S A K
N: 这条路一共有多少位置可以移动
S: 机器人初始位置
A: 需要到达的目的地
K: 可以移动多少步
输出:ANS
ANS: 共有多少种移动方法
解法1:递归法

我们假设机器人当前所在的位置为 C,还可以移动M次,那么接下来的一步,机器人要么去到左边,要么去到右边

1 2 3 4 5 6
  👆   👆
  C     A
剩余步数 K: 4

那么机器人接下来,要么向左移动到1位置,剩余步数为3,目的地不变;要么向右移动到3位置,剩余步数为3,目的地不变。那么就出现了递归调用。

因为每次机器人移动时候所要考虑的就是两件事情,

1.还有没有可以动的步数

2.往左还是往右

所以递归的方法体就可以出来了

/**
     * 机器人可以到达的方式
     * @param N 有多少可以走的位置
     * @param start 开始的位置
     * @param aim 到达的目的
     * @param K 剩余步数
     * @return 所有的组合
     */
    public static int ways(int N, int start, int aim, int K){
        
    }

参数选定了,就考虑方法体的具体细节

注意1: 如果机器人走到了边界,也就是1,或者6的位置,他就只能往反方向走

注意2: 方法的出口就是,当机器人可移动的步数为0之后,所在的位置,是否是目的地

那么递归方法体就是:

/**
     * 机器人可以到达的方式
     * @param N 有多少可以走的位置
     * @param start 开始的位置
     * @param aim 到达的目的
     * @param K 剩余步数
     * @return 所有的组合
*/
public static int process(int N, int start, int aim, int K){
  // 递归终止条件
  if (K == 0){
    return start == aim ? 1: 0;
  }
  // 如果在起点,只能往右走
  if (start == 0){
    return process(N, start+1, aim, K-1);
  }
  // 如果在终点,只能往左走
  if (start == N){
    return process(N, start-1, aim, K-1);
  }
  // 否则,往右往左都试一试
  return process(N, start-1, aim, K-1) + process(N, start+1, aim, K-1);
}

调用测试:

@Test
public void test1(){
  // 假如路径为下
  // 1 2 3 4 5 6
  // 初始在位置2,目的地达到6, 共可移动6步
  System.out.println(ways(4, 2, 4, 4));
}

结果:

3
解法2:动态规划

所谓的动态规划,可以理解为 把每次计算的过程都记录下来,再次使用到这次计算时候,可以直接在记录数据中拿到,而不需要再次计算了。

上述的递归解法中,因为在递归调用的过程中,会存在重复计算的过程,所以使用一个缓存表,记录每次的结果。

上述例子中,一直在变动的参数有两个

  1. 当前的位置
  2. 剩余可移动的步数

所以可以实例一个二维数组,记录 在 cur位置时候,剩余 step步数时候,返回的结果是什么样的?

因为机器人是从1开始的,为了迎合,所以二维数组多实例一行一列,初始值设为-1,如果是-1,就认为该递归方法体未被调用过

int[][] dp = new int[N+1][K+1];
for (int i = 0; i < N+1; i++) {
  for (int j = 0; j < K+1; j++) {
    dp[i][j] = -1;
  }
}

那么在每次调用的时候,方法中都夹带着这个缓存表,如果调用的方法已经被计算过了,就从缓存表里拿结果,如果没有,就计算。

那么就需要在开始加一个判断,是否该次计算已经执行过,也就是缓存表相对位置为-1时候,就是未被执行

/**
     *
     * 机器人可以到达的方式
     * @param N 有多少可以走的位置
     * @param start 开始的位置
     * @param aim 到达的目的
     * @param K 剩余步数
     * @return 所有的组合
*/
public static int process2(int N, int start, int aim, int K, int[][] dp){

  // 作为缓存,把每次[start][K]都放进去
  if (dp[start][K] != -1){
    return dp[start][K];
  }
  // 记录本次结果,因为要放进去缓冲表
  int ans = 0;
  // 就是正经的递归调用了
  // 递归终止条件
  if (K == 0){
    ans =  (start == aim ? 1: 0);
  }else if (start == 1){
    ans = process2(N, start+1, aim, K-1, dp);
  }else if (start == N){
    ans = process2(N, start-1, aim, K-1, dp);
  }else{
    ans = process2(N, start-1, aim, K-1, dp) + process2(N, start+1, aim, K-1, dp);
  }
  dp[start][K] = ans;
  return ans;
}

测试:

@Test
public void test1(){
  // 1 2 3 4 5 6
  // 在位置2,达到6, 共6步
  System.out.println(ways(4, 2, 4, 4));
}

public static int ways(int N, int start, int aim, int K){
  int[][] dp = new int[N+1][K+1];
  for (int i = 0; i < N+1; i++) {
    for (int j = 0; j < K+1; j++) {
      dp[i][j] = -1;
    }
  }
  return process2(N, start, aim, K, dp);
}
2.背包问题

假设有一个背包,容量为k,有n个物品,其重量和价值为w[i],v[i],把东西放进背包,如何拿到最有价值的组合

解法1:暴力递归法

同样,先考虑暴力递归的问题

假如我们需要把所有的组合都算一遍,那么先定义一下参数都有哪些?

可选的物品重量,可选的物品价值,背包容量

只有三个,那么主函数的参数列表就有了:

/**
     *
     * @param weight 物品重量
     * @param value 物品价值
     * @param capacity 背包容量
     * @return
*/
public static int bag(int[] weight, int[] value, int capacity){

}

那递归时候怎么做呢?

首先思考函数出口在哪里?

  1. 因为我们是递归选择物品,那么,如果选择到了最后一个商品,就没有东西选择了,直接返回0
  2. 如果背包容量没了,也可以作为函数出口

出口选择好了,那么我们需要构思递归方法体,按照特点,递归调用要朝着出口去

那么我们在方法体中增加一个index,认为是递归选择到了当前的物品,那么参数也有了

/**
     * 背包问题,当前考虑index号货物,index后所有货物都可以选择
     * @param weight 可以拿的物品重量
     * @param value 可以拿的物品价值
     * @param cap 背包剩余容量
     * @return
*/
public static int maxValue(int[] weight, int[] value, int index, int cap){
  // 函数出口
  if (cap < 0){
    return -1;
  }
  if (index == weight.length){
    return 0;
  }
  
  // 函数体
}

那么考虑方法体,在到了一个物品时候,有两种选择

  1. 选择该商品,减背包,加价值
  2. 不选择该商品,背包和价值不变,之间index+1

那么方法体就有了

/**
     * 背包问题,当前考虑index号货物,index后所有货物都可以选择
     * @param weight 可以拿的物品重量
     * @param value 可以拿的物品价值
     * @param cap 背包剩余容量
     * @return
*/
public static int maxValue(int[] weight, int[] value, int index, int cap){
  if (cap < 0){
    return -1;
  }
  if (index == weight.length){
    return 0;
  }
  // 有货,index位置有货
  // 两种选择,要 / 不要
  // 不选择当前商品
  int p1 = maxValue(weight, value, index+1, cap);
  // 选择当前商品
  int m = maxValue(weight, value, index+1, cap-weight[index]);
  int p2 = 0;
  // 如果选择后背包变成了负数,也就是背包不能选择这个商品了,那么不做处理
  if (m != -1) {
    p2 = value[index] + m;
  }
  return Math.max(p1, p2);
}

测试:

@Test
public void test2(){
  int[] w = {5, 2, 3, 6, 7};
  int[] v = {10, 5, 8, 2, 6};
  System.out.println(bag(w, v, 15));
}
2.动态规划

同样因为在上述递归过程中,有重复的计算问题,所以我们用一个缓存表,来存放每次的递归结果,那么这张缓存表该怎么设置?

因为在方法递归时候,只有两个参数是变换的

  1. 背包的剩余容量
  2. 当前的选择物品

确定了之后,那么改为动态规划,就是变成缓存表

int[][] dp = new int[weight.length+1][capacity+1];
for (int i = 0; i < weight.length + 1; i ++){
  for (int j = 0; j < capacity + 1; j ++){
    dp[i][j] = -2;
  }
}

写入缓存表时候:

/**
     * 背包问题,当前考虑index号货物,index后所有货物都可以选择
     * @param weight 可以拿的物品重量
     * @param value 可以拿的物品价值
     * @param cap 背包剩余容量
     * @return
*/
public static int maxValue2(int[] weight, int[] value, int index, int cap, int[][] dp){
  if (cap < 0){
    return -1;
  }
  if (index == weight.length){
    return 0;
  }
  if (dp[index][cap] != -2){
    return dp[index][cap];
  }
  int ans = 0;
  // 有货,index位置有货
  // 两种选择,要 / 不要
  int p1 = maxValue2(weight, value, index+1, cap, dp);
  int m = maxValue2(weight, value, index+1, cap-weight[index], dp);
  int p2 = 0;
  if (m != -1) {
    p2 = value[index] + m;
  }
  ans = Math.max(p1, p2);
  dp[index][cap] = ans;
  return ans;
}

测试:

@Test
public void test2(){
  int[] w = {5, 2, 3, 6, 7};
  int[] v = {10, 5, 8, 2, 6};
  System.out.println(bag(w, v, 15));
}


/**
     *
     * @param weight 物品重量
     * @param value 物品价值
     * @param capacity 背包容量
     * @return
*/
public static int bag(int[] weight, int[] value, int capacity){

  if (weight == null || weight == null || weight.length != value.length || weight.length == 0){
    return 0;
  }
  int[][] dp = new int[weight.length+1][capacity+1];
  for (int i = 0; i < weight.length + 1; i ++){
    for (int j = 0; j < capacity + 1; j ++){
      dp[i][j] = -2;
    }
  }

  // 尝试函数
  return maxValue2(weight, value, 0, capacity, dp);
}
  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

牧码文

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值