什么情况用动态规划

  • 发现有重叠子问题的

动态规划阶梯套路

  • 写出动态状态转移方程

题目:求一组数字不相邻的数字和最大值

例如:3 3 4 1 9 3 6 6,最大值是 22

分析

求解动态规划的套路是把每一种情况都分为两种选择,要么选,要么不选。例如在 8 个数中求不相邻和的最大值,例如你选了序号8,就不能选7了。每一位就有两种情况,要么选,要么不选。拿序号8来说:
image.png
如果选:也就是如果选 8,那么最优解可能是 8 本身的值 6,加上左边 6 个数中的其中一个,7 不能选了。所以得出等式为:opt(8) = opt(6) + 6。

如果不选:不选的话就是从左边的7个中选两个,换句话说就是从 7 个数中求两和的最大值。所以可以得出等式为:opt(8) = opt(7)。

这里的 opt 是单词 optimal(最优)的缩写。

对于上面一位数字是这么分析的,其它的 7 位也是这样分析,每一位有两种情况,我们只要比较出这两种情况中最大的那个值就可以了。
image.png
(+表示选,-表示不选)
所以得出动态转移方程为:
选:opt(n) = opt(n-2) + arr[n-1]
不选:opt(n) = opt(n)

递归的出口:
当 n = 1 那么只能是 n 本身了;if (n == 1) return arr[n-1];
当 n = 2 那么可能是 1,或者是 2;if(n == 2) retur max(arr[n-1],arr[n-2])

递归求解

先尝试用递归的方式写:

  1. /**
  2. * @description:
  3. * 在一组数中,求不相邻之间的数最大和是多少
  4. * 例如:
  5. * 3 3 4 1 9 3 6 6
  6. * 最大值是 22
  7. * @date: 2020-06-16 02:32
  8. * @author: 十一
  9. */
  10. public class Sum {
  11. @Test
  12. public void testRecOpt() {
  13. int[] arr = {3,3,4,1,9,3,6,6};
  14. int i = arr.length;
  15. Sum sum = new Sum();
  16. int r = sum.recOpt(i, arr);
  17. System.out.println(r);
  18. Assert.assertEquals(22,r);
  19. int[] arr2 = {1,2,4,1,7,8,3};
  20. int i2 = arr2.length;
  21. r = sum.recOpt(i2, arr2);
  22. System.out.println(r);
  23. Assert.assertEquals(15,r);
  24. }
  25. /**
  26. * 递归求最优解
  27. * @param i 序号 - 1 = 数组下标
  28. * @param arr 数组
  29. * @return
  30. */
  31. public int recOpt(int i,int[] arr) {
  32. // 1. 递归先写退出条件
  33. if (i == 1) {
  34. return arr[i - 1];
  35. }
  36. if (i == 2) {
  37. return max(arr[i - 1], arr[i - 2]);
  38. }
  39. // 2. 选,就是左边隔一个的最优解 + 自己
  40. int a = recOpt(i - 2,arr) + arr[i - 1];
  41. // 3. 不选,就是左边的最优解
  42. int b = recOpt(i-1,arr);
  43. return max(a,b);
  44. }
  45. public int max(int a,int b) {
  46. return a > b ? a : b;
  47. }
  48. }

缺点:递归会重复的计算一些已经算过的,非常浪费!

动态规划求解

动态规划求解不会有很多重复,是因为用一个内存空间来保存了每个节点的值,也是空间换时间。

  1. @Test
  2. public void testDpOpt() {
  3. int[] arr = {3,3,4,1,9,3,6,6};
  4. Sum sum = new Sum();
  5. int r = sum.dpOpt(arr);
  6. System.out.println(r);
  7. Assert.assertEquals(22,r);
  8. int[] arr2 = {1,2,4,1,7,8,3};
  9. r = sum.dpOpt(arr2);
  10. System.out.println(r);
  11. Assert.assertEquals(15,r);
  12. }
  13. public int dpOpt(int[] arr) {
  14. // 保存每种情况的值
  15. int opt[] = new int[arr.length];
  16. // 这两种情况是已知的
  17. opt[0] = arr[0];
  18. opt[1] = max(arr[0],arr[1]);
  19. for (int i = 2; i < arr.length; i++) {
  20. int a = opt[i - 2] + arr[i];
  21. int b = opt[i - 1];
  22. opt[i] = max(a,b);
  23. }
  24. return opt[arr.length -1];
  25. }
  26. public int max(int a,int b) {
  27. return a > b ? a : b;
  28. }

题目:机器人走方格

有一个 m * n 的网格,机器人一次只能走一个格点,且只能向右或向下走,要从左上角走到右下角。请设计一个算法,计算机器人有多少种走法。
image.png

分析

机器人要么往下走,要么往右走,所以在坐标轴中可以看做,起点是(m,n)终点是(0,0),那么当 m or n = 1 时,这时只能往一个方向走,当 m or n > 1 时,这时对于每一个格子来说都有两种走法,往下或往右。

递归出口为:if(m == 1 || n == 1) return 1;

状态转移方程:f(m,n)=(m-1,n)+(m,n-1);

递归求解

  1. public int f1(int m, int n) {
  2. if (m == 1 || n == 1) {
  3. return 1;
  4. }
  5. return f1(m-1,n) + f1(m,n-1);
  6. }

动态规划

动态规划和递归类似,这里用数组来保存状态

  1. public int f2(int m, int n) {
  2. int[][] r = new int[m+1][n+1];
  3. r[0][0] = 0;
  4. for (int i = 1; i <= m; i++) {
  5. for (int i1 = 1; i1 <= n; i1++) {
  6. if (i1 - 1 == 0 || i - 1 == 0) {
  7. r[i][i1] = 1;
  8. continue;
  9. }
  10. r[i][i1] = r[i-1][i1] + r[i][i1-1];
  11. }
  12. }
  13. return r[m][n];
  14. }