动态规划总结及套路【4】

旧城等待, 2024-04-06 10:16 126阅读 0赞

动态规划总结及套路【4】

1 基本题目

1.1 较小集合的累加和

在这里插入图片描述

1.1.1 尝试方法
  1. /**
  2. * 给定一个数组arr,将arr分为两个集合,让两个集合的累加和尽量接近
  3. * @param arr
  4. * @return 返回较小集合的累加和
  5. */
  6. public static int right(int[] arr){
  7. if(arr == null || arr.length < 2){
  8. return 0;
  9. }
  10. int sum = 0;
  11. for(int num : arr){
  12. sum += num;
  13. }
  14. return process(arr, 0, sum / 2);
  15. }
  16. /**
  17. *
  18. * @param arr 所给集合arr
  19. * @param i 当前位置为i
  20. * @param rest 目标累加和rest
  21. * @return 返回arr[i...]累加和尽量接近rest的值
  22. */
  23. public static int process(int[] arr, int i, int rest){
  24. if(i == arr.length){
  25. //来到了数组末尾,没有数可选
  26. return 0;
  27. } else {
  28. //不选择arr[i]数
  29. int p1 = process(arr, i+1, rest);
  30. int p2 = -1;
  31. if(arr[i] <= rest){
  32. //选择了arr[i],累加和需要加上
  33. p2 = arr[i] + process(arr, i+1, rest - arr[i]);
  34. }
  35. return Math.max(p1, p2);
  36. }
  37. }
1.1.2 改为动态规划

一定要根据递归找出依赖

  1. //p2 = arr[i] + process(arr, i+1, rest - arr[i]);
  2. //i, rest位置上的值依赖i+1, rest - arr[i]的值
  3. public static int dp2(int[] arr){
  4. if(arr == null || arr.length < 2){
  5. return 0;
  6. }
  7. int sum = 0;
  8. for(int num : arr){
  9. sum += num;
  10. }
  11. int aim = sum / 2;
  12. int N = arr.length;
  13. //最接近N,可以取到N,因此长度为N+1
  14. //index 行
  15. //aim 列
  16. int[][] dp = new int[N+1][aim+1];
  17. //p2 = arr[i] + process(arr, i+1, rest - arr[i]);
  18. //i, rest位置上的值依赖i+1, rest - arr[i]的值
  19. //一定要根据递归找出依赖
  20. for(int index = N - 1; index >= 0; index--){
  21. for(int rest = aim; rest >= 0; rest--){
  22. //不选择index位置上的数
  23. int p1 = dp[index+1][rest];
  24. //选择index位置上的数
  25. int p2 = -1;
  26. if(arr[index] <= rest){
  27. p2 = arr[index] + dp[index+1][rest - arr[index]];
  28. }
  29. dp[index][rest] = Math.max(p1, p2);
  30. }
  31. }
  32. return dp[0][aim];
  33. }

1.2 较小集合的累加和2

在这里插入图片描述

1.2.1 尝试
  1. //返回较小集合的累加和
  2. //两个集合长度要一样多,如果arr的length为奇数,两个集合长度只差1
  3. public static int right(int[] arr) {
  4. if (arr == null || arr.length < 2) {
  5. return 0;
  6. }
  7. int sum = 0;
  8. for (int num : arr) {
  9. sum += num;
  10. }
  11. return Math.max(process(arr, 0, (arr.length + 1) >> 1, sum / 2), process(arr, 0, arr.length / 2, sum / 2));
  12. }
  13. /**
  14. *
  15. * @param arr
  16. * @param i 当前来到arr的位置
  17. * @param picks 目标集合长度
  18. * @param rest 累加和小于等于rest,最接近rest
  19. * @return arr[i....]自由选择,挑选的个数一定要是picks个,累加和<=rest, 离rest最近的返回
  20. */
  21. public static int process(int[] arr, int i, int picks, int rest) {
  22. if (i == arr.length) {
  23. //看看是否满足长度限制
  24. return picks == 0 ? 0 : -1;
  25. } else {
  26. int p1 = process(arr, i + 1, picks, rest);
  27. int p2 = -1;
  28. int next = -1;
  29. if (arr[i] <= rest) {
  30. next = process(arr, i + 1, picks - 1, rest - arr[i]);
  31. }
  32. if(next != -1){
  33. p2 = next + arr[i];
  34. }
  35. return Math.max(p1, p2);
  36. }
  37. }
