还原原始字符串 | 豆包MarsCode AI刷题

96 阅读4分钟

题目解析

题目详细描述

本题要求我们在给定一个最终字符串 F 的情况下,逆向推导出最初的起始字符串 S。具体操作如下:

  • 初始时,我们有一个字符串 S,其长度 K 满足 K < S.length()
  • 执行操作:将 S 更新为 S + S[k:S.length()],即将 S 的子串从第 k 个字符到结尾的部分拼接到 S 的末尾。
  • 这个操作可以执行多次,每次选择不同的 k 值(满足 k < S.length())。
  • 经过若干次操作后,最终得到字符串 F

我们的任务是,在已知最终字符串 F 的情况下,找出可能的起始字符串 S 中长度最短的一个。

解题思路

为了求解最短的起始字符串 S,我们可以采用**动态规划(Dynamic Programming)**的思想。具体步骤如下:

  1. 状态定义

    • 定义 dp[i] 表示对于字符串 F 的前 i 个字符 F[:i],最短的起始字符串的长度。
  2. 初始状态

    • 对于任意 idp[i] 最初可以设定为 i,即假设最初的字符串 S 就是 F 的前 i 个字符,这也是最简单的情况。
  3. 状态转移

    • 对于每一个位置 end(从 0 到 F.length()),我们尝试找到一个合适的位置 k,满足 k < end,并且 F[:k] 的末尾部分与 F[k:end] 匹配。也就是说,F[:k] 的后缀与 F[k:end] 相同。
    • 如果找到这样的 k,那么 dp[end] 可以更新为 dp[k],即 F[:k] 的最短起始字符串长度。
    • 为了减少不必要的计算,我们可以从 end/2 开始尝试 k,因为若 k > end/2,则 F[k:end] 的长度小于等于 end/2,可以减少匹配次数。
  4. 最终结果

    • 最终,dp[F.length()] 就表示整个字符串 F 的最短起始字符串的长度。通过截取 F 的前 dp[F.length()] 个字符,即可得到所需的最短起始字符串。

代码实现

以下是基于上述思路的Java代码实现:

public class Main {

    public static String solution(String F) {
        // dp[i] 表示对于字符串F[:i],最短的起始字符串长度
        int[] dp = new int[F.length() + 1];
        // 初始情况:假设最初的字符串就是当前的子串
        for (int end = 0; end < dp.length; end++) {
            dp[end] = end;
        }
        // 逐步填充dp数组
        for (int end = 0; end < dp.length; end++) {
            // 从end/2开始尝试k,减少不必要的匹配
            int k = end / 2;
            while (k <= end) {
                // 分割成两个部分:F[:k] 和 F[k:end]
                var firstPart = F.substring(0, k);
                var secondPart = F.substring(k, end);
                // 检查firstPart的末尾是否与secondPart匹配
                if (firstPart.endsWith(secondPart)) {
                    // 如果匹配,则更新dp[end]
                    dp[end] = Math.min(dp[end], dp[k]);
                }
                k++;
            }
        }

        // 得到最短起始字符串的长度
        int len = dp[dp.length - 1];
        // 返回最短起始字符串
        return F.substring(0, len);
    }

    public static void main(String[] args) {
        // 测试用例
        System.out.println(solution("abbabbbabb").equals("ab")); // true
        System.out.println(solution("abbbabbbb").equals("ab")); // true
        System.out.println(solution("jiabanbananananiabanbananananbananananiabanbananananbananananbananananbanananan").equals("jiaban")); // true
        System.out.println(solution("selectecttectelectecttectcttectselectecttectelectecttectcttectectelectecttectcttectectcttectectcttectectcttect").equals("select")); // true
        System.out.println(solution("discussssscussssiscussssscussssdiscussssscussssiscussssscussssiscussssscussss").equals("discus")); // true
    }
}

时间复杂度和空间复杂度分析

时间复杂度

  • 外层循环遍历 end 从 0 到 N,其中 N 是字符串 F 的长度。
  • 内层循环中,k 从 end/2 开始遍历到 end,最坏情况下,内层循环的次数为 end/2
  • 在每次内层循环中,substring 和 endsWith 的操作都是线性的时间复杂度,即 O(end)
  • 因此,整体时间复杂度为 O(N^3),但由于我们从 end/2 开始遍历,可以在一定程度上减少实际运行时间。然而,最坏情况下仍然是三重嵌套的线性操作。

空间复杂度

  • 主要的空间消耗来自于 dp 数组,其大小为 O(N)
  • 另外,还需要存储一些临时变量,如 firstPart 和 secondPart,但它们的空间复杂度也是 O(N)
  • 因此,整体空间复杂度为 O(N)

结论

本题通过动态规划的方法,有效地解决了从最终字符串逆推最短起始字符串的问题。尽管初始算法的时间复杂度较高,但通过合理的优化,可以在实际应用中取得更好的性能表现。理解并掌握这种动态规划的思路,对于解决类似的字符串匹配与优化问题具有重要的参考价值。