数据结构算法刷题--动态规划

1. 动态规划理论基础

  • 动态规划与贪心:动态规划是由前一个状态推导出来;贪心是局部选最优;
  • 动态规划五部曲:
    • 确定 dp 数组以及下标的含义;
    • 确定递推公式;
    • 确定 dp 数组如何初始化;
    • 确定遍历顺序;
    • 举例推导 dp 数组;
  • 动态规划 debug:如果写出的代码没通过,先打印出来 dp 数组,看和自己的预期是不是一样,如果不一样那就是代码实现有问题;如果和预期一样,那就是 dp 数组初始化、递推公式、遍历顺序有问题。

2. 斐波那契数

  • 题目链接:https://leetcode.cn/problems/fibonacci-number/description/
  • 思路:注意求 n 为0、1时的边界条件不能用动态规划递推;然后 n > 1 时递推公式已经给了dp[n] = dp[n-2] + dp[n-1],初始化为0、1,从前往后遍历;注意可以直接用一个二维数组代替 dp 数组。
  • 代码实现:
class Solution {
    public int fib(int n) {
        if(n < 2) {
            return n;
        }
        int[] dp = new int[2];
        dp[0] = 0;
        dp[1] = 1;

        for(int i = 2; i <= n; ++i) {
            int temp = dp[1];
            dp[1] = dp[1] + dp[0];
            dp[0] = temp;
        }

        return dp[1];
    }
}

3. 爬楼梯

  • 题目链接:https://leetcode.cn/problems/climbing-stairs/
  • 思路:第一层直接爬一阶上来,第二层可以走两个一阶或者一次走一个两阶上来–两种方法,第三层可以从第一层一次两阶上来也可以从第二层爬一阶上来–到达第三层的方法就是到达第一层和第二层的方法之和。dp[i] – 到达第 i 层有多少种方法,状态转移:dp[i] = dp[i - 1] + dp[i - 2],初始条件:第1层为1种,第2层为2种,第0层不用管。
  • 代码实现:
class Solution {
    public int climbStairs(int n) {
        // dp[i] -- 第i层楼梯有多少种方法到达
        // dp[1] = 1,一步走上来;
        // dp[2] = 2,分两步上来或者一次两个台阶上来
        // dp[3] -- 可以从1阶一次上两个台阶上来,也可以从2阶上一个台阶上来 dp[3] = dp[2] + dp[1]
        if(n <= 2) {
            return n;
        }

        // dp[i] = dp[i - 1] + dp[i - 2], i >=3
        int[] dp = new int[n + 1];
        dp[1] = 1;
        dp[2] = 2;
        for(int i = 3; i < n + 1; ++i) {
            dp[i] = dp[i - 1] + dp[i - 2];
        }

        return dp[n];
    }
}

4. 使用最小花费爬楼梯

  • 题目链接:https://leetcode.cn/problems/min-cost-climbing-stairs/
  • 思路:动态规划
    • dp[i] – 达到第 i 层需要花费的体力;
    • 递推公式:第 i 层可以从 i-1 或者 i-2 层爬上来,那么花费的体力应该是到达这两层花费的体力和离开这一层的成本之和的小者,即 dp[i] = Min{dp[i - 1] + cost[i - 1], dp[i - 2] + cost[i - 2]}
    • 初始化:dp[0] = 0, dp[1] = 0;
    • 遍历顺序:从前往后
  • 代码实现:
class Solution {
    public int minCostClimbingStairs(int[] cost) {
        // 1、dp数组,因为最后到达楼顶也需要一步,所以比cost + 1
        int[] dp = new int[cost.length + 1];

        // 2、初始化
        dp[0] = 0;
        dp[1] = 0;

        // 3、遍历 -- 从前往后
        for(int i = 2; i < dp.length; ++i) {
            // 4、状态转移:第i层可以从低一层或者低两层爬上来
            // dp[i] = Min{dp[i - 1] + cost[i - 1], dp[i - 2] + cost[i - 2]}
            dp[i] = Math.min(dp[i - 2] + cost[i - 2], dp[i - 1] + cost[i - 1]);
        }

        return dp[dp.length - 1];
    }
}

5. 动态规划周总结

6. 不同路径

  • 题目链接:https://leetcode.cn/problems/unique-paths/
  • 思路:
    • dp二维数组,dp[i][j],到达 i,j 位置有多少种路径;
    • 递推公式:每一个位置只能从其上边或者左边移动过来 – dp[i][j] = dp[i - 1][j] + dp[i][j - 1]
    • 初始化:第一行和第一列只有一种路径过来;
    • 遍历:从左上到右下,已经初始化的第一行第一列不需要遍历
  • 代码实现:
