问题描述
给定一个整数 n
和两个长度均为 n
的数组 words
和 groups
:
-
words
是字符串数组 -
groups
是整数数组
我们需要找到一个最长的子序列,满足以下条件:
-
相邻元素的
groups
值不同 -
相邻元素的字符串长度相同且汉明距离为1(即只有一个字符不同)
初步思考
什么是子序列?
子序列是指从原数组中删除一些(或不删除)元素后,剩余元素保持原有顺序的新数组。
关键条件分析
-
groups值不同:相邻元素的groups值必须不同
-
字符串条件:
-
长度相同
-
汉明距离为1(仅一个字符不同)
-
暴力解法思路
最直观的方法是枚举所有可能的子序列,然后检查每个子序列是否满足条件。但这种方法的时间复杂度为O(2^n),显然不适用于较大的n。
动态规划解法
动态规划状态定义
定义 dp[i]
表示以第i个元素结尾的最长子序列长度,prev[i]
记录前驱节点。
状态转移方程
对于每个i,检查所有j < i:
-
groups[j] != groups[i]
-
words[j].length() == words[i].length()
-
汉明距离为1
如果满足条件,则:
dp[i] = max(dp[i], dp[j] + 1)
基础实现
java
public List<String> basicDPSolution(String[] words, int[] groups) { int n = words.length; int[] dp = new int[n]; int[] prev = new int[n]; Arrays.fill(dp, 1); Arrays.fill(prev, -1); int maxLen = 1; int maxIndex = 0; for (int i = 0; i < n; i++) { for (int j = 0; j < i; j++) { if (groups[j] != groups[i] && words[j].length() == words[i].length() && isHammingOne(words[i], words[j])) { if (dp[j] + 1 > dp[i]) { dp[i] = dp[j] + 1; prev[i] = j; } } } if (dp[i] > maxLen) { maxLen = dp[i]; maxIndex = i; } } // 回溯路径... }
这个基础实现的时间复杂度是O(n^2),对于n=10^5来说仍然不够高效。
优化策略
优化1:按字符串长度分组
观察到相邻元素必须长度相同,我们可以预先按字符串长度分组:
java
Map<Integer, List<Integer>> lenMap = new HashMap<>(); for (int i = 0; i < n; i++) { int len = words[i].length(); lenMap.computeIfAbsent(len, k -> new ArrayList<>()).add(i); }
这样对于每个i,我们只需要检查相同长度的j。
优化2:按dp值排序
在检查可能的j时,按dp[j]降序排列,这样找到第一个满足条件的j就可以停止:
java
possibleJs.sort((a, b) -> Integer.compare(dp[b], dp[a])); for (int j : possibleJs) { if (isHammingOne(words[i], words[j])) { if (dp[j] + 1 > dp[i]) { dp[i] = dp[j] + 1; prev[i] = j; break; } } }
优化3:汉明距离计算的优化
可以在比较时提前终止:
java
private boolean isHammingOne(String a, String b) { if (a.length() != b.length()) return false; int diff = 0; for (int i = 0; i < a.length(); i++) { if (a.charAt(i) != b.charAt(i)) { diff++; if (diff > 1) return false; } } return diff == 1; }
完整优化代码
java
import java.util.*; class Solution { public List<String> getWordsInLongestSubsequence(String[] words, int[] groups) { int n = words.length; int[] dp = new int[n]; int[] prev = new int[n]; Arrays.fill(dp, 1); Arrays.fill(prev, -1); // 预处理:按字符串长度分组 Map<Integer, List<Integer>> lenMap = new HashMap<>(); for (int i = 0; i < n; i++) { int len = words[i].length(); lenMap.computeIfAbsent(len, k -> new ArrayList<>()).add(i); } int maxLen = 1; int maxIndex = 0; for (int i = 0; i < n; i++) { int currentLen = words[i].length(); List<Integer> candidates = lenMap.get(currentLen); if (candidates == null) continue; List<Integer> possibleJs = new ArrayList<>(); for (int j : candidates) { if (j < i && groups[j] != groups[i]) { possibleJs.add(j); } } possibleJs.sort((a, b) -> Integer.compare(dp[b], dp[a])); for (int j : possibleJs) { if (isHammingOne(words[i], words[j])) { if (dp[j] + 1 > dp[i]) { dp[i] = dp[j] + 1; prev[i] = j; break; } } } if (dp[i] > maxLen) { maxLen = dp[i]; maxIndex = i; } } List<Integer> path = new ArrayList<>(); int current = maxIndex; while (current != -1) { path.add(current); current = prev[current]; } Collections.reverse(path); List<String> result = new ArrayList<>(); for (int idx : path) { result.add(words[idx]); } return result; } private boolean isHammingOne(String a, String b) { if (a.length() != b.length()) return false; int diff = 0; for (int i = 0; i < a.length(); i++) { if (a.charAt(i) != b.charAt(i)) { diff++; if (diff > 1) return false; } } return diff == 1; } }
复杂度分析
-
时间复杂度:
-
预处理分组:O(n)
-
主循环:O(n * m),其中m是相同长度的最大元素数量
-
汉明距离检查:O(L),L是字符串长度
-
-
空间复杂度:O(n)用于存储dp和prev数组
总结
通过动态规划结合优化策略,我们有效地解决了这个问题。关键点在于:
-
使用动态规划记录状态
-
按字符串长度分组减少不必要的比较
-
按dp值排序优先检查更优解
-
优化汉明距离计算
这种分阶段优化思路可以应用于许多类似的序列问题。