题目链接2901. 最长相邻不相等子序列 II - 力扣(LeetCode)
一、题目分析
给定一个整数 n,以及长度均为 n 的字符串数组 words 和数组 groups。我们的目标是从下标 [0, 1, ..., n - 1] 中找出一个最长子序列,这个子序列需满足两个条件:
- 相邻下标对应的 groups 值不同,即对于所有满足 0 < j + 1 < k 的 j,都有 groups[j] != groups[j + 1]。
- 对于所有 0 < j + 1 < k 的下标 j,words[j] 和 words[j + 1] 的长度相等,且两个字符串之间的汉明距离为 1。汉明距离是指对应位置字符不同的数目。最后需要返回该下标子序列依次对应 words 数组中的字符串连接形成的字符串数组。
二、解题思路
本题可以使用动态规划(Dynamic Programming,DP)的方法来解决。核心思路是通过记录以每个位置为结尾的最长子序列长度,逐步推导出全局最长子序列。
- 初始化:
- 定义 dp 数组,dp[i] 表示以 i 为结尾的最长子序列长度,初始化为 1。
- 定义 pre 数组,pre[i] 用于记录最长子序列中 i 的前一个位置,初始化为 -1。
- 定义 maxIndex 用于记录最长子序列的最后一个位置,初始化为 -1。
- 动态规划过程:
- 外层循环遍历 words 数组的每个位置 i。
- 内层循环从 i - 1 倒序遍历到 0,对于每个 j,判断是否满足两个条件:
- groups[i] != groups[j],即相邻位置的 groups 值不同。
- checkWord(words[i], words[j]),即 words[i] 和 words[j] 长度相等且汉明距离为 1。如果满足条件,且 dp[j] + 1 > dp[i],则更新 dp[i] 和 pre[i]。
- 每次遍历完 i 后,更新 maxIndex,使其指向当前找到的最长子序列的最后一个位置。
- 构造结果:
- 从 maxIndex 开始,通过 pre 数组回溯,将对应的 words 元素添加到结果数组 res 中。
- 最后将 res 数组反转,得到正确顺序的最长子序列对应的字符串数组。
三、代码实现与详解
class Solution {
public:
// 主函数,用于获取满足条件的最长子序列对应的字符串数组
vector<string> getWordsInLongestSubsequence(vector<string>& words,
vector<int>& groups) {
int n = words.size();
// dp[i] 表示以 i 为结尾的最长子序列长度,初始化为 1
vector<int> dp(n, 1);
// pre[i] 用于记录最长子序列中 i 的前一个位置,初始化为 -1
vector<int> pre(n, -1);
int maxIndex = -1;
// 外层循环遍历 words 数组的每个位置 i
for (int i = 0; i < n; i++) {
// 内层循环从 i - 1 倒序遍历到 0
for (int j = i - 1; j >= 0; j--) {
if (dp[j] >= dp[i]) {
// 判断相邻位置的 groups 值不同,且 words[i] 和 words[j] 满足条件
if (groups[i] != groups[j] &&
checkWord(words[i], words[j])) {
if (dp[j] + 1 > dp[i]) {
// 更新 dp[i] 和 pre[i]
dp[i] = dp[j] + 1;
pre[i] = j;
}
}
}
}
if (maxIndex < 0 || dp[i] > dp[maxIndex]) {
// 更新 maxIndex 为当前最长子序列的最后一个位置
maxIndex = i;
}
}
vector<string> res;
// 从 maxIndex 开始,通过 pre 数组回溯,将对应的 words 元素添加到结果数组 res 中
for (int i = maxIndex; i >= 0; i = pre[i]) {
res.emplace_back(words[i]);
}
// 反转 res 数组,得到正确顺序的最长子序列对应的字符串数组
reverse(res.begin(), res.end());
return res;
}
// 检查两个字符串是否长度相等且汉明距离为 1
bool checkWord(string s1, string s2) {
if (s1.size() != s2.size()) {
return false;
}
int count = 0;
int n = s1.size();
// 遍历字符串,统计不同字符的个数
for (int i = 0; i < n; i++) {
if (s1[i] != s2[i]) {
count++;
if (count > 1) {
return false;
}
}
}
return count == 1;
}
};
代码说明
- getWordsInLongestSubsequence 函数:
- 首先初始化 dp、pre 数组和 maxIndex。
- 通过两层循环进行动态规划,更新 dp 和 pre 数组,并找到最长子序列的最后一个位置 maxIndex。
- 最后通过回溯和反转操作,得到满足条件的最长子序列对应的字符串数组。
- checkWord 函数:
- 用于判断两个字符串是否长度相等且汉明距离为 1。通过遍历字符串,统计不同字符的个数来实现判断。
四、时间与空间复杂度分析
- 时间复杂度:动态规划部分的时间复杂度主要由两层循环决定,外层循环 n 次,内层循环最多 n 次,所以动态规划部分时间复杂度为 O(n^2)。checkWord 函数中遍历字符串的时间复杂度为 O(m),其中 m 为字符串的长度。由于在动态规划中每次调用 checkWord,所以总的时间复杂度为 O(n^2 * m),其中 n 为 words 数组的长度,m 为字符串的平均长度。
- 空间复杂度:使用了 dp 和 pre 两个长度为 n 的数组,以及结果数组 res 最多存储 n 个元素,所以空间复杂度为 O(n)。