1.2.2 动态规划
  1. public static int dp3(int[] arr) {
  2. if (arr == null || arr.length < 2) {
  3. return 0;
  4. }
  5. int sum = 0;
  6. for (int num : arr) {
  7. sum += num;
  8. }
  9. int N = arr.length;
  10. int aim = sum / 2;
  11. //集合长度
  12. int M = (N + 1) / 2;
  13. //X:index
  14. //M:集合长度 > picks
  15. //aim:rest
  16. int[][][] dp = new int[N + 1][M + 1][aim + 1];
  17. //初始化全部置为:-1,无效值
  18. for (int i = 0; i <= N; i++) {
  19. for (int j = 0; j <= M; j++) {
  20. for (int k = 0; k <= aim; k++) {
  21. dp[i][j][k] = -1;
  22. }
  23. }
  24. }
  25. //根据递归填写对应值:i==arr.length > picks == 0 ? 0 : -1
  26. for (int rest = 0; rest <= aim; rest++) {
  27. dp[N][0][rest] = 0;
  28. }
  29. //index[根据递归可以知道每层依赖下一层的值,因此需要从最底层开始填]
  30. for (int i = N - 1; i >= 0; i--) {
  31. //pick
  32. for (int pick = 0; pick <= M; pick++) {
  33. //rest
  34. for (int rest = 0; rest <= aim; rest++) {
  35. //
  36. int p1 = dp[i + 1][pick][rest];
  37. int p2 = -1;
  38. int next = -1;
  39. if (pick - 1 >= 0 && arr[i] <= rest) {
  40. next = dp[i + 1][pick - 1][rest - arr[i]];
  41. }
  42. if (next != -1) {
  43. p2 = next + arr[i];
  44. }
  45. dp[i][pick][rest] = Math.max(p1, p2);
  46. }
  47. }
  48. }
  49. //如果arr长度为奇数,看是选择奇数个集合还是长度为偶数的集合
  50. if ((arr.length & 1) == 0) {
  51. //arr长度为偶数
  52. return dp[0][arr.length / 2][aim];
  53. } else {
  54. return Math.max(dp[0][(arr.length + 1) / 2][aim], dp[0][arr.length / 2][aim]);
  55. }
  56. }

1.3 N皇后问题

在这里插入图片描述

1.3.1 尝试
  1. public static int num1(int n) {
  2. if (n < 1) {
  3. return 0;
  4. }
  5. int[] record = new int[n];
  6. return process1(0, record, n);
  7. }
  8. //当前来到i行,一共是0~N-1行
  9. //在i行上放皇后,所有列都尝试
  10. //必须要保证跟之前所有的皇后都不打架
  11. //int[] record record[x] = y 之前的第x行的皇后,放在了y列行
  12. //返回:不关心i以上发生了什么,i...后续有多少合法的方法数
  13. public static int process1(int i, int[] record, int n) {
  14. if (i == n) {
  15. return 1;
  16. }
  17. int res = 0;
  18. //i行的皇后,放在哪一列呢?j列
  19. for (int j = 0; j < n; j++) {
  20. if (isValid(record, i, j)) {
  21. record[i] = j;
  22. res += process1(i + 1, record, n);
  23. }
  24. }
  25. return res;
  26. }
  27. //皇后放在i行j列
  28. public static boolean isValid(int[] record, int i, int j) {
  29. //0..i-1
  30. for (int k = 0; k < i; k++) {
  31. // j == record[k] 判断列是否合法
  32. // Math.abs(record[k] - j) == Math.abs(i - k) 是否在同一条斜线上
  33. if (j == record[k] || Math.abs(record[k] - j) == Math.abs(i - k)) {
  34. return false;
  35. }
  36. }
  37. return true;
  38. }
