leecode更新

This commit is contained in:
markilue 2022-11-23 14:49:51 +08:00
parent 85339939f6
commit 0fcc05eba4
3 changed files with 375 additions and 0 deletions

View File

@ -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];
}
}

View File

@ -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[ij]这一项计算 dp[i]的值只需要考虑 j=2和 j=3 的情况
* 2)考虑(i-j)*j这一项当i-j大于4时不需要考虑 j×(ij)的值 ij<4时不用考虑j=1; j4时计算 dp[i] 只需要考虑 j×dp[ij],不需要考虑 j×(ij)
* 总结根据上面两个结论可知因此无论是考虑 j×dp[ij]还是考虑 j×(ij)都只需要考虑 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;
}
}
}

View File

@ -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;
}
}