代码随想录算法训练营第34天 | LeetCode62.不同路径、LeetCode63.不同路径II、LeetCode343.整数拆分、LeetCode96.不同的二叉搜索树

目录

LeetCode62.不同路径

1. 动态规划

2. 数论方法

LeetCode63.不同路径II

LeetCode343.整数拆分

1. 动态规划

2. 贪心方法

LeetCode96.不同的二叉搜索树


LeetCode62.不同路径

一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为 “Start” )。

机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish” )。

问总共有多少条不同的路径?

思路:这种每次只能向右或者向下走一步,最后到达指定位置的题目特别像二叉树从头节点到叶子结点,然后问你有多少种到达方法,采用二叉树求头节点到叶子结点的路径数就是这道题从某个其实点到终点的路径数,但是如果使用二叉树进行递归,时间复杂度太大,是2的指数级别,因此不建议使用,会超限。

这里介绍两种方法,动态规划以及数论方法解决。

1. 动态规划

使用动态规划思想,dp[i][j]表示到达(i,j)点的时候的路径条数。状态转换方程也很好找,(i,j)结点的路径条数是它的左边点(i-1,j)以及上边点(i,j-1)的路径数相加。这里的初始化也是将(0,i)以及(i,0)点赋值为1,因为本身从起始点出发到达这些地方也就直着走一条路径。

    int uniquePaths(int m, int n) {
        vector<vector<int>> dp(m, vector<int>(n, 0));//设置dp数组,dp[i][j]表示从(0,0)到(i,j)的所有路径
        for(int i = 0; i < m; i ++) dp[i][0] = 1;
        for(int j = 0; j < n; j ++) dp[0][j] = 1;//初始化,因为到达这些点的路径只有1条
        for(int i = 1; i < m; i ++){
            for(int j = 1; j < n; j ++){
                dp[i][j] = dp[i - 1][j] + dp[i][j - 1];//确定状态转换方程,
                //对于(i,j)点来说,能够到达的路径数就是从上方或者左方到达该点的路径总数和
            }
        }
        return dp[m - 1][n - 1];
    }

时间复杂度:O(m*n)

空间复杂度:O(m*n)

当然,也可以对上面的空间复杂度进行优化,只维护一个一维数组,这个数组就像一个滚动数组一样,每次更新一行的内容,最后返回最后一个元素即为所求。

    int uniquePaths(int m, int n) {
        vector<int> dp(n);
        for(int i = 0; i < n; i ++) dp[i] = 1;
        for(int i = 1; i < m; i ++){
            for(int j = 1; j < n; j ++){
                dp[j] += dp[j - 1];
            }
        }
        return dp[n - 1];
    }

时间复杂度:O(m*n)

空间复杂度:O(n)

2. 数论方法

使用数论方法,首先我们从题目中可以得知,走的总共的步数是为m+n-2步,其中向下走的为m-1步,于是就相当于从m+n-2个元素中找出m-1个元素,所以这就是一个组合问题 C^{_{m+n-2}^{m-1}}

但是需要注意,两个int整型数相乘容易超限,所以如果是想要分子分母计算完成后开始约分,多半AC不了,太大了。

所以就需要在乘的过程中约分。

    int uniquePaths(int m, int n) {
        long long numerator = 1;//设置分子为1
        int denominator = m - 1;//设置分母从m-1开始进行
        int count = m - 1;//计数
        int t = n + m - 2;
        while(count > 0){
            numerator *= (t --);//分子乘上相应的元素
            while(denominator != 0 && numerator % denominator == 0){//这里是进行相乘的过程中就进行约分,避免两个整型相乘数目较大
                numerator /= denominator;
                denominator --;
            }
            count --;
        }
        return numerator;
    }

时间复杂度:O(m)

空间复杂度:O(1)

LeetCode63.不同路径II

一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为 “Start” )。

机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish”)。

现在考虑网格中有障碍物。那么从左上角到右下角将会有多少条不同的路径?

网格中的障碍物和空位置分别用 10 来表示。

思路:这道题有了上面的题做铺垫,其实大体思路一致。主要是在遇到障碍的时候如何处理。

在初始化时,如果遇到了为1,那么后面的都为0,因为后面的点不可能再有路径了。

在循环中,遇到了1,那么就跳过,因为有障碍,本身也过不去,没必要再进入循环。

如果没有遇到障碍,那就该怎么赋值就怎么赋值,该怎么循环就怎么循环。

    int uniquePathsWithObstacles(vector<vector<int>>& obstacleGrid) {
        int m = obstacleGrid.size();//获取网格的行数
        int n = obstacleGrid[0].size();//获取网格的列数
        if(obstacleGrid[0][0] == 1 || obstacleGrid[m - 1][n - 1] == 1) return 0;//起点或终点有障碍物了,直接返回0
        vector<vector<int>> dp(m, vector<int>(n, 0));//设置dp数组,dp[i][j]表示(i,j)点的路径条数
        for(int i = 0; i < n && obstacleGrid[0][i] == 0; i ++) dp[0][i] = 1;//对于(0,i)点,若没有障碍物,置为1
        for(int i = 0; i < m && obstacleGrid[i][0] == 0; i ++) dp[i][0] = 1;//对于(i,0)点,若没有障碍物,置为1
        for(int i = 1; i < m; i ++){
            for(int j = 1; j < n; j ++){
                if(obstacleGrid[i][j] == 0){//当没有障碍物时进行对(i,j)点进行更新
                    dp[i][j] = dp[i - 1][j] + dp[i][j - 1];
                }
            }
        }
        return dp[m - 1][n - 1];
    }