1.3.2 常数时间优化【列影响、左下、右下影响】
  1. //请不要超过32皇后问题
  2. public static int num2(int n){
  3. if(n < 1 || n > 32){
  4. return 0;
  5. }
  6. //如果是13皇后的问题,limit最右13个1,其他都是0
  7. int limit = n == 32 ? -1 : (1 << n) - 1;
  8. return process2(limit, 0, 0, 0);
  9. }
  10. //7皇后问题
  11. //limit: 0....0 1 1 1 1 1 1 1
  12. //之前皇后的列影响:colLim
  13. //之前皇后的左下对角线影响:leftDiaLim
  14. //之前皇后的右下对角线影响:rightDiaLim
  15. public static int process2(int limit, int colLim, int leftDiaLim, int rightDiaLim){
  16. if(colLim == limit){
  17. return 1;
  18. }
  19. //pos中所有是1的位置,是可以去尝试放置皇后的位置
  20. int pos = limit & (~(colLim | leftDiaLim | rightDiaLim));
  21. int mostRightOne = 0;
  22. int res = 0;
  23. while(pos != 0){
  24. //每次取出最右侧的1【先打散再聚合再&】
  25. mostRightOne = pos & (~pos + 1);
  26. pos = pos - mostRightOne;
  27. res += process2(limit, colLim | mostRightOne, (leftDiaLim | mostRightOne) << 1,
  28. (rightDiaLim | mostRightOne) >>> 1);
  29. }
  30. return res;
  31. }

2 动态规划总结

我们常见的动态规划,都可以通过暴力递归修改得到。

  • 什么暴力递归可以继续优化?

有重复调用同一个子问题的解,这个递归可以优化
如果每一个子问题都是不同的解,无法优化也不用优化

  • 暴力递归与动态规划关系

某个暴力递归,有解的重复调用,就可以把这个暴力递归优化成动态规划
任何动态规划问题,都一定对应着某一个有重复过程的暴力递归
但是不是所有的暴力递归,都一定对应着动态规划【动态规划是暴力递归的子集】

  • 面试题与动态规划的关系

解决一个问题,可能有很多尝试方法
可能在很多尝试中,又有若干个尝试方法有动态规划的方式
一个问题 可能有 若干种动态规划的解法

  • 如何找到某个问题的动态规划方式?
  1. 设计暴力递归:重要原则+4种常见模型!重点!!!

  2. 分析有没有重复解:套路解决

  3. 用记忆化搜索 -> 用严格表结构实现动态规划:套路解决
  4. 看看能够继续优化:套路解决
  • **面试中设计暴力递归过程的原则**
  1. 每一个可变参数的类型,一定不要比int类型更加复杂
  2. 原则1可以违反,让类型突破到一维线性结构,但必须是单一可变参数【String、数组…】
  3. 如果发现原则1被违反,但不违反原则2,只需要做到记忆化搜索即可
  4. 可变参数的个数,能少则少【两个可变参数就是2维,三个可变参数就是3维】
  • 知道了面试中设计暴力递归过程的原则,然后呢?

我们一定要逼自己找到不违反原则情况下的暴力尝试!
如果我们找到的暴力尝试,不符合原则,马上舍弃,寻找新的!!
如果某个题目突破了设计原则,那么该题一定极难,在面试中出现概率低于5%!

  • 常见的4种尝试模型
  1. 从左往右的尝试模型:背包问题
  2. 范围上的尝试模型
  3. 多样本对应的尝试模型【多样本位置全对应】
  4. 寻找业务限制的尝试模型
  • 如何分析有没有重复解

列出递归调用过程,可以只列出前几层
有没有重复解,一看就可以知道

  • 暴力递归到动态规划的套路
  1. 你已经有了一个不违反原则的暴力递归,而且的确存在解的重复调用
  2. 找到哪些参数的变化会影响返回值,对每一个列列出变化范围
  3. 参数间的所有组合数量,意味着表大小
  4. 记忆化搜索的方法就是傻缓存,非常容易得到
  5. 规定好严格表的大小,分析位置的依赖顺序,然后从基础填写到最终解【base case】
  6. 对于有枚举行为的决策过程,进一步优化
  • 动态规划的进一步优化

1) 空间压缩
2) 状态化简
3) 四边形不等式
4) 其他优化技巧

发表评论

表情:
评论列表 (有 0 条评论,126人围观)

还没有评论,来说两句吧...

相关阅读

    相关 动态规划总结

    本文转载自:[风中之炎 不盛则灭][Link 1] 本文着重讨论状态是如何表示,以及方程是怎样表示的。当然,还附上关键的,有可能作为模板的代码段。但有的代码的实现是