合并相邻字符串形成目标串的最小拼接成本问题
字数 4337 2025-12-10 20:48:44

合并相邻字符串形成目标串的最小拼接成本问题


1. 题目描述

给定一个字符串 s 和一个目标字符串 target
你每次可以将 s 中的一个连续子串(长度任意)提取出来,并将其拼接到当前已形成的字符串的末尾,形成一个新的字符串。
初始时,你手中已形成的字符串为空。
每次操作的成本等于被提取的子串的长度。
求:将 s 通过若干次这样的操作,最终拼接成 target 的最小总成本

注意

  • 必须按 target 中的字符顺序依次拼接,即最终形成的字符串必须与 target 完全相同。
  • 每次只能从 s 中提取一个连续子串,且每次提取后,s 不会改变(可以重复使用 s 中的任意位置的字符,但必须按原串中的连续顺序提取)。
  • 提取的子串在 s 中必须是连续的一段。

2. 示例

假设:

s = "abcde"
target = "abce"

一种可行的操作方案:

  1. 提取 s 的子串 "ab"(长度 2),成本 2,当前字符串变为 "ab"
  2. 提取 s 的子串 "c"(长度 1),成本 1,当前字符串变为 "abc"
  3. 提取 s 的子串 "e"(长度 1),成本 1,当前字符串变为 "abce"
    总成本 = 2 + 1 + 1 = 4。

但更优的方案是:

  1. 提取 s 的子串 "abc"(长度 3),成本 3,当前字符串变为 "abc"
  2. 提取 s 的子串 "e"(长度 1),成本 1,当前字符串变为 "abce"
    总成本 = 3 + 1 = 4。

最优解是 4 吗?
其实可以:

  1. 提取 "abce"s 的子串吗?不是,因为 s"abcde""abce" 不连续(缺少 "d")。
    实际上,"abce" 无法一次提取。

但我们可以:

  1. 提取 "abc"(成本 3)
  2. 提取 "e"(成本 1)
    总成本 4。

另一种可能:

  1. 提取 "ab"(成本 2)
  2. 提取 "c"(成本 1)
  3. 提取 "e"(成本 1)
    成本 4。

似乎最优是 4。

但如果我们考虑 s = "abcabc", target = "abcbc"

  1. 提取 "abc"(成本 3)
  2. 提取 "bc"(成本 2)
    总成本 5。
    但我们可以:
  3. 提取 "ab"(成本 2)
  4. 提取 "cbc"s 中吗?"cbc" 不连续,因为 s 中是 abcabc"cbc" 不是连续子串。不行。
    其实更优可能是:
  5. 提取 "abc"(成本 3)
  6. 提取 "bc"(成本 2)
    总成本 5。
    但也可以:
  7. 提取 "a"(成本 1)
  8. 提取 "bc"(成本 2)
  9. 提取 "bc"(成本 2)
    总成本 5。
    看来需要动态规划找最优。

3. 问题分析

我们实际上是要将 target 分成若干连续段,每一段必须是 s 的某个连续子串,并且这些段按顺序拼成 target
我们要最小化:段的数量(因为成本是每段的长度之和,即 target 的长度是固定的,段数越多,成本越大?)不对,注意成本是每段的长度,不是段数

target 长度为 m
如果分成 k 段,各段长度分别为 len1, len2, ..., lenk,总成本 = len1 + len2 + ... + lenk = m
咦?那总成本总是等于 target 的长度?那岂不是与如何分段无关?
不对!因为每次提取的子串来自 s,但提取的长度可以比该段在 target 中的长度更长吗?不可以,因为提取的子串必须完全用于形成 target 的对应段,不能多出字符。

仔细看操作:每次提取 s 的一个连续子串,拼接到已形成的串末尾。最终形成的串必须严格等于 target
提取的子串长度 = 它在 target 中对应的那一段的长度。
所以每次操作的成本 = 该段长度。
那么总成本 = 所有段长度之和 = target.length,固定不变。

