From 600003ba7f6721ebf84b70dc295b07be5fa5e596 Mon Sep 17 00:00:00 2001 From: markilue <745518019@qq.com> Date: Fri, 16 Dec 2022 14:52:30 +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 --- .../leecode/dynamic/T29_FindLength.java | 275 ++++++++++++++++++ .../dynamic/T30_LongestCommonSubsequence.java | 230 +++++++++++++++ 2 files changed, 505 insertions(+) create mode 100644 Leecode/src/main/java/com/markilue/leecode/dynamic/T29_FindLength.java create mode 100644 Leecode/src/main/java/com/markilue/leecode/dynamic/T30_LongestCommonSubsequence.java diff --git a/Leecode/src/main/java/com/markilue/leecode/dynamic/T29_FindLength.java b/Leecode/src/main/java/com/markilue/leecode/dynamic/T29_FindLength.java new file mode 100644 index 0000000..340d861 --- /dev/null +++ b/Leecode/src/main/java/com/markilue/leecode/dynamic/T29_FindLength.java @@ -0,0 +1,275 @@ +package com.markilue.leecode.dynamic; + +import org.junit.Test; + +import java.util.HashSet; +import java.util.Set; + +/** + *@BelongsProject: Leecode + *@BelongsPackage: com.markilue.leecode.dynamic + *@Author: dingjiawen + *@CreateTime: 2022-12-16 09:51 + *@Description: + * TODO 力扣718题 最长重复子数组: + * 给两个整数数组 nums1 和 nums2 ,返回 两个数组中 公共的 、长度最长的子数组的长度。 + * 经过测试子数组必须连续,即[3,2,1]和[3,1,2,1],其最长子数组为2而不是3 + *@Version: 1.0 + */ +public class T29_FindLength { + + @Test + public void test() { + int[] nums1 = {1, 2, 3, 2, 1}; + int[] nums2 = {3, 2, 1, 4, 7}; + System.out.println(findLength1(nums1, nums2)); + } + + @Test + public void test1() { + int[] nums1 = {1,2,3,1,2,1}; + int[] nums2 = {3,2,1,4,7}; + System.out.println(findLength2(nums1, nums2)); + } + + /** + * 思路:最长子数组既然必须要求是连续的,那么跟28其实是比较类似的,就是需要找到对应的情况 + * 比如[3,2,1]和[3,2,3,2,1],代码随想录的思路是,直接全都遍历了就知道了,所以时间复杂度O(N*M) + * TODO 代码随想录动态规划法:题目中说的子数组,其实就是连续子序列。这种问题动规最拿手 + * (1)dp定义:dp[i][j] 表示以下标i - 1为结尾的A,和以下标j - 1为结尾的B,最长重复子数组长度为dp[i][j]。 + * (特别注意: “以下标i - 1为结尾的A” 标明一定是 以A[i-1]为结尾的字符串) + * 其实dp[i][j]的定义也就决定着,我们在遍历dp[i][j]的时候i 和 j都要从1开始。 + * (2)dp状态转移方程:即当A[i - 1] 和B[j - 1]相等的时候,dp[i][j] = dp[i - 1][j - 1] + 1; + * (3)dp初始化:根据dp[i][j]的定义,dp[i][0] 和dp[0][j]其实都是没有意义的! + * 为了方便递归公式dp[i][j] = dp[i - 1][j - 1] + 1所以dp[i][0] 和dp[0][j]初始化为0。 + * (4)dp遍历顺序:外层for循环遍历A,内层for循环遍历B。 + * (5)dp举例推导:A: [1,2,3,2,1],B: [3,2,1,4,7]为例 + * B: 3 2 1 4 7 + * A: 0 0 0 0 0 0 + * 1 0 0 0 1 0 0 + * 2 0 0 1 0 0 0 + * 3 0 1 0 0 0 0 + * 2 0 1 2 0 0 0 + * 1 0 0 0 3 0 0 + * 速度击败71.18%,内存击败24.68% + * 时间复杂度为O(N*M) + * @param nums1 + * @param nums2 + * @return + */ + public int findLength(int[] nums1, int[] nums2) { + + int[][] dp = new int[nums1.length + 1][nums2.length + 1]; + int result = 0; + + for (int i = 1; i < dp.length; i++) { + for (int j = 1; j < dp[0].length; j++) { + if (nums1[i - 1] == nums2[j - 1]) dp[i][j] = dp[i - 1][j - 1] + 1; + if (result < dp[i][j]) result = dp[i][j]; + } + } + + return result; + } + + + /** + * 滚动数组优化思路: + *速度击败44.24%,内存击败88.2% + * @param nums1 + * @param nums2 + * @return + */ + public int findLength1(int[] nums1, int[] nums2) { + + int[] dp = new int[nums2.length + 1]; + int result = 0; + + for (int i = 1; i < nums1.length + 1; i++) { + for (int j = nums2.length + 1 - 1; j >= 1; j--) { + if (nums1[i - 1] == nums2[j - 1]) { + dp[j] = dp[j - 1] + 1; + }else { + dp[j]=0; //如果不等需要重新赋值,避免不覆盖的情况;而二维的时候不用,因为不用重复利用 + } + if (result < dp[j]) result = dp[j]; + } + } + + return result; + } + + + /** + * 官方 滑动窗口法:从A来一遍又从B来一遍,目的是防止[3,2,1]和[3,2,3,2,1],出现两次3,2的情况 + * 时间复杂度O((N+M)*min(N,M)),空间复杂度O(1) + * 速度击败38.34%,内存击败84.78% + * @param nums1 + * @param nums2 + * @return + */ + public int findLength2(int[] nums1, int[] nums2) { + int n = nums1.length, m = nums2.length; + int ret = 0; + for (int i = 0; i < n; i++) { + int len = Math.min(m, n - i); + int maxlen = maxLength(nums1, nums2, i, 0, len); + ret = Math.max(ret, maxlen); + } + for (int i = 0; i < m; i++) { + int len = Math.min(n, m - i); + int maxlen = maxLength(nums1, nums2, 0, i, len); + ret = Math.max(ret, maxlen); + } + return ret; + } + + //寻找A从addA开始和B从addB开始,连续相同的最大值 + public int maxLength(int[] A, int[] B, int addA, int addB, int len) { + int ret = 0, k = 0; + for (int i = 0; i < len; i++) { + if (A[addA + i] == B[addB + i]) { + k++; + } else { + k = 0; + } + ret = Math.max(ret, k); + } + return ret; + } + + + /** + * 官方二分查找+哈希表法,不细研究: + * 核心思想是如果数组 A 和 B 有一个长度为 k 的公共子数组,那么它们一定有长度为 j <= k 的公共子数组。 + * 这样我们可以通过二分查找的方法找到最大的 k。 + */ + int mod = 1000000009; + int base = 113; + + public int findLength3(int[] A, int[] B) { + int left = 1, right = Math.min(A.length, B.length) + 1; + while (left < right) { + int mid = (left + right) >> 1; + if (check(A, B, mid)) { + left = mid + 1; + } else { + right = mid; + } + } + return left - 1; + } + + public boolean check(int[] A, int[] B, int len) { + long hashA = 0; + for (int i = 0; i < len; i++) { + hashA = (hashA * base + A[i]) % mod; + } + Set bucketA = new HashSet(); + bucketA.add(hashA); + long mult = qPow(base, len - 1); + for (int i = len; i < A.length; i++) { + hashA = ((hashA - A[i - len] * mult % mod + mod) % mod * base + A[i]) % mod; + bucketA.add(hashA); + } + long hashB = 0; + for (int i = 0; i < len; i++) { + hashB = (hashB * base + B[i]) % mod; + } + if (bucketA.contains(hashB)) { + return true; + } + for (int i = len; i < B.length; i++) { + hashB = ((hashB - B[i - len] * mult % mod + mod) % mod * base + B[i]) % mod; + if (bucketA.contains(hashB)) { + return true; + } + } + return false; + } + + // 使用快速幂计算 x^n % mod 的值 + public long qPow(long x, long n) { + long ret = 1; + while (n != 0) { + if ((n & 1) != 0) { + ret = ret * x % mod; + } + x = x * x % mod; + n >>= 1; + } + return ret; + } + + + /** + * 官方题解中,最快的方法: + * 似乎是二分查找+哈希表法 + * 5ms + * 速度击败100%,内存击败82.24% + */ + long a = 13131;//形象地说,就是把 S 看成一个类似 base 进制的数(左侧为高位,右侧为低位),它的十进制值就是这个它的哈希值。 + + int N; + int M; + + public int findLength4(int[] nums1, int[] nums2) { + N = nums1.length; + M = nums2.length; + + int l = 0; + int r = Math.min(N, M); + + while(l < r){ + int mid = l + r + 1 >> 1; + //寻找有没有长度为k的公共子数组,找到了就说明可能有更大的 + if(findDup(nums1, nums2, mid)) l = mid; + else r = mid - 1; + } + + return l; + } + + public boolean findDup(int[] nums1, int[] nums2, int len){ + //为了便于理解进行举例:若nums1={1,2,3,1,2,1};nums2={3,2,1,4,7} + long h1 = 0; + long h2 = 0; + long al = 1; + + Set set = new HashSet<>(); + + //计算[0-len]的nums1子数组哈希值 + for(int i = 0; i < len; i++){ + //a进制下的123 + h1 = h1 * a + nums1[i]; + //a进制下的1000 + al = al * a; + } + set.add(h1); + + //计算所有长度为len的子数组哈希值 + for(int i = 1; i <= N - len; i++){ + //以i=1为例 + h1 = h1 * a;//1230 + h1 = h1 - nums1[i - 1] * al;//1230-1000 + h1 = h1 + nums1[i + len - 1];//231 + set.add(h1); + } + //计算[0-len]的nums1子数组哈希值 + for(int i = 0; i < len; i++) h2 = h2 * a + nums2[i]; + + if(set.contains(h2)) return true; + //计算所有len长的的nums2子数组哈希值 + for(int i = 1; i <= M - len; i++){ + h2 = h2 * a; + h2 = h2 - nums2[i - 1] * al; + h2 = h2 + nums2[i + len - 1]; + + if(set.contains(h2)) return true;//只要找到了就返回找到 + } + + return false; + } + + +} diff --git a/Leecode/src/main/java/com/markilue/leecode/dynamic/T30_LongestCommonSubsequence.java b/Leecode/src/main/java/com/markilue/leecode/dynamic/T30_LongestCommonSubsequence.java new file mode 100644 index 0000000..6ab0841 --- /dev/null +++ b/Leecode/src/main/java/com/markilue/leecode/dynamic/T30_LongestCommonSubsequence.java @@ -0,0 +1,230 @@ +package com.markilue.leecode.dynamic; + +import org.junit.Test; + +/** + *@BelongsProject: Leecode + *@BelongsPackage: com.markilue.leecode.dynamic + *@Author: dingjiawen + *@CreateTime: 2022-12-16 12:51 + *@Description: + * TODO 力扣1143题 最长公共子序列: + * 给定两个字符串 text1 和 text2,返回这两个字符串的最长 公共子序列 的长度。如果不存在 公共子序列 ,返回 0 。 + * 一个字符串的 子序列 是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。 + * 例如,"ace" 是 "abcde" 的子序列,但 "aec" 不是 "abcde" 的子序列。 + * 两个字符串的 公共子序列 是这两个字符串所共同拥有的子序列。 + *@Version: 1.0 + */ +public class T30_LongestCommonSubsequence { + + @Test + public void test(){ + String text2 = "abcacde"; + String text1 = "acfe"; + System.out.println(longestCommonSubsequence1(text1,text2)); + } + + @Test + public void test1(){ + String text2 = "abcba"; + String text1 = "abcbcba"; + System.out.println(longestCommonSubsequence1(text1,text2)); + } + + /** + * 思路:与T29类似,只是将连续条件去掉了,变得可以不连续: + * TODO 动态规划五部曲: + * (1)dp定义:dp[i][j]表示以i-1结尾的text1和以j-1结尾的text2的最长公共子序列 + * (2)dp状态转移方程: for j in (0,len-(i-1) ) if(num[i-1]==num[i-1+j:]) dp[i][j]=max(dp[i-1])+1 + * (3)dp初始化:dp[0]=0 + * (4)dp遍历顺序:两个for可以随意交换顺序 + * (5)dp举例推导:以 text2 = "abcacde", text1 = "acfe" 为例 + * text2: a b c a c d e + * text1: 0 0 0 0 0 0 0 0 + * a: 0 1 1 1 1 1 1 1 + * c: 0 1 1 2 2 2 2 2 + * f: 0 1 1 2 2 2 2 2 + * e: 0 1 1 2 2 2 2 3 + * 速度击败39.62%,内存击败47.86% 11ms + * @param text1 + * @param text2 + * @return + */ + public int longestCommonSubsequence(String text1, String text2) { + + int[][] dp = new int[text1.length() + 1][text2.length() + 1]; + + for (int i = 1; i < text1.length()+1; i++) { + char char1 = text1.charAt(i-1); + for (int j = 1; j < text2.length() + 1; j++) { + if(char1==text2.charAt(j-1)){ + //不要text1[i-1]大,还是不要text2[j-1]大,还是都要大 + dp[i][j]=Math.max(dp[i][j-1],Math.max(dp[i-1][j],dp[i-1][j-1]+1)); + }else { + //不要text1[i-1]大,还是不要text2[j-1]大 + dp[i][j]=Math.max(dp[i-1][j],dp[i][j-1]); + } + } + } + return dp[text1.length()][text2.length()]; + + + } + + /** + * 对于自己的浅浅优化一下:不需要判断不要text2[j-1]大, + * 因为他事实上取决于它的[j-2]和[i-1][j-2]谁大,所以他一定比[i-1]+1小 + * 速度击败65.48%,内存击败34.92% 10ms + * @param text1 + * @param text2 + * @return + */ + public int longestCommonSubsequence1(String text1, String text2) { + + int[][] dp = new int[text1.length() + 1][text2.length() + 1]; + + for (int i = 1; i < text1.length()+1; i++) { + char char1 = text1.charAt(i-1); + for (int j = 1; j < text2.length() + 1; j++) { + if(char1==text2.charAt(j-1)){ + //不要text1[i-1]大,还是都要大 + dp[i][j]=Math.max(dp[i][j-1],dp[i-1][j-1]+1); + }else { + //不要text1[i-1]大,还是不要text2[j-1]大 + dp[i][j]=Math.max(dp[i-1][j],dp[i][j-1]); + } + } + } + return dp[text1.length()][text2.length()]; + + + } + + /** + * 对于代码随想录进一步优化一下:不需要判断dp[i][j]=Math.max(dp[i][j-1],dp[i-1][j-1]+1); + * 因为他事实上dp[i-1][j-1]+1一定比dp[i][j-1]大 + * 速度击败77.38%,内存击败34.92% 9ms + * @param text1 + * @param text2 + * @return + */ + public int longestCommonSubsequence2(String text1, String text2) { + + int[][] dp = new int[text1.length() + 1][text2.length() + 1]; + + for (int i = 1; i < text1.length()+1; i++) { + char char1 = text1.charAt(i-1); + for (int j = 1; j < text2.length() + 1; j++) { + if(char1==text2.charAt(j-1)){ + //不要text1[i-1]大,还是都要大 + dp[i][j]=dp[i-1][j-1]+1; + }else { + //不要text1[i-1]大,还是不要text2[j-1]大 + dp[i][j]=Math.max(dp[i-1][j],dp[i][j-1]); + } + } + } + return dp[text1.length()][text2.length()]; + + + } + + /** + * 一维dp数组优化 + * 速度击败77.38%,内存击败97.17% + * @param text1 + * @param text2 + * @return + */ + public int longestCommonSubsequence3(String text1, String text2) { + int n1 = text1.length(); + int n2 = text2.length(); + + // 多从二维dp数组过程分析 + // 关键在于 如果记录 dp[i - 1][j - 1] + // 因为 dp[i - 1][j - 1] dp[j - 1] <=> dp[i][j - 1] + int [] dp = new int[n2 + 1]; + + for(int i = 1; i <= n1; i++){ + + // 这里pre相当于 dp[i - 1][j - 1] + int pre = dp[0]; + for(int j = 1; j <= n2; j++){ + + //用于给pre赋值 + int cur = dp[j]; + if(text1.charAt(i - 1) == text2.charAt(j - 1)){ + //这里pre相当于dp[i - 1][j - 1] 千万不能用dp[j - 1] !! + //或者倒序遍历也行 + dp[j] = pre + 1; + } else{ + // dp[j] 相当于 dp[i - 1][j] + // dp[j - 1] 相当于 dp[i][j - 1] + dp[j] = Math.max(dp[j], dp[j - 1]); + } + + //更新dp[i - 1][j - 1], 为下次使用做准备 + pre = cur; + } + } + + return dp[n2]; + } + + /** + * 官方题解中合理且最快的方法:本质上还是动态规划, + * 但是优化在选取text长度最小的作为内层for,并将string变成数组后进行判断 + * 速度击败99.96%,内存击败98.16% 3ms + * @param text1 + * @param text2 + * @return + */ + public int longestCommonSubsequence4(String text1, String text2) { + //动态规划 + //f(n) f(n-1) + //f(1) 递推 + //确保text1最长 + if(text1.length()