class Solution {
    public int uniquePaths(int m, int n) {
        // 1、dp二维数组,dp[i][j],到达 i,j 位置有多少种路径
        int[][] dp = new int[m][n];

        // 2、初始化 -- 第一行和第一列的元素只有一种路径能到达
        for(int i = 0; i < m; ++i) {
            dp[i][0] = 1;
        }
        for(int i = 0; i < n; ++i) {
            dp[0][i] = 1;
        }

        // 3、遍历,从左上到右下
        for(int i = 1; i < m; ++i) {
            for(int j = 1; j < n; ++j) {
                // 4、递推:每一个位置只能从其上边或者左边移动过来
                dp[i][j] = dp[i - 1][j] + dp[i][j - 1];
            }
        }

        return dp[m - 1][n - 1];
    }
}

7. 不同路径 II

  • 题目链接:https://leetcode.cn/problems/unique-paths-ii/description/
  • 思路:与不同路径基本相同,对于障碍物的处理,在初始化时如果有障碍物,第一行/列障碍物之后的位置都无法到达;对于状态转移方程,如果当前单元格是障碍物,就无法到达,那么到达路径数直接设为0,否则是从正上方或者正左方到达的路径之和。
  • 代码实现:
class Solution {
    public int uniquePathsWithObstacles(int[][] obstacleGrid) {
        int m = obstacleGrid.length;
        int n = obstacleGrid[0].length;
        int[][] dp = new int[m][n];

        // 初始化
        for(int i = 0; i < m; ++i) {
            // 第一列中障碍物后面的都无法到达
            if(obstacleGrid[i][0] != 0) {
                break;
            }
            dp[i][0] = 1;
        }
        for(int i = 0; i < n; ++i) {
            // 第一行中障碍物后面的都无法到达
            if(obstacleGrid[0][i] != 0) {
                break;
            }
            dp[0][i] = 1;
        }

        // 遍历
        for(int i = 1; i < m; ++i) {
            for(int j = 1; j < n; ++j) {
                // 状态转移:如果当前单元格是障碍物,到达路径数就是0
                dp[i][j] = obstacleGrid[i][j] == 0 ? dp[i - 1][j] + dp[i][j - 1] : 0;
            }
        }

        return dp[m - 1][n - 1];
    }
}

8. 整数拆分

  • 题目链接:https://leetcode.cn/problems/integer-break/
  • 思路:
    • dp[i]:数值i能够拆分出来的最大乘积;
    • 递推公式:dp[i] = Math.max(dp[i], Math.max(j * (i - j), j * dp[i - j]));,j * (i - j) – 对i只拆成两个数、j * dp[i - j] – 对i拆成两个及两个以上的数;
    • 初始化:0、1没有拆分的意义,初始化2的拆分 dp[2] = 1;
    • 遍历:从3开始。
  • 代码实现:
class Solution {
    public int integerBreak(int n) {
        // 1、动态数组 -- dp[i] 数值i能够拆分出来的最大乘积
        int[] dp = new int[n + 1];

        // 2、初始化:0、1不有拆分,dp没有意义,dp[2]可以拆分成1+1
        dp[2] = 1;

        // 3、遍历
        // 遍历从3开始的每个数值
        for(int i = 3; i <= n; ++i) {
            // 遍历数值i从1开始拆出来
            for(int j = 1; j < i; ++j) {
                // j * (i - j) -- 对i只拆成两个数
                // j * dp[i - j] -- 对i拆成两个及两个以上的数
                dp[i] = Math.max(dp[i], Math.max(j * (i - j), j * dp[i - j]));
            }
        }

        return dp[n];
    }
}

9. 不同的二叉搜索树

  • 题目链接:https://leetcode.cn/problems/unique-binary-search-trees/description/
  • 思路:i个节点组成的二叉搜索树的种类可以取从1到i分别作为根节点,将每个根节点情况的搜索树的种类累加;每种情况下树的种类是左右子树的种类之即积,对应每种情况下左右子树的情况是:
    • 1为根节点–左子树0个节点组成搜索树的种类*右子树i-1个节点做成搜索树的种类;
    • 2为根节点–左子树1个节点组成搜索树的种类*右子树i-2个节点组成搜索树的种类;
    • i为根节点–做字数i-1个节点组成搜索数的种类*右子树0个节点组成搜索树的种类;
    • 0个节点组成搜索树的种类就是一个空树,种类为1;
    • 1个节点组成的搜索树和单独一个节点组成的搜索树的种类数相同。
  • 代码实现:
// 动态规划
// dp[i]代表由i个节点做成的二叉搜索树的种类
// 递推公式:i个节点组成的二叉搜索树 -- 从1到i分别作为根节点
// 对应每种情况下左右子树的情况是:
// 1为根节点--左子树0个节点组成搜索树的种类*右子树i-1个节点做成搜索树的种类;
// 2为根节点--左子树1个节点组成搜索树的种类*右子树i-2个节点组成搜索树的种类;
// ...
// i为根节点--做字数i-1个节点组成搜索数的种类*右子树0个节点组成搜索树的种类
class Solution {
    public int numTrees(int n) {
        // 1、dp数组
        int[] dp = new int[n + 1];

        // 2、初始化,只用初始化0个节点组成的树的种类 -- 1个空树
        dp[0] = 1;

        // 3、遍历
        for(int i = 1; i <= n; ++i) {
            for(int j = 0; j < i; ++j) {
                dp[i] += dp[j] * dp[i - j - 1];
            }
        }

        return dp[n];
    }
}

10. 动态规划周总结

11. 0-1背包理论基础1

  • 题目链接:null
  • 思路:二维dp数组实现0-1背包
    • dp数组及下标含义:dp[i][j] – 从物品0~i任意取,放进容量为j的背包能够得到的最大价值;
    • 递推公式:如果容量j放得下当前物品i,那么可以选择放该物品也可以不放该物品;放该物品 – 最大价值应该是当前物品价值 value[i] + 放该物品之前剩余容量能够获得的最大价值dp[i - 1][j - weight[i]];
    • 初始化:对于j = 0,容量为0,什么都放不了,价值为0,即第一列为 0;对于第一行i = 0,物品0放进不同容量的背包,最大价值就看能不能放下,能放下价值就是物品0的价值value[0],否则就什么也没放,价值为0;
    • 遍历顺序:可以先遍历物品,每遍历到一个物品,然后遍历依次增大的背包容量,确定对应背包容量下能够得到的最大价值;也可以先遍历背包容量,然后遍历该容量下不断增加可以选择的物品种类时对应的最大价值;本质上遍历到每个元素,用到的都是矩阵左上角已经计算过的结果,这里选择先遍历物品再遍历背包容量。
  • 代码实现:
package com.dp;

public class Knapsack01 {
    public static void main(String[] args) {
        int[] weight = {1,3,4};
        int[] value = {15,20,30};
        int bagSize = 4;
        testWeightBagProblem(weight,value,bagSize);
    }

    private static void testWeightBagProblem(int[] weight, int[] value, int bagSize) {
        // 1、dp数组 dp[i][j] -- 物品0~i任意取,放进容量为j的背包能得到的最大价值
        int[][] dp = new int[weight.length][bagSize + 1];

        // 2、初始化dp数组
        // j为0时,放不进物品,所以第一列默认都是0
        for(int j = 0; j <= bagSize; ++j) {
            // 第一行,对应物品0,如果容量j不能放下物品0则价值为0,否则为物品0的价值
            if(j >= weight[0]) {
                dp[0][j] = value[0];
            }
        }

        // 3、遍历
        for(int i = 1; i < weight.length; ++i) {
            // 先遍历物品
            for(int j = 1; j <= bagSize; ++j) {
                // 再遍历背包容量
                // 4、递推
                if (weight[i] > j) {
                    // 4.1 背包装不下物品i,那就是物品不选择,价值就是dp[i-1][j]
                    dp[i][j] = dp[i - 1][j];
                } else {
                    // 4.2 背包能装下物品i,dp[i][j]既可以选择不放物品i,也可一选择放物品i
                    // 选择放 -- 最大价值就是i的价值 + 容量为j-weight[i]时的最大价值即dp[i-1][j-weight[i]]
                    dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
                }
            }
        }

        // 打印dp数组
        for(int i = 0; i < dp.length; ++i) {
            for (int j = 0; j < dp[0].length; j++) {
                System.out.print(dp[i][j] + "\t");
            }
            System.out.println();
        }

    }
}

12. 0-1背包理论基础2

  • 题目链接:null
  • 思路:通过滚动数组减少空间复杂度,使用一维数组实现0-1背包;
    • dp数组及下标含义:dp[j] – 容量为j的背包能够获得的最大价值,随着物品的遍历更新;
    • 递推公式:当前容量能够放下当前物品才有放进去递推更新的必要 – 不放当前的物品,直接就是原来的 dp[j],放当前物品,就是 value[i] + dp[j - weight[i]],取二者之间的大的;
    • 初始化:j为0时最大价值肯定为0,其他的都可以根据后面的遍历来获得;
    • 遍历顺序:先遍历物品,再倒序遍历背包容量;如果正序遍历背包容量,那么后面的容量取dp[j - weight[i]]时对应的元素就被覆盖了,所以只能倒序遍历背包容量;而如果先倒序遍历背包容量,再遍历物品,每个dp[j]就只会放一个物品。归根到底,二维数组中是右下角的值依赖于其左上方的,使用滚动一维数组,就要先遍历物品再遍历背包容量,不能提前覆盖–倒序。
  • 代码实现:
