From 0fcc05eba42aa6695fec9200676ec4a194187868 Mon Sep 17 00:00:00 2001 From: markilue <745518019@qq.com> Date: Wed, 23 Nov 2022 14:49:51 +0800 Subject: [PATCH] =?UTF-8?q?leecode=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dynamic/T05_UniquePathsWithObstacles.java | 101 +++++++++++ .../leecode/dynamic/T06_IntegerBreak.java | 169 ++++++++++++++++++ .../leecode/dynamic/T07_NumTrees.java | 105 +++++++++++ 3 files changed, 375 insertions(+) create mode 100644 Leecode/src/main/java/com/markilue/leecode/dynamic/T05_UniquePathsWithObstacles.java create mode 100644 Leecode/src/main/java/com/markilue/leecode/dynamic/T06_IntegerBreak.java create mode 100644 Leecode/src/main/java/com/markilue/leecode/dynamic/T07_NumTrees.java diff --git a/Leecode/src/main/java/com/markilue/leecode/dynamic/T05_UniquePathsWithObstacles.java b/Leecode/src/main/java/com/markilue/leecode/dynamic/T05_UniquePathsWithObstacles.java new file mode 100644 index 0000000..25e7516 --- /dev/null +++ b/Leecode/src/main/java/com/markilue/leecode/dynamic/T05_UniquePathsWithObstacles.java @@ -0,0 +1,101 @@ +package com.markilue.leecode.dynamic; + +import org.junit.Test; + +/** + * @BelongsProject: Leecode + * @BelongsPackage: com.markilue.leecode.dynamic + * @Author: markilue + * @CreateTime: 2022-11-22 20:11 + * @Description: + * TODO 力扣63题 不同路径II: + * 一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为 “Start” )。 + * 机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish”)。 + * 现在考虑网格中有障碍物。那么从左上角到右下角将会有多少条不同的路径? + * 网格中的障碍物和空位置分别用 1 和 0 来表示。 + * @Version: 1.0 + */ +public class T05_UniquePathsWithObstacles { + + @Test + public void test(){ + int[][] obstacleGrid = {{0, 0, 0},{0, 1, 0},{0, 0, 0}}; + int[][] obstacleGrid1 = {{1, 0}}; + System.out.println(uniquePathsWithObstacles(obstacleGrid1)); + } + + /** + * 自己尝试动态规划法:与T04类似,关键是对于阻隔路径的处理,当遇到阻隔路径那个位置的方法数直接付为0 + * 速度超过100%,内存超过72.86% + * TODO 这个动态规划法的初始化方式值得注意 + * 注意这两个break比较奇妙,以应对只有一行且中间有障碍物的情况,事实上只要这两行中有障碍物后面的位置也是到不了的,所以直接为0也没有问题 + * @param obstacleGrid + * @return + */ + 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]!=1){ + dp[i][0]=1; + }else { + //注意这两个break比较奇妙,以应对只有一行且中间有障碍物的情况,事实上只要这两行中有障碍物后面的位置也是到不了的,所以直接为0也没有问题 + break; + } + } + + for (int i = 0; i < n; i++) { + if(obstacleGrid[0][i]!=1){ + dp[0][i]=1; + }else { + break; + } + } + + for (int i = 1; i < m; i++) { + for (int j = 1; j < n; j++) { + if(obstacleGrid[i][j]!=1){ + dp[i][j]=dp[i-1][j]+dp[i][j-1]; + }else { + dp[i][j]=0; + } + } + } + + return dp[m-1][n-1]; + + } + + + /** + * 官方的滚动数组动态规划法:将空间复杂度优化到O(m) + * 速度超过100%,内存超过98.73% + * TODO 这个动态规划法的初始化方式值得注意 + * @param obstacleGrid + * @return + */ + public int uniquePathsWithObstacles1(int[][] obstacleGrid) { + int n = obstacleGrid.length, m = obstacleGrid[0].length; + int[] f = new int[m]; + + f[0] = obstacleGrid[0][0] == 0 ? 1 : 0;//初始化位置 + for (int i = 0; i < n; ++i) { + for (int j = 0; j < m; ++j) { + if (obstacleGrid[i][j] == 1) { + f[j] = 0; + continue; + } + if (j - 1 >= 0 && obstacleGrid[i][j - 1] == 0) { + f[j] += f[j - 1]; + } + } + } + + return f[m - 1]; + + + } +} diff --git a/Leecode/src/main/java/com/markilue/leecode/dynamic/T06_IntegerBreak.java b/Leecode/src/main/java/com/markilue/leecode/dynamic/T06_IntegerBreak.java new file mode 100644 index 0000000..bf23091 --- /dev/null +++ b/Leecode/src/main/java/com/markilue/leecode/dynamic/T06_IntegerBreak.java @@ -0,0 +1,169 @@ +package com.markilue.leecode.dynamic; + +import org.junit.Test; + +/** + * @BelongsProject: Leecode + * @BelongsPackage: com.markilue.leecode.dynamic + * @Author: markilue + * @CreateTime: 2022-11-23 10:32 + * @Description: + * TODO 力扣343题 整数拆分: + * 给定一个正整数 n ,将其拆分为 k 个 正整数 的和( k >= 2 ),并使这些整数的乘积最大化。 + * 返回 你可以获得的最大乘积 。 + * @Version: 1.0 + */ +public class T06_IntegerBreak { + + @Test + public void test(){ + for (int i = 2; i < 20; i++) { + System.out.println(integerBreak(i)); + System.out.println(integerBreak2(i)); + System.out.println("============================="); + } + + } + + + /** + * 自己尝试动态规划法: + * 没有想好dp[i]的含义:如果dp[i]就表示n=i时的最大乘积,那递推公式是? 当n>5之后dp[i]=2*dp[i-2]?好像不对,找不好递推公式,本质上是想用贪心算法,具体可以看代码随想录贪心写法 + * dp[2]=1*1;dp[3]=1*2;dp[4]=2*2;dp[5]=2*3;dp[6]=3*3;dp[7]=2*2*3;dp[8]=3*3*2;dp[9]=3*3*3;dp[10]=3*3*2*2;dp[11]=3*3*3*2 + * @param n + * @return + */ + public int integerBreak(int n) { + if(n<=6){ + switch (n){ + case 2: + return 1; + case 3: + return 2; + case 4: + return 4; + case 5: + return 6; + case 6: + return 9; + } + } + int[] dp = new int[n]; + + dp[4]=6;dp[5]=9; + for (int i = 6; i < n; i++) { + dp[i]=2*dp[i-2]; + } + return dp[n-1]; + + } + + + + /** + * 代码随想录动态规划法: + * TODO 递归五部曲如下: + * (1)确定dp数组及下标含义:dp[i]就表示n=i时的最大乘积 + * (2)确定递推公式:dp[i]的最大乘积如何得到?从1开始遍历j,有两种渠道可以得到dp[i]: + * 1)j*(i-j) =>实际上就是把一个数拆成两个数的乘积 + * 2)j*dp[i-j],相当于拆分(i-j) =>由于dp[i-j]可以继续往下拆,所以实际上是吧一个数拆成多个数的乘积; + * 注意:由于j是从1开始遍历,因此事实上也拆分了j;其次,不拆成dp[j]*dp[i-j],因为这相当于至少要把一个数拆成四个数乘积这不合题意 + * 所以状态转移方程 dp[i] = max(dp[i],(i-j)*j,dp[i-j]*j) + * (3)dp数组如何初始化:直接初始化dp[2]因为是从dp[2]开始的dp[2] = 1 + * (4)确定遍历顺序:从递归公式dp[i] = max(dp[i],(i-j)*j,dp[i-j]*j);中可以看出,dp[i]是依赖 dp[i - 1],那么遍历的顺序是从前到后遍历的 + * (5)举例推导dp数组:当N为10的时候,dp数组应该是如下的数列:1 100,1,2,... + * 速度超过79.82%,内存超过33.6% 1ms + * 由于不仅需要遍历n还需要在每个数中对n进行拆分j,所以时间复杂度为O(n^2),空间复杂度O(n) + * @param n + * @return + */ + public int integerBreak1(int n) { + int[] dp = new int[n+1]; + dp[2]=1; + + for (int i = 3; i <= n; i++) { + //遍历j进行拆分 + for (int j = 1; j < i-j; j++) { + // 这里的 j 其实最大值为 i-j,再大只不过是重复而已, + //这里dp[i]可以不用进行初始化,因为dp[i]一定比0大 + dp[i]=Math.max(dp[i],Math.max((i-j)*j,dp[i-j]*j)); + } + } + + return dp[n]; + } + + + /** + * 代码随想录贪心法:将题目输入的n拆成k个3,如果剩下的是4则保留4,然后将拆分出来的数相乘,当这个结论需要证明他的合理性 + * 怎么感觉跟自己的这么像,自己的也是隔两个就×2,一定不会大于3,但是自己的答案执着于找递推公式 + * 速度超过100%,内存超过77.17% 0ms + * 所以时间复杂度为O(n),空间复杂度O(1) + * @param n + * @return + */ + public int integerBreak2(int n) { + if(n==2)return 1; + if(n==3)return 2; + if(n==4)return 4; + int result=1; + while (n>4){ + result*=3; + n-=3; + } + result*=n; + return result; + } + + + /** + * 官方动态规划法优化:考虑到代码随想录的动态规划法的递归公式中还需要遍历j次的拆分结果才能得到最终的最大值, + * TODO 原始递推公式dp[i] = max(dp[i],(i-j)*j,dp[i-j]*j) + * 优化方式考虑能否不遍历通过推导就可以得到其中的最大值,将时间复杂度降为O(n) + * 由于推导过程还涉及一些,这里不在写出(具体可以参考笔记),直接给出结论: + * 1)考虑 j×dp[i−j]这一项。计算 dp[i]的值只需要考虑 j=2和 j=3 的情况 + * 2)考虑(i-j)*j这一项。当i-j大于4时不需要考虑 j×(i−j)的值; i−j<4时,不用考虑j=1;当 j≥4时,计算 dp[i] 只需要考虑 j×dp[i−j],不需要考虑 j×(i−j)。 + * 总结:根据上面两个结论可知:因此无论是考虑 j×dp[i−j]还是考虑 j×(i−j),都只需要考虑 j=2 和 j=3的情况。 + * 状态转移方程可以简化为:dp[i] = max(2*(i-2),2*dp[i-2],3*(i-3),3*dp[i-3]) + * 速度超过100%,内存超过12.19% + * 无需遍历n在每个数中对n进行拆分j,所以时间复杂度为O(n),空间复杂度O(n) + * @param n + * @return + */ + public int integerBreak3(int n) { + if (n <= 3) { + return n - 1; + } + int[] dp = new int[n + 1]; + dp[2] = 1; + for (int i = 3; i <= n; i++) { + dp[i] = Math.max(Math.max(2 * (i - 2), 2 * dp[i - 2]), Math.max(3 * (i - 3), 3 * dp[i - 3])); + } + return dp[n]; + } + + + /** + * 官方贪心算法优化:×3时不用for循环,而是pow指令集 + * 时间复杂度O(1),空间复杂度O(1) + * 速度击败100%,内存击败76.71% + * @param n + * @return + */ + public int integerBreak4(int n) { + if (n <= 3) { + return n - 1; + } + int quotient = n / 3; + int remainder = n % 3; + if (remainder == 0) { + return (int) Math.pow(3, quotient); + } else if (remainder == 1) { + return (int) Math.pow(3, quotient - 1) * 4; + } else { + return (int) Math.pow(3, quotient) * 2; + } + } + + +} diff --git a/Leecode/src/main/java/com/markilue/leecode/dynamic/T07_NumTrees.java b/Leecode/src/main/java/com/markilue/leecode/dynamic/T07_NumTrees.java new file mode 100644 index 0000000..167e2a0 --- /dev/null +++ b/Leecode/src/main/java/com/markilue/leecode/dynamic/T07_NumTrees.java @@ -0,0 +1,105 @@ +package com.markilue.leecode.dynamic; + +import org.junit.Test; + +/** + * @BelongsProject: Leecode + * @BelongsPackage: com.markilue.leecode.dynamic + * @Author: markilue + * @CreateTime: 2022-11-23 13:20 + * @Description: + * TODO 力扣96题 不同的二叉搜索数: + * 给你一个整数 n ,求恰由 n 个节点组成且节点值从 1 到 n 互不相同的 二叉搜索树 有多少种?返回满足题意的二叉搜索树的种数。 + * @Version: 1.0 + */ +public class T07_NumTrees { + + @Test + public void test(){ + int n=5; + System.out.println(numTrees1(n)); + } + + + /** + * 自己尝试动态规划法:动态规划法的精髓在于把当前问题拆分为一些小问题 + * TODO 动态规划法五部曲: + * 1)确定dp的定义:这里好像没有什么特别的,先暂时确定dp[i]表示给定i个节点的二叉搜索树的数量 + * 2)确定状态转移方程:给定的dp[i]的数量可以抽象为哪些小问题?dp[i-1]在把i节点放上去? + * 好像可以:所以抽象为i种:以1为根节点,以2为根节点,以此类推 + * 状态转移方程:dp[i]=1*dp[i-1]+dp[1]*dp[i-2]+dp[2]*dp[i-3]+...+dp[i-1]*1; + * 3)确定初始值:dp[1]=1 + * 4)确定递推顺序:状态转移方程dp[i]=dp[i-1]+dp[i-1] +1;所以是从前往后遍历 + * 5)举例观察dp是否符合意义:1 2 + * 好像不太对,这个状态转移方程尚且有漏洞 + * ram n + * @return + */ + public int numTrees(int n) { + + int[] dp = new int[n+1]; + dp[1]=1; + + for (int i = 2; i <= n; i++) { + for (int j = 1; j < i; j++) { + dp[i]+=j*dp[i-j]; + } + + } + return 0; + + } + + + + /** + * 代码随想录动态规划法:可以发现与本人的思路已经十分接近了,但是还是没有迈出最后一步 + * TODO 动态规划法五部曲: + * 1)确定dp的定义:这里好像没有什么特别的,先暂时确定dp[i]表示给定i个节点的二叉搜索树的数量 + * 2)确定状态转移方程:给定的dp[i]的数量可以抽象为哪些小问题?dp[i-1]在把i节点放上去? + * 好像可以:所以抽象为i种:以1为根节点(左边无元素,右边两个元素),以2为根节点(左边一个元素,右边一个元素),以此类推,到以j为根节点 + * 状态转移方程:dp[i]=dp[0]*dp[i-1]+dp[1]*dp[i-2]+...+dp[j-1]*dp[i-j] + * 3)确定初始值:dp[0]=1 + * 4)确定递推顺序:状态转移方程dp[i]=dp[0]*dp[i-1]+dp[1]*dp[i-2]+...+dp[j-1]*dp[i-j];所以是从前往后遍历 + * 5)举例观察dp是否符合意义:1 1 2 5 + * 速度击败100%,内存击败46.64% + * 时间复杂度O(N^2),空间复杂度O(N) + * ram n + * @return + */ + public int numTrees1(int n) { + + int[] dp = new int[n+1]; + dp[0]=1; + + for (int i = 1; i <= n; i++) { + for (int j = 1; j <= i; j++) { + dp[i]+=dp[j-1]*dp[i-j]; + } + } + return dp[n]; + + } + + + /** + * 官方数学法:事实上我们在方法一中推导出的 G(n)函数的值在数学上被称为卡塔兰数 + * 速度击败100%,内存击败48.43% + * 时间复杂度O(N),空间复杂度O(1) + * ram n + * @return + */ + public int numTrees2(int n) { + // 提示:我们在这里需要用 long 类型防止计算过程中的溢出 + long C = 1; + for (int i = 0; i < n; ++i) { + C = C * 2 * (2 * i + 1) / (i + 2); + } + return (int) C; + + } + + + + +}