diff --git a/Leecode/src/main/java/com/markilue/leecode/dynamic/T13_CombinationSum4.java b/Leecode/src/main/java/com/markilue/leecode/dynamic/T13_CombinationSum4.java new file mode 100644 index 0000000..9954306 --- /dev/null +++ b/Leecode/src/main/java/com/markilue/leecode/dynamic/T13_CombinationSum4.java @@ -0,0 +1,61 @@ +package com.markilue.leecode.dynamic; + +import org.junit.Test; + +/** + * @BelongsProject: Leecode + * @BelongsPackage: com.markilue.leecode.dynamic + * @Author: markilue + * @CreateTime: 2022-12-06 10:45 + * @Description: + * TODO 力扣377题 组合总和IV: + * 给你一个由 不同 整数组成的数组 nums ,和一个目标整数 target 。请你从 nums 中找出并返回总和为 target 的元素组合的个数。 + * 题目数据保证答案符合 32 位整数范围。 + * @Version: 1.0 + */ +public class T13_CombinationSum4 { + + @Test + public void test(){ + int[] nums = {1, 2, 3}; + int target = 4; + System.out.println(combinationSum4(nums,target)); + } + + @Test + public void test1(){ + int[] nums = {9}; + int target = 3; + System.out.println(combinationSum4(nums,target)); + } + + + /** + * 思路:由题意知:顺序不同的序列被视作不同的组合,因此这个一个排列题: + * TODO 动态规划五部曲: + * (1)dp定义:dp[i][j] 表示使用nums[0-i]可以构成j的方法数dp[i][j] + * (2)dp状态转移方程:dp[i][j]可以由不要num[i]的前nums[i-1]个数构成;或者要num[i]的dp[i-1][j-num[i]] + * (3)dp初始化:dp[i][0]=1且dp[0][j%num[0]==0]=1 + * (4)dp遍历顺序:由于是排列问题,因此先背包后num[i] + * (5)dp举例推导: + * @param nums + * @param target + * @return + */ + public int combinationSum4(int[] nums, int target) { + + int[] dp = new int[target + 1]; + + dp[0]=1; + + for (int i = 0; i < target + 1; i++) { + for (int j = 0; j < nums.length; j++) { + if (i 实际上可以转换为可以从nums=[1,2]中选爬1或者2 + * (2)确定递推公式:题目已经把递推公式给我们了:状态转移方程 dp[i] +=dp[i-num[i]] + * (3)dp数组如何初始化:dp[0] = 1 因为在一楼就是num[i]一个都不选就可以到达 + * (4)确定遍历顺序:实际上就是先遍历背包在遍历楼梯num[i] + * (5)举例推导dp数组:dp数组应该是如下的数列: + * 速度击败100%,内存击败5.7% + * @param n + * @return + */ + public int climbStairs3(int n) { + int[] nums = {1,2}; + int[] dp = new int[n+1]; + dp[0]=1; + for (int i = 0; i < n + 1; i++) { + //可以理解理解为最后爬num[i]层到达,事实上前面怎么爬的不管,也就是排列问题 + for (int j = 0; j < nums.length; j++) { + if (i>=nums[j]){ + dp[i]+=dp[i-nums[j]]; + } + } + } + return dp[n]; + } + + + + + +} diff --git a/Leecode/src/main/java/com/markilue/leecode/dynamic/T15_CoinChange.java b/Leecode/src/main/java/com/markilue/leecode/dynamic/T15_CoinChange.java new file mode 100644 index 0000000..999afc8 --- /dev/null +++ b/Leecode/src/main/java/com/markilue/leecode/dynamic/T15_CoinChange.java @@ -0,0 +1,228 @@ +package com.markilue.leecode.dynamic; + +import org.junit.Test; + +import java.util.Arrays; + +/** + * @BelongsProject: Leecode + * @BelongsPackage: com.markilue.leecode.dynamic + * @Author: markilue + * @CreateTime: 2022-12-06 12:05 + * @Description: TODO 力扣322题 零件兑换: + * 给你一个整数数组 coins ,表示不同面额的硬币;以及一个整数 amount ,表示总金额。 + * 计算并返回可以凑成总金额所需的 最少的硬币个数 。如果没有任何一种硬币组合能组成总金额,返回 -1 。 + * 你可以认为每种硬币的数量是无限的。 + * @Version: 1.0 + */ +public class T15_CoinChange { + + @Test + public void test() { + int[] coins = {1, 2, 5}; + int amount = 11; + + System.out.println(coinChange1(coins, amount)); + } + + @Test + public void test1() { + int[] coins = {2, 5, 10, 1}; + int amount = 27; + + System.out.println(coinChange(coins, amount)); + } + + @Test + public void test2() { + int[] coins = {186,419,83,408}; + int amount = 6249; + + System.out.println(coinChange(coins, amount)); + } + + + /** + * 思路:由于题目要求凑出金额所需最少的硬币个数,符合背包问题常见的字眼(最少最大,true or false,排列组合方法) =>错误 + * 因此可以把dp就定义为凑金币所需最少金币数 + * TODO 动规五部曲: + * (1)dp的定义:dp[j]表示使用coins[0-i]能凑出j的最少硬币数 + * (2)dp的状态转移方程: dp[j]=min(dp[j],dp[j-coins[i]]+1) + * (3)dp的初始化:dp[0]=Integer.maxvalue i=0时 dp[j%coin[i]==0]=j/coin[i] + * (4)dp的遍历顺序:由于使用coins的顺序无所谓,因此两个for的顺序无所谓,这里先coins后背包 先coins在背包会出错, + * 因为如果小钱在后面前面很多都是用不了小钱的,但实际上小钱都可以用Test1无法通过 + * 事实上是可以的,需要添加一个判断if(dp[j-coins[i]]!=Integer.MAX_VALUE),即条件成立时,该位才有选择的必要 + * (5)dp的举例推导:当test时,dp: + * [0 1 2 3 4 5 6 7 8 9 10 11] + * i=0 0 1 2 3 4 5 6 7 8 9 10 11 + * i=1 0 1 1 2 2 3 3 4 4 5 5 6 + * i=2 0 1 1 2 2 1 2 2 3 3 2 3 + * 根据官方代码简化后 速度击败97.78%,内存击败36.17% + * @param coins + * @param amount + * @return + */ + public int coinChange(int[] coins, int amount) { + + int[] dp = new int[amount + 1]; + int max=amount+1; + dp[0] = 0; + for (int i = 1; i < dp.length; i++) { + dp[i] = max; + } + + for (int i = 0; i < coins.length; i++) { + for (int j = coins[i]; j < dp.length; j++) { + if(dp[j-coins[i]]!=max)//这个判断理论上来说可以去掉,可以看这句和下句那个更费时 + dp[j] = Math.min(dp[j], dp[j - coins[i]] + 1);//如果Integer.MAX_VALUE+1就等于负数了,直接选了 + } + } + + return dp[amount] == max ? -1 : dp[amount]; + + + } + + + /** + * 思路:由于题目要求凑出金额所需最少的硬币个数,符合背包问题常见的字眼(最少最大,true or false,排列组合方法) + * 因此可以把dp就定义为凑金币所需最少金币数 + * TODO 动规五部曲: + * (1)dp的定义:dp[j]表示使用coins[0-i]能凑出j的最少硬币数 + * (2)dp的状态转移方程: dp[j]=min(dp[j],dp[j-coins[i]]+1) + * (3)dp的初始化:dp[0]=Integer.maxvalue i=0时 dp[j%coin[i]==0]=j/coin[i] + * (4)dp的遍历顺序:由于使用coins的顺序有所谓,因为如果coins小的在后面可能会在前面用不到,所以coins循环需要在内部,即最后一个硬币可以是任意 + * (5)dp的举例推导:当test时,dp: + * [0 1 2 3 4 5 6 7 8 9 10 11] + * i=0 0 1 2 3 4 5 6 7 8 9 10 11 + * i=1 0 1 1 2 2 3 3 4 4 5 5 6 + * i=2 0 1 1 2 2 1 2 2 3 3 2 3 + * 速度击败87.61%,内存击败10.73% + * @param coins + * @param amount + * @return + */ + public int coinChange1(int[] coins, int amount) { + + int[] dp = new int[amount + 1]; + + dp[0] = 0; + for (int i = 1; i < dp.length; i++) { + //由于coins[i]最小为1,所以dp[i]最大为amount,一定会小于amount+1 + dp[i] = i % coins[0] == 0 ? i / coins[0] : amount+1; + } + + + for (int j = 0; j < dp.length; j++) { + for (int i = 1; i < coins.length; i++) { + if (j >= coins[i]) { + dp[j] = Math.min(dp[j], dp[j - coins[i]] + 1); + } + } + } + + return dp[amount] == amount+1? -1 : dp[amount]; + + + } + + + /** + * 代码随想录方法:钱币有顺序和没有顺序都可以,都不影响钱币的最小个数。所以内外for循环都可以 + * 速度击败97.78%,内存击败79.89% + * 快在我的for循环还需要判断一次是否能整除 + * @param coins + * @param amount + * @return + */ + public int coinChange2(int[] coins, int amount) { + int max = Integer.MAX_VALUE; + int[] dp = new int[amount + 1]; + //初始化dp数组为最大值 + for (int j = 0; j < dp.length; j++) { + dp[j] = max; + } + //当金额为0时需要的硬币数目为0 + dp[0] = 0; + for (int i = 0; i < coins.length; i++) { + //正序遍历:完全背包每个硬币可以选择多次 + for (int j = coins[i]; j <= amount; j++) { + //只有dp[j-coins[i]]不是初始最大值时,该位才有选择的必要 + if (dp[j - coins[i]] != max) { + //选择硬币数目最小的情况 + dp[j] = Math.min(dp[j], dp[j - coins[i]] + 1); + } + } + } + return dp[amount] == max ? -1 : dp[amount]; + } + + + /** + * 官方记忆化搜索法:本质其实就是动态规划法,但是通过dfs实现 + * 即假设最后一枚硬币为C,那么F(S)=F(S-C)+1,而最后一张的面值可以是coins[i],因此遍历,具体可以查看笔记 + * 时间复杂度和空间复杂度与动态规划法一致 + * 但速度击败6.66%,内存击败5.94% 38ms + * @param coins + * @param amount + * @return + */ + public int coinChange3(int[] coins, int amount) { + if (amount < 1) { + return 0; + } + return coinChange(coins, amount, new int[amount]); + } + + private int coinChange(int[] coins, int rem, int[] count) { + if (rem < 0) { + return -1; + } + if (rem == 0) { + return 0; + } + if (count[rem - 1] != 0) { + return count[rem - 1]; + } + int min = Integer.MAX_VALUE; + for (int coin : coins) { + int res = coinChange(coins, rem - coin, count); + if (res >= 0 && res < min) { + min = 1 + res; + } + } + count[rem - 1] = (min == Integer.MAX_VALUE) ? -1 : min; + return count[rem - 1]; + } + + + /** + * 评论区贪心+dfs法:本质上就是优先选择硬币面值较大的方法,但实际上就是暴力解法,但是利用贪心剪枝,效果可能比动规还好 + * 速度击败99% 8ms + * 由于本质上是暴力解法,如果案例不好可能会超时 + */ + int res = Integer.MAX_VALUE; + public int coinChange4(int[] coins, int amount){ + if(amount==0){ + return 0; + } + Arrays.sort(coins); + mincoin(coins,amount,coins.length-1,0); + return res==Integer.MAX_VALUE? -1:res; + } + private void mincoin(int[] coins,int amount, int index, int count){ + if(amount==0){ + res = Math.min(res,count); + return; + } + if(index<0){ + return; + } + for(int i = amount/coins[index];i>=0 && i+count