动态规划-基础篇-01

第九章 动态规划

动态规划五部曲

  1. 确定dp数组及下标的含义。
  2. 确定递推公式。
  3. 初始化dp数组。
  4. 确定遍历顺序。
  5. 举例推导dp数组。

509.斐波那契数

关键词:动态规划、边界处理

我们只要遍历n然后生成数即可,由于下一个值只与前两位的值有关,所以我们可以省略dp数组,直接使用两个变量存储值即可。

class Solution {
    public int fib(int n) {
        int a=0,b=1,temp=0;
        if(n<2){ return n;}
        for(int i=2;i<=n;i++){
            temp = a+b;
            a = b;
            b = temp;
        }
        return b;
    }
}

我的实现,使用递归

class Solution {
    public int fib(int n) {
        if(n==0 || n==1){
            return n;
        }
        return fib(n-1) + fib(n-2);
    }
}

70.爬楼梯

关键词:动态规划、边界处理

这题实际上也是斐波那契数的变种,第n阶楼梯是由第n-1阶楼梯走1步和第n-2阶楼梯走2步得到的。我们只需修改上一题的初值即可解决该题。

class Solution {
    public int climbStairs(int n) {
        int a=1,b=1,temp=0;
        if(n<2){ return n;}
        for(int i=2;i<=n;i++){
            temp = a+b;
            a = b;
            b = temp;
        }
        
        return b;
    }
}

我觉得这是一个排列问题,而不是组合问题。1和2在不同的位置算不同的答案。

dp[1] = 1; {[1]}
dp[2] = 2; {[1,1],[2]}
dp[3] = 3; {[1,1,1],[1,2],[2,1]}
dp[4] = 5; {[1,1,2],[2,2],[1,1,1,1],[1,2,1],[2,1,1]}
...
dp[i] = dp[i-1] + dp[i-2]

因为是排列问题,所以dp[i] = dp[i-1] + dp[i-2],dp[i]表示总和为i个台阶有多少种1或2的排列,使得排列的和等于i。解释上说dp[i]要么是dp[i-1]爬一节台阶上来,要么是dp[i-2]爬两节台阶上来。不妨换一种公式的写法:dp[i] = 1*dp[i-1] + 1*dp[i-2],1*dp[i-1]表示在dp[i-1]的各种排列的基础上,排列最后的位置放一个1,总共有1*dp[i-1]种排列结果;1*dp[i-2]表示在dp[i-2]的各种排列的基础上,排列最后的位置放一个2,总共有1*dp[i-2]种排列结果。dp[i-1] + dp[i-2]的排列肯定不存在重叠的情况,所以两种排列结果相加就是dp[i]。

同理,若每次可走台阶数:{1,2,3},则dp[i]=dp[i-1] + dp[i-2] + dp[i-3]

若此题问的是组合有多少种,那就不是动态规划的题了,即dp[i]可以独立计算,与dp[j](j<i)无关。

我的解答:

//不维护dp数组的两种方法
//循环,时间复杂度:O(n)
class Solution {
    public int climbStairs(int n) {
        int a=1,b=2;
        int res = 0;
        if(n<=2) return n;
        for(int i = 3;i<=n;i++){
            res = a + b;
            a=b;
            b=res;
        }
        return res;
    }
}
//2.递归的方法:超出时间限制,时间复杂度:O(2^n)
class Solution {
    public int climbStairs(int n) {
        if(n==1 || n==2) return n;
        return climbStairs(n-1) + climbStairs(n-2);
    }
}
//使用循环维护dp数组
class Solution {
    public int climbStairs(int n) {
        int[] dp = new int[n+1];
        if(n<3) return n;
        dp[1]=1;
        dp[2]=2;
        for(int i=3;i<=n;i++){
            dp[i] = dp[i-1] + dp[i-2];
        }
        return dp[n];
    }
}

746.使用最小花费爬楼梯

关键词:动态规划、边界处理

和上一题一样,第n阶楼梯是由第n-1阶楼梯走1步和第n-2阶楼梯走2步得到的,只是这题增加了花费的概念。我们使用dp数组来记录每一步的花费,可以推导出这样递归公式dp[n+2] = Math.min(dp.get(i)+cost[i],dp.get(i+1)+cost[i+1]),确定好递归公式后,我们就需要为dp数组设定初值,由于可以选择在0或1的台阶开始,因此他们的初值为0,根据递归公式我么也可以知道应该从左到右进行遍历。