那这题还有意义吗?
等一下,我发现题目理解有陷阱。


4. 重新理解题意与核心

实际上,常见的一个正确题意是:
每次提取 s 的一个连续子串(长度任意),但这个子串在 target 中必须对应尚未拼接的部分的前缀,并且可以一次提取比需要更长的子串,但必须连续匹配。不对,这样说不通。

我查阅类似题目,这其实是 “字符串拆分” 问题的变种:
给定 starget,每次从 s 中取一个子串(长度任意)拼接到当前串末尾,但最终要形成 target
如果每次成本是提取的子串长度,并且最终必须完全匹配 target,则总成本 = target 长度,是定值,题目无意义。

因此,正确的理解是
每次提取的子串长度是任意的,但必须完全用于形成 target 的下一段。
如果我们可以在一次操作中提取较长的一段,可能减少操作次数,但成本会增加(因为成本是长度),所以我们要在“一次提取较长段”和“分多次提取较短段”之间权衡吗?但总成本是长度和,是固定的,所以没有权衡。

矛盾表明,可能成本不是长度,而是每次操作固定成本 + 长度相关成本,或者是每次操作成本为 1,目标是最小化操作次数


5. 修正题意(采用常见的合并相邻字符串形成目标的最小操作次数)

我重新表述正确的区间 DP 题目:

给定字符串 s 和 target,初始手中为空串。每次可从 s 中选一个连续子串,拼接到当前串末尾。求形成 target 的最少操作次数。
这样,目标就是最小化分段数。

为什么是区间 DP?
因为 target 的每段必须是 s 的子串,我们需要将 target 分成尽量少的段,每段是 s 的子串。


6. 动态规划状态定义

dp[i] 表示形成 target[0:i](长度为 i 的前缀)所需的最少操作次数。
初始:dp[0] = 0(空串不需要操作)。

转移:
dp[i] = min{ dp[j] + 1 },其中 j < i,并且子串 target[j:i]s 的一个连续子串。

问题转化为:对每个区间 [j, i),判断 target[j:i] 是否是 s 的子串。


7. 判断子串的优化

如果直接对每个区间用哈希表或 KMP 检查,复杂度高。
我们可以预处理一个二维数组 isSub[j][i] 表示 target[j:i] 是否在 s 中出现(作为连续子串)。
检查方法:对每个 j,在 s 中寻找匹配 target[j:] 的前缀,更新 isSub[j][i]
更高效:用字符串哈希(Rolling Hash)在 O(1) 判断 s 的所有子串是否等于 target[j:i]。
但为了讲解清楚,我们先假设有一个函数 check(j, i) 能 O(1) 判断。


8. 状态转移方程

dp[0] = 0
for i in 1 to m:
    dp[i] = INF
    for j in 0 to i-1:
        if check(j, i) is true:
            dp[i] = min(dp[i], dp[j] + 1)

最终答案为 dp[m]

时间复杂度 O(m²),加上 check 的复杂度。
check 可预处理:
对每个起始位置 j,计算在 s 中匹配 target[j:] 的最长匹配长度 maxLen[j]
那么对于任意 i ≤ j + maxLen[j],target[j:i] 都是 s 的子串。
这样 check(j, i) 就是 O(1)。

如何求 maxLen[j]
可以在 s 中找 target[j] 的出现,然后逐字符比较。但更简单的是用字符串哈希:
将 s 的所有子串的哈希存入哈希集合,然后对每个 j,二分查找最远的 i 使得哈希在集合中。
这样预处理 O(n²) 或 O(n log n)。


9. 举例说明

例:

s = "abcdef"
target = "abcf"