package com.dp;

public class Knapsack01 {
    public static void main(String[] args) {
        int[] weight = {1,3,4};
        int[] value = {15,20,30};
        int bagSize = 4;
        // testWeightBagProblem1(weight,value,bagSize);
        testWeightBagProblem2(weight,value,bagSize);
    }

    /**
     * 一维数组实现01背包
     * @param weight
     * @param value
     * @param bagSize
     */
    private static void testWeightBagProblem2(int[] weight, int[] value, int bagSize) {
        // 1、dp数组 -- dp[j] 背包容量为j时能够获得的最大价值
        int[] dp = new int[bagSize + 1];

        // 2、初始化 -- 容量为0时价值为0,其余的都可以通过遍历得到(前提 -- 价值全都为正)
        dp[0] = 0;

        // 3、遍历
        // 3.1 先遍历物品
        for(int i = 0; i < weight.length; ++i) {
            // 3.2 再遍历背包容量,倒序遍历,这样计算后面的容量的时候前面的结果不会被覆盖;倒序时控制背包容量能够装下当前物品
            for(int j = bagSize; j >= weight[i]; --j) {
                // 能装下当前物品时才有装和不装两个选择,取其中大的
                dp[j] = Math.max(dp[j], value[i] + dp[j - weight[i]]);
            }
        }

        // 打印dp数组
        for (int j = 0; j < dp.length; j++) {
            System.out.print(dp[j] + "\t");
        }
    }

13. 分割等和子集

  • 题目链接:https://leetcode.cn/problems/partition-equal-subset-sum/
  • 思路:先统计数组的总和,然后将总和的一半target作为背包容量 – 0-1背包:数组nums中的每个元素都是物品,物品的重量和价值都是元素的值,确定背包容量为target时能够装进去的最大价值是否和target相同 – 相同就是刚好有不同的元素和为总和一半;
    • dp数组:dp[j]容量为j的背包能够装进去的最大价值;
    • 递推公式:dp[j] = Math.max(dp[j], nums[i] + dp[j - nums[i]])
    • 遍历顺序:先正序遍历物品,再倒序遍历背包容量;每次背包容量遍历完,判断是否满足dp[target] == target
  • 代码实现:
// 0-1背包动态规划,统计总和,获得一半为target
// 求背包容量为target时背包中的最大价值,如果dp[target] == target,就找到了

class Solution {
    public boolean canPartition(int[] nums) {
        // 1、遍历求和
        int sum = 0;
        for(int num : nums) {
            sum += num;
        }

        // 2、和的一半作为目标
        if(sum % 2 != 0) {
            // 奇数不可能分割
            return false;
        }
        int target = sum / 2;

        // 3、0-1背包,找容量为target的背包是否里面能装进去和为target的数
        // 3.1 dp数组
        int[] dp = new int[target + 1];
        // 3.2 初始化 dp[0] = 0;
        // 3.3 遍历
        // 先遍历物品
        for(int i = 0; i < nums.length; ++i) {
            // 再倒序遍历背包
            for(int j = target; j >= nums[i]; --j) {
                // 3.4 递推公式
                dp[j] = Math.max(dp[j], nums[i] + dp[j - nums[i]]);
            }

            // 判断考虑当前物品后,dp[target] 是否等于 target
            if(target == dp[target]) {
                return true;
            }
        }

        // 没有
        return false;

    }
}

14. 斐波那契数

  • 题目链接:
  • 思路:
  • 代码实现:

15. 斐波那契数

  • 题目链接:
  • 思路:
  • 代码实现:

16. 斐波那契数

  • 题目链接:
  • 思路:
  • 代码实现:

17. 斐波那契数

  • 题目链接:
  • 思路:
  • 代码实现:

18. 斐波那契数

  • 题目链接:
  • 思路:
  • 代码实现:

19. 斐波那契数

  • 题目链接:
  • 思路:
  • 代码实现:

20. 斐波那契数

  • 题目链接:
  • 思路:
  • 代码实现:

21. 斐波那契数

  • 题目链接:
  • 思路:
  • 代码实现:

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值