时间复杂度:O(m*n)

空间复杂度:O(m*n)

当然,同样可以使用一维数组进行优化,但是比较绕,建议将上面掌握好了再来看下面的优化。

这里初始赋值的时候要注意,不能仅凭为1和为0来判定赋值,因为假设前面一个是1,后面一个即便是0,这时候的值也不能为1,因为前面都把路堵住了,根本就不可能到后面来。

还有在循环过程中,遇到障碍1了,要将元素置为0,不能直接跳过,因为后面还需要这个值,是一环扣一环的,没处理就会出问题。

同时注意里层循环下标是从0开始的,我们后面的计算会用到dp[0],所以计算的时候下标不能超限,也就是说j要大于1,否则j-1就有问题。

    int uniquePathsWithObstacles(vector<vector<int>>& obstacleGrid){
        int m = obstacleGrid.size();//获取网格的行
        int n = obstacleGrid[0].size();//获取网格的列
        if(obstacleGrid[0][0] == 1 || obstacleGrid[m - 1][n - 1] == 1) return 0;
        vector<int> dp(n, 0);//dp[i]记录每一行更新后的路径条数
        for(int i = 0; i < n; i ++){
            if(obstacleGrid[0][i] == 1){//对第一行(0,i)更新路径条数
                dp[i] = 0;
            }else if(i == 0){
                dp[i] = 1;
            }else{
                dp[i] = dp[i - 1];//注意这里不能仅凭0和1来判断是否赋值为0或1
                //因为当前一个元素为1,后一个元素为0时,前面把路都给堵住了,不可能再赋值为dp[i]=1;
                //所以这里是当其他情况的时候,dp[i] = dp[i - 1]
            }
        }
        for(int i = 1; i < m; i ++){
            for(int j = 0; j < n; j ++){
                if(obstacleGrid[i][j] == 1){
                    dp[j] = 0;//当遇到障碍了,更新dp[j]等于0
                }else if(j != 0){
                    dp[j] += dp[j - 1];//当没有障碍时,更新dp[j]的值 
                }
            }
        }
        return dp.back();
    }

时间复杂度:O(m*n)

空间复杂度:O(n)

LeetCode343.整数拆分

给定一个正整数 n ,将其拆分为 k正整数 的和( k >= 2 ),并使这些整数的乘积最大化。

返回 你可以获得的最大乘积 。

思路:整数拆分难就难在怎么拆,将一个数拆成一两个数可能好使,但是四五个数的话,怎么弄呢?

这里介绍两种方法,动态规划贪心算法

1. 动态规划

动态规划就是在进行这里的计算时可以使用前面已经计算好的最优值,于是这里我们使用dp[i]作为i整数的划分最大乘积。

那么其实就开始变得明了了。

比如说i整数的拆分乘积和什么有关,可以与(i-j)*j,dp[i-j]*j有关,其中(i-j)*j就相当于拆分为两个数相乘,而dp[i-j]*j至少是两个数相乘,或者更多,也就将整数i拆分为多个整数想要找最大乘积提供了依据。如下图代码所示。

但是这里比较为什么还有个dp[i]呢?是不是觉得很莫名奇妙,我要求它,怎么还要将它和其他两者比较取最大?

因为这里的内层循环其实不止进行一次,每一次都会计算出dp[i],所以它是有必要加入的,因为我要的是整个循环结束后,dp[i]最大值,所以它参与了比较。

如果没太懂可以试着手动模拟一些测试数据,自然而然就懂了。

    int integerBreak(int n) {
        vector<int> dp(n + 1);//设置dp数组,dp[i]为正整数i分割后的最大乘积
        dp[2] = 1;//初始化
        for(int i = 3; i < n + 1; i ++){
            for(int j = 1; j < i; j ++){
                dp[i] = max(dp[i], max((i - j) * j, dp[i - j] * j));
                //为什么这里需要将dp[i]放上去比较,因为dp[i]在内层循环不止进行了一次,
                //当第一次进入循环后就生成了一个dp[i],然后j++,内层循环进入了下一层,
                //又生成了一个dp[i],所以要取里面最大的,所以要包括之前生成的dp[i]进行比较
            }
        }
        return dp[n];
    }