预处理:

  • target[0:3] = "abc" 是 s 的子串
  • target[0:4] = "abcf" 不是
  • target[3:4] = "f"
    计算:
    dp[0]=0
    dp[1]:"a" 是子串,dp[1] = dp[0]+1=1
    dp[2]:"ab" 是子串,dp[2] = min(dp[0]+1=1, dp[1]+1=2) = 1
    dp[3]:"abc" 是子串,dp[3] = min(dp[0]+1=1, dp[1]+1=2, dp[2]+1=2) = 1
    dp[4]:
  • 从 j=0: "abcf" 不是子串,不行
  • j=1: "bcf" 不是
  • j=2: "cf" 不是
  • j=3: "f" 是,dp[4] = dp[3]+1=2
    所以最少 2 次操作。

10. 区间 DP 视角

虽然这是线性 DP,但核心判断 target[j:i] 是否是 s 的子串,这涉及子串匹配,通常不叫区间 DP,但我们可以用区间 DP 的思想:
定义 can[j][i] 表示 target[j:i] 是否是 s 的子串,这是区间性质。
然后 dp 用这个区间信息转移。

有些变种题目中,s 可以多次提取且可重叠,但本题只是普通子串判断。


11. 算法步骤总结

  1. 预处理:对每个 j,计算 maxLen[j] = target 从 j 开始的最长前缀,满足它是 s 的子串。
    (可以用字符串哈希,或对每个 j 在 s 中找匹配)
  2. 初始化 dp 数组长度为 m+1,dp[0]=0,其余为无穷大。
  3. 对 i 从 1 到 m:
    • 对 j 从 0 到 i-1:
      • 如果 i-j ≤ maxLen[j],则 dp[i] = min(dp[i], dp[j]+1)
  4. 答案 = dp[m] 如果 dp[m] 不是无穷大,否则 -1 表示无法形成。

12. 复杂度

  • 预处理 maxLen:O(m × n) 如果用朴素匹配(或 O(m + n) 用 KMP 对每个 j 跑一次,但总体 O(mn) 简单些)。
  • DP 转移:O(m²)。
    总复杂度 O(mn + m²),在 m, n ≤ 1000 时可行。

13. 边界情况

  • 如果 target 中有字符不在 s 中,则无解。
  • 如果 target 是空串,操作次数 0。
  • 如果 s 中包含整个 target 作为子串,则一次操作即可。

这样我们就得到了“合并相邻字符串形成目标串的最小操作次数”问题的完整解法。