class Solution {
    public int minCostClimbingStairs(int[] cost) {
        List<Integer> dp = new ArrayList<Integer>();
        dp.add(0);
        dp.add(0);
        for(int i=0;i<cost.length-1;i++){
            dp.add(Math.min(dp.get(i)+cost[i],dp.get(i+1)+cost[i+1]));
        }

        return dp.get(dp.size()-1);
    }
}

我的解答:

//使用循环,维护dp数组
class Solution {
    public int minCostClimbingStairs(int[] cost) {
        int[] dp = new int[cost.length+1];
        dp[0]=0;
        dp[1]=0;
        for(int i=2;i<=cost.length;++i){
            dp[i] = Math.min(dp[i-1]+cost[i-1],dp[i-2]+cost[i-2]);
        }
        return dp[cost.length];
    }
}
//不维护dp数组
class Solution {
    public int minCostClimbingStairs(int[] cost) {
 	    int a=0,b=0,res=0; //初始化
        for(int i=2;i<=cost.length;++i){
            res = Math.min(b+cost[i-1],a+cost[i-2]);
            a=b;
            b=res;
        }
        return res;
    }
}

62.不同路径

关键词:动态规划、二维dp数组、边界处理、数学

由于机器人每次只能向下移动或者向右移动一步,那么dp[i][j]的值就是左边的路线和加上上面的路线和,递推公式为dp[i][j] = dp[i][j-1]+dp[i-1][j];。显然最上面和最左边的会产生越界错误,我们需要为其赋初值,由于限制条件,因此初值为1。最后只需返回右下角的值即为不同路径的总数。或者使用高中数学的数论基础知识解决,机器人一定会走m+n-2步,即从m+n-2中挑出m-1步向下走不就行了吗?即(C^{m-1}_{m+n-2})。

class Solution {
    public int uniquePaths(int m, int n) {
        int[][] dp = new int[m][n];
        for(int i=0;i<n;i++){
            dp[0][i] = 1;
        }
        for(int i=0;i<m;i++){
            dp[i][0] = 1;
        }
        for(int i=1;i<m;i++){
            for(int j=1;j<n;j++){
                dp[i][j] = dp[i][j-1]+dp[i-1][j];
            }
        }
         return dp[m-1][n-1];   
    }
}

我的解答:

经验总结:递归实现的动态规划很有可能超时,所以递归方法并不是首选方法。

//二叉树,遍历计算叶子节点(叶子节点当作终点)。起点作为二叉树的根节点,在任意位置都只有向下,向右两种运动方式,可以看作二叉树的左孩子和右孩子。判断是否为叶子节点的标志是其位置(i=m-1,j=n-1)。本题只是借用二叉树的思想,操作对象并不是节点。
//超时,重复计算,时间复杂度为O(2^(m + n - 1) - 1)。其思想和递归实现的动态规划相似:位置(i,j)的路径数=位置(i+1,j)路径数 + 位置(i,j+1)路径数。
//递归的底部是终点,从终点向起点逐步计算。
class Solution {
    public int uniquePaths(int m, int n) {
        return dfs(0, 0, m, n);
    }
    public int dfs(int i, int j, int m, int n){
        if(i>=m || j>=n) return 0;
        if(i==m-1 && j==n-1) return 1;
        return dfs(i+1, j, m, n) + dfs(i, j+1, m, n);
    }
}
//二维dp数组初始化边界,一维dp数组初始化开头。从起点向终点逐步计算。双重循环
//dp[i][j]表示从位置(i,j)到终点(m-1,n-1)的路径数。最终返回dp[0][0]
//dp[m-1][],dp[][n-1]都为1
class Solution {
    public int uniquePaths(int m, int n) {
        int[][] dp = new int[m][n];
        ///初始化
        for(int i=0;i<n;++i) dp[m-1][i] = 1;
        for(int j=0;j<m;++j) dp[j][n-1] = 1;
        //双重循环
        for(int i=m-2;i>=0;--i){
            for(int j=n-2;j>=0;--j){
                dp[i][j] = dp[i+1][j] + dp[i][j+1];
            }
        }
        return dp[0][0];    
    }
}
//dp[i][j]表示从位置(0,0)到位置(i,j)的路径数。最终返回dp[m-1][n-1]
//dp[0][],dp[][0]都为1
class Solution {
    public int uniquePaths(int m, int n) {
        int[][] dp = new int[m][n];
        ///初始化
        
        //双重循环
           
    }
}
posted @ 2025-04-07 22:27  码宝  阅读(19)  评论(0)    收藏  举报