当然,这里还可以优化一下,将内层循环的终止下标提前一位到i-1,因为i-1这里的元素和下标为1时算出的值一样,其实就没必要重复进行一次了。

    int integerBreak(int n) {
        vector<int> dp(n + 1);//设置dp数组,dp[i]为正整数i分割后的最大乘积
        dp[2] = 1;//初始化
        for(int i = 3; i < n + 1; i ++){
            for(int j = 1; j < i - 1; j ++){
                dp[i] = max(dp[i], max((i - j) * j, dp[i - j] * j));
                //为什么这里需要将dp[i]放上去比较,因为dp[i]在内层循环不止进行了一次,
                //当第一次进入循环后就生成了一个dp[i],然后j++,内层循环进入了下一层,
                //又生成了一个dp[i],所以要取里面最大的,所以要包括之前生成的dp[i]进行比较
            }
        }
        return dp[n];
    }

当然了,还可以优化一下,这次是将元素砍半,因为这是基于这样一个假设,也就是说,想要拆分的数字乘积足够大,那么其实这些乘积最大的拆分的数字是很接近的,一般就在2、3等左右,所以基于此,可以将循环减半。

当然如果说比较感兴趣,想要证明的话,可以自己找找资料。这里就不展开了,当然如果觉得这个假设不太容易接受,就按照正常的遍历即可。

    int integerBreak(int n) {
        vector<int> dp(n + 1);//设置dp数组,dp[i]为正整数i分割后的最大乘积
        dp[2] = 1;//初始化
        for(int i = 3; i < n + 1; i ++){
            for(int j = 1; j <= i / 2; j ++){
                dp[i] = max(dp[i], max((i - j) * j, dp[i - j] * j));
                //为什么这里需要将dp[i]放上去比较,因为dp[i]在内层循环不止进行了一次,
                //当第一次进入循环后就生成了一个dp[i],然后j++,内层循环进入了下一层,
                //又生成了一个dp[i],所以要取里面最大的,所以要包括之前生成的dp[i]进行比较
            }
        }
        return dp[n];
    }

时间复杂度:O(n^2)

空间复杂度:O(n)

2. 贪心方法

这里的贪心同样源于一种假设,也就是说如果说拆分的数字中有3、4等,就将其拆分为3、4,最后相乘即可。代码比较简单。同样,和上面一样,如果感兴趣可以自己去查查资料。

    int integerBreak(int n) {
        if(n <= 3) return n - 1;
        int result = 1;
        while(n > 4){
            result *= 3;
            n -= 3;
        }
        result *= n;
        return result;
    }

时间复杂度:O(n)

空间复杂度:O(1)

LeetCode96.不同的二叉搜索树

给你一个整数 n ,求恰由 n 个节点组成且节点值从 1n 互不相同的 二叉搜索树 有多少种?返回满足题意的二叉搜索树的种数。 

思路:关系到二叉搜索树,想必大家都还记录它的定义以及性质。

如果使用动态规划,其实最难找的就是它的状态转换方程,但其实也有迹可循。

对于1个结点,不同的二叉搜索树的数目为1;对于2个结点,不同的二叉搜索树的数目为2;对于3个结点,不同的二叉搜索树的数目为5......

这里截了一张三个结点的不同二叉搜索树的所有可能情况。

可以看到,结点1,2,3分别都作为过头节点,选择其中的1来分析,以1作为头节点,左孩子0个结点,右孩子两个结点,它的右孩子的两个结点的样子,正好是只有两个结点是的所有可能情况,同理对于2,3来说都是。

于是我们可以知道,n个结点的不同二叉树个数与头节点以及其左右孩子个数有关,例如3,它的不同二叉树的个数就等于1作为头节点(0个左孩子,2个右孩子),2作为头节点(1个左孩子,1个右孩子),3作为头节点(2个右孩子,0个左孩子)的不同二叉搜索树个数之和。

所以这里我们使用dp[i]表示i个结点的不同二叉树个数,dp[3]就等于dp[0]*dp[2]+dp[1]*dp[1]+dp[2]*dp[0](相当于求个数就等于左边孩子的不同二叉搜索树个数*右边孩子的不同二叉搜索树个数)。

这里频繁涉及到了dp[0],所以需要对其进行初始化,0相当于没有结点,也就是空树,空树也是一种二叉搜索树,于是dp[0]=1。

于是这里dp[i] += dp[j-1]*dp[i-j]表示的就是以j为结点,j-1个左孩子,i-j个右孩子的情况时,将其所具有的不同二叉搜索树个数加起来,内层循环结束,个数也统计完成。

    int numTrees(int n) {
        vector<int> dp(n + 1, 0);//dp[i]表示i个结点能构成的不同的二叉搜索树的个数
        dp[0] = 1;//当没有结点的时候,也相当于二叉搜索树中的一种,只不过此时结点个数为0
        for(int i = 1; i < n + 1; i ++){
            for(int j = 1; j <= i; j ++){
                dp[i] += (dp[j - 1] * dp[i - j]);//dp[j-1]*dp[i-j]表示将结点j当作头节点,左边j-1个结点,右边i-j个结点时的不同二叉搜索树个数
            }
        }
        return dp[n];
    }

时间复杂度:O(n^2)

空间复杂度:O(n)

感谢你的阅读,希望我的文章能够给你帮助,如果有帮助,麻烦点赞加收藏,或者点点关注,非常感谢。

如果有什么问题欢迎评论区讨论!

 

  • 11
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值