合并相邻字符串形成目标串的最小拼接成本问题 1. 题目描述 给定一个字符串 s 和一个目标字符串 target 。 你每次可以将 s 中的一个 连续子串 (长度任意)提取出来,并将其 拼接到当前已形成的字符串的末尾 ,形成一个新的字符串。 初始时,你手中已形成的字符串为空。 每次操作的 成本 等于被提取的子串的长度。 求:将 s 通过若干次这样的操作, 最终拼接成 target 的最小总成本 。 注意 : 必须按 target 中的字符顺序依次拼接,即最终形成的字符串必须与 target 完全相同。 每次只能从 s 中提取一个连续子串,且每次提取后, s 不会改变(可以重复使用 s 中的任意位置的字符,但必须按原串中的连续顺序提取)。 提取的子串在 s 中必须是连续的一段。 2. 示例 假设: 一种可行的操作方案: 提取 s 的子串 "ab" (长度 2),成本 2,当前字符串变为 "ab" 。 提取 s 的子串 "c" (长度 1),成本 1,当前字符串变为 "abc" 。 提取 s 的子串 "e" (长度 1),成本 1,当前字符串变为 "abce" 。 总成本 = 2 + 1 + 1 = 4。 但更优的方案是: 提取 s 的子串 "abc" (长度 3),成本 3,当前字符串变为 "abc" 。 提取 s 的子串 "e" (长度 1),成本 1,当前字符串变为 "abce" 。 总成本 = 3 + 1 = 4。 最优解是 4 吗? 其实可以: 提取 "abce" 是 s 的子串吗?不是,因为 s 是 "abcde" , "abce" 不连续(缺少 "d" )。 实际上, "abce" 无法一次提取。 但我们可以: 提取 "abc" (成本 3) 提取 "e" (成本 1) 总成本 4。 另一种可能: 提取 "ab" (成本 2) 提取 "c" (成本 1) 提取 "e" (成本 1) 成本 4。 似乎最优是 4。 但如果我们考虑 s = "abcabc" , target = "abcbc" : 提取 "abc" (成本 3) 提取 "bc" (成本 2) 总成本 5。 但我们可以: 提取 "ab" (成本 2) 提取 "cbc" 在 s 中吗? "cbc" 不连续,因为 s 中是 abcabc , "cbc" 不是连续子串。不行。 其实更优可能是: 提取 "abc" (成本 3) 提取 "bc" (成本 2) 总成本 5。 但也可以: 提取 "a" (成本 1) 提取 "bc" (成本 2) 提取 "bc" (成本 2) 总成本 5。 看来需要动态规划找最优。 3. 问题分析 我们实际上是要将 target 分成若干连续段,每一段必须是 s 的某个连续子串,并且这些段按顺序拼成 target 。 我们要最小化:段的数量(因为成本是每段的长度之和,即 target 的长度是固定的,段数越多,成本越大?)不对,注意 成本是每段的长度,不是段数 。 设 target 长度为 m 。 如果分成 k 段,各段长度分别为 len1, len2, ..., lenk ,总成本 = len1 + len2 + ... + lenk = m 。 咦?那总成本总是等于 target 的长度?那岂不是与如何分段无关? 不对 !因为每次提取的子串来自 s ,但提取的长度可以比该段在 target 中的长度更长吗?不可以,因为提取的子串必须完全用于形成 target 的对应段,不能多出字符。 仔细看操作:每次提取 s 的一个连续子串,拼接到已形成的串末尾。最终形成的串必须严格等于 target 。 提取的子串长度 = 它在 target 中对应的那一段的长度。 所以每次操作的成本 = 该段长度。 那么总成本 = 所有段长度之和 = target.length ,固定不变。 那这题还有意义吗? 等一下 ,我发现题目理解有陷阱。 4. 重新理解题意与核心 实际上,常见的一个正确题意是: 每次提取 s 的一个 连续子串 (长度任意),但这个子串在 target 中必须对应 尚未拼接的部分的前缀 ,并且 可以一次提取比需要更长的子串,但必须连续匹配 。不对,这样说不通。 我查阅类似题目,这其实是 “字符串拆分” 问题的变种: 给定 s 和 target ,每次从 s 中取一个子串(长度任意)拼接到当前串末尾,但 最终要形成 target 。 如果每次成本是提取的子串长度,并且最终必须完全匹配 target ,则总成本 = target 长度,是定值,题目无意义。 因此,正确的理解是 : 每次提取的子串长度是任意的,但必须完全用于形成 target 的下一段。 如果我们可以在一次操作中提取较长的一段,可能减少操作次数,但成本会增加(因为成本是长度),所以我们要在“一次提取较长段”和“分多次提取较短段”之间权衡吗?但总成本是长度和,是固定的,所以没有权衡。 矛盾表明,可能成本不是长度,而是 每次操作固定成本 + 长度相关成本 ,或者是 每次操作成本为 1 ,目标是 最小化操作次数 。 5. 修正题意(采用常见的合并相邻字符串形成目标的最小操作次数) 我重新表述正确的区间 DP 题目: 给定字符串 s 和 target,初始手中为空串。每次可从 s 中选一个连续子串,拼接到当前串末尾。求形成 target 的最少操作次数。 这样,目标就是最小化分段数。 为什么是区间 DP? 因为 target 的每段必须是 s 的子串,我们需要将 target 分成尽量少的段,每段是 s 的子串。 6. 动态规划状态定义 设 dp[i] 表示形成 target[0:i] (长度为 i 的前缀)所需的最少操作次数。 初始: dp[0] = 0 (空串不需要操作)。 转移: dp[i] = min{ dp[j] + 1 } ,其中 j < i ,并且子串 target[j:i] 是 s 的一个连续子串。 问题转化为:对每个区间 [j, i) ,判断 target[j:i] 是否是 s 的子串。 7. 判断子串的优化 如果直接对每个区间用哈希表或 KMP 检查,复杂度高。 我们可以预处理一个二维数组 isSub[j][i] 表示 target[j:i] 是否在 s 中出现(作为连续子串)。 检查方法:对每个 j,在 s 中寻找匹配 target[j:] 的前缀,更新 isSub[j][i] 。 更高效:用字符串哈希(Rolling Hash)在 O(1) 判断 s 的所有子串是否等于 target[ j:i ]。 但为了讲解清楚,我们先假设有一个函数 check(j, i) 能 O(1) 判断。 8. 状态转移方程 最终答案为 dp[m] 。 时间复杂度 O(m²),加上 check 的复杂度。 check 可预处理: 对每个起始位置 j,计算在 s 中匹配 target[ j:] 的最长匹配长度 maxLen[j] 。 那么对于任意 i ≤ j + maxLen[ j], target[j:i] 都是 s 的子串。 这样 check(j, i) 就是 O(1)。 如何求 maxLen[j] ? 可以在 s 中找 target[ j ] 的出现,然后逐字符比较。但更简单的是用字符串哈希: 将 s 的所有子串的哈希存入哈希集合,然后对每个 j,二分查找最远的 i 使得哈希在集合中。 这样预处理 O(n²) 或 O(n log n)。 9. 举例说明 例: 预处理: target[0:3] = "abc" 是 s 的子串 target[0:4] = "abcf" 不是 target[3:4] = "f" 是 计算: dp[ 0 ]=0 dp[ 1]: "a" 是子串,dp[ 1] = dp[ 0 ]+1=1 dp[ 2]: "ab" 是子串,dp[ 2] = min(dp[ 0]+1=1, dp[ 1 ]+1=2) = 1 dp[ 3]: "abc" 是子串,dp[ 3] = min(dp[ 0]+1=1, dp[ 1]+1=2, dp[ 2 ]+1=2) = 1 dp[ 4 ]: 从 j=0: "abcf" 不是子串,不行 j=1: "bcf" 不是 j=2: "cf" 不是 j=3: "f" 是,dp[ 4] = dp[ 3 ]+1=2 所以最少 2 次操作。 10. 区间 DP 视角 虽然这是线性 DP,但核心判断 target[j:i] 是否是 s 的子串,这涉及子串匹配,通常不叫区间 DP,但我们可以用区间 DP 的思想: 定义 can[j][i] 表示 target[ j:i ] 是否是 s 的子串,这是区间性质。 然后 dp 用这个区间信息转移。 有些变种题目中,s 可以多次提取且可重叠,但本题只是普通子串判断。 11. 算法步骤总结 预处理:对每个 j,计算 maxLen[j] = target 从 j 开始的最长前缀,满足它是 s 的子串。 (可以用字符串哈希,或对每个 j 在 s 中找匹配) 初始化 dp 数组长度为 m+1,dp[ 0 ]=0,其余为无穷大。 对 i 从 1 到 m: 对 j 从 0 到 i-1: 如果 i-j ≤ maxLen[ j],则 dp[i] = min(dp[i], dp[j]+1) 答案 = dp[ m] 如果 dp[ m ] 不是无穷大,否则 -1 表示无法形成。 12. 复杂度 预处理 maxLen:O(m × n) 如果用朴素匹配(或 O(m + n) 用 KMP 对每个 j 跑一次,但总体 O(mn) 简单些)。 DP 转移:O(m²)。 总复杂度 O(mn + m²),在 m, n ≤ 1000 时可行。 13. 边界情况 如果 target 中有字符不在 s 中,则无解。 如果 target 是空串,操作次数 0。 如果 s 中包含整个 target 作为子串,则一次操作即可。 这样我们就得到了“合并相邻字符串形成目标串的最小操作次数”问题的完整解法。