循序渐进解决最长子序列问题:动态规划与优化策略

问题描述

给定一个整数 n 和两个长度均为 n 的数组 words 和 groups

  • words 是字符串数组

  • groups 是整数数组

我们需要找到一个最长的子序列,满足以下条件:

  1. 相邻元素的 groups 值不同

  2. 相邻元素的字符串长度相同且汉明距离为1(即只有一个字符不同)

初步思考

什么是子序列?

子序列是指从原数组中删除一些(或不删除)元素后,剩余元素保持原有顺序的新数组。

关键条件分析

  1. groups值不同:相邻元素的groups值必须不同

  2. 字符串条件

    • 长度相同

    • 汉明距离为1(仅一个字符不同)

暴力解法思路

最直观的方法是枚举所有可能的子序列,然后检查每个子序列是否满足条件。但这种方法的时间复杂度为O(2^n),显然不适用于较大的n。

动态规划解法

动态规划状态定义

定义 dp[i] 表示以第i个元素结尾的最长子序列长度,prev[i] 记录前驱节点。

状态转移方程

对于每个i,检查所有j < i:

  1. groups[j] != groups[i]

  2. words[j].length() == words[i].length()

  3. 汉明距离为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数组

总结

通过动态规划结合优化策略,我们有效地解决了这个问题。关键点在于:

  1. 使用动态规划记录状态

  2. 按字符串长度分组减少不必要的比较

  3. 按dp值排序优先检查更优解

  4. 优化汉明距离计算

这种分阶段优化思路可以应用于许多类似的序列问题。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值