leecode更新
This commit is contained in:
parent
85339939f6
commit
0fcc05eba4
|
|
@ -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];
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue