JAVA数组题(7)
(1)
思路
- 动态规划定义:定义一个二维数组
dp
,其中dp[i][j]
表示以nums1[i]
和nums2[j]
开头的最长公共子数组的长度。 - 状态转移方程:
- 如果
nums1[i]
等于nums2[j]
,则dp[i][j]
等于dp[i+1][j+1] + 1
。 - 否则,
dp[i][j]
为 0。
- 如果
- 遍历方向:由于每个状态
dp[i][j]
依赖于dp[i+1][j+1]
,我们需要从后向前遍历数组,即从i = n-1
到0
,从j = m-1
到0
。 - 结果记录:在遍历过程中,记录遇到的最大
dp[i][j]
值,即为最长公共子数组的长度。
代码
class Solution {public int findLength(int[] nums1, int[] nums2) {int n = nums1.length, m = nums2.length;int[][] dp = new int[n + 1][m + 1];int ans = 0;for (int i = n - 1; i >= 0; i--) {for (int j = m - 1; j >= 0; j--) {dp[i][j] = nums1[i] == nums2[j] ? dp[i + 1][j + 1] + 1 : 0;ans = Math.max(ans, dp[i][j]);}}return ans;}
}
解释
- 初始化:创建一个
(n+1) x (m+1)
的二维数组dp
,其中dp[n][j]
和dp[i][m]
初始化为 0,用于处理边界情况。 - 状态转移:遍历每个可能的起始位置
(i, j)
,如果当前元素相等,则更新dp[i][j]
为后续公共子数组长度加 1,否则为 0。 - 结果更新:在每次更新
dp[i][j]
后,检查是否为当前最长的公共子数组长度,并更新结果ans
。
dp[i][j]=nums1[i]==nums2[j]?dp[i+1][j+1]+1:0;
这段代码实现了一个经典的动态规划解法,用于计算两个序列的最长公共子串(Longest Common Substring)的长度。
- 状态定义:
dp[i][j]
表示以nums1[i]
和nums2[j]
开头的最长公共子串的长度。 - 状态转移:
- 如果
nums1[i]
等于nums2[j]
,则dp[i][j]
等于dp[i+1][j+1] + 1
。 - 否则,
dp[i][j]
为 0(因为当前字符不匹配,无法构成公共子串)。
- 如果
- 结果:遍历所有可能的起始位置
(i, j)
,取最大值即为最长公共子串的长度。
dp[i+1][j+1]
的使用与状态转移的方向有关。这里采用的是从后向前的遍历方式,因此当前状态 dp[i][j]
需要依赖于后续状态 dp[i+1][j+1]
。
实例说明:
假设 nums1 = [1, 2, 3]
,nums2 = [2, 3, 4]
,公共子串是 [2, 3]
。
动态规划数组 dp
的填充过程如下:
i\j | 0 | 1 | 2 | |
---|---|---|---|---|
0 | 0 | 0 | 0 | |
1 | 2 | 0 | 0 | ← dp[1][0] = dp[2][1] + 1 = 1 + 1 = 2 |
2 | 0 | 1 | 0 | ← dp[2][1] = dp[3][2] + 1 = 0 + 1 = 1 |
- 当
i=1, j=0
时,nums1[1] == nums2[0] == 2
,因此dp[1][0]
等于dp[2][1] + 1
。dp[2][1]
表示以nums1[2]
和nums2[1]
开头的最长公共子串长度(即3
和3
,长度为 1)。- 因此,
dp[1][0] = 1 + 1 = 2
,对应公共子串[2, 3]
。
为什么不是 i-1
?
如果使用 dp[i-1][j-1]
,则状态定义会变为以 nums1[i-1]
和 nums2[j-1]
结尾的最长公共子串,此时需要从前向后遍历(i
和 j
从小到大递增)
def longest_common_substring(nums1, nums2):m, n = len(nums1), len(nums2)dp = [[0] * (n + 1) for _ in range(m + 1)]max_len = 0for i in range(1, m + 1):for j in range(1, n + 1):if nums1[i-1] == nums2[j-1]: # 注意索引调整dp[i][j] = dp[i-1][j-1] + 1 # 依赖前一个状态max_len = max(max_len, dp[i][j])return max_len
(2)
思路和算法
- 排序:首先对单词列表进行排序,排序规则为先按长度升序排列,如果长度相同则按字典序降序排列。这样可以确保我们在遍历过程中先处理较短的单词,并且在长度相同时优先考虑字典序较小的单词。
- 哈希集合:使用一个哈希集合来存储所有可以由其他单词逐步构建的单词。初始时,集合中只包含空字符串。
- 遍历检查:遍历排序后的单词列表,对于每个单词,检查它去掉最后一个字母后的子串是否存在于哈希集合中。如果存在,则将当前单词加入哈希集合,并更新最长单词的结果。
代码
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;class Solution {public String longestWord(String[] words) {// 先按长度升序,长度相同按字典序降序Arrays.sort(words, (a, b) -> {if (a.length() != b.length()) {return a.length() - b.length();} else {return b.compareTo(a);}});Set<String> set = new HashSet<>();set.add("");String longest = "";for (String word : words) {String prefix = word.substring(0, word.length() - 1);if (set.contains(prefix)) {set.add(word);longest = word;}}return longest;}
}
代码解释
- 排序:使用
Arrays.sort
对单词列表进行自定义排序,确保较短的单词排在前面,长度相同的单词按字典序降序排列。 - 哈希集合初始化:创建一个哈希集合
set
,并添加空字符串作为初始元素,因为空字符串是所有单词的基础。 - 遍历检查:遍历排序后的单词列表,对于每个单词,提取其前缀(即去掉最后一个字母的子串),检查该前缀是否存在于哈希集合中。如果存在,则将当前单词添加到集合中,并更新最长单词为当前单词。
自定义排序
Arrays.sort(words, (a, b) -> {if (a.length() != b.length()) {return a.length() - b.length();} else {return b.compareTo(a);}});
使用了 Lambda 表达式定义排序规则。排序规则解析
- 按长度升序:如果两个字符串长度不同,较短的字符串排在前面。
- 按字典序降序:如果两个字符串长度相同,字典序较大的字符串排在前面。
具体来说:
a.length() - b.length()
:当a
比b
短时返回负数,a
排在b
前;反之则b
排在a
前。b.compareTo(a)
:当b
的字典序比a
大时返回正数,b
排在a
前(即字典序降序)。
考虑示例输入:["a", "ab", "abc", "abd"]
排序后顺序为:["a", "ab", "abd", "abc"]
(长度相同的abc
和abd
按字典序降序排列)。
当遍历到"abc"
时,由于"ab"
已存在于集合中,"abc"
会被加入结果;而"abd"
虽然也符合条件,但由于排在后面,不会覆盖"abc"
。