合并字符串形成目标串的最小插入成本问题(带位置权重)
题目描述
给定两个字符串 s 和 t,以及一个二维数组 cost[i][j],表示在字符串 s 的第 i 个字符(下标从1开始)之后插入一个字符(这个字符是 t 的第 j 个字符)所需的花费。目标是通过在 s 中插入若干个字符(包括在开头、结尾和任意位置插入),使得 s 最终变成 t。每次插入可以选择 t 中的任意字符,但需要支付对应的位置权重成本。求完成转换的最小总插入成本。
注意:
- 原始字符串
s中的字符不能删除或修改,只能通过在任意位置插入新字符来形成目标串t。 - 插入操作有“位置权重”:在
s的第i个字符之后插入(i=0表示在开头前插入),且插入的字符是t的第j个字符时,成本为cost[i][j]。 - 题目保证
s是t的一个子序列(即s可以通过删除t中的某些字符得到),因此一定有解。 - 长度:
s长度为n,t长度为m,1 ≤ n, m ≤ 200,cost数组大小为(n+1) × m,每个值为非负整数。
解题思路
这个问题本质上是“在保持 s 原有字符顺序不变的前提下,通过插入补齐缺失的字符,使得最终序列等于 t”。由于插入操作有位置权重,不能简单地贪心匹配。我们可以用区间DP来规划在 s 的某个区间与 t 的某个子串的匹配过程中,如何以最小成本插入字符。
定义状态
定义 dp[i][j] 表示:已经考虑将 s 的前 i 个字符(下标1到i)转换为 t 的前 j 个字符(下标1到j)所需的最小插入成本。
i范围是0到n(i=0表示s中还没有字符被考虑)。j范围是0到m(j=0表示t为空)。- 目标:求
dp[n][m]。
状态转移
我们考虑当前状态 dp[i][j],即已经匹配了 s[1..i] 和 t[1..j],现在要决定下一步:
-
如果
s[i] == t[j]:
那么当前字符可以直接匹配,不需要插入,所以:
dp[i][j] = dp[i-1][j-1](即两个字符自然对齐,无成本)。 -
如果
s[i] != t[j]:
由于不能删除或修改s的字符,我们必须通过在s的前 i 个字符之后插入字符来匹配t[j]。
但插入字符有一个关键:插入的字符必须是t中的某个字符,并且有位置权重。我们可以考虑“在s的第 k 个字符之后插入t[j]”,其中k是插入位置(0 ≤ k ≤ i)。
但这样直接枚举插入位置会使状态转移复杂。更巧妙的方式是:
我们换个角度,dp[i][j]表示在已经匹配了s[1..i]和t[1..j]时,最后一个操作可能是:- 匹配了
s[i]和t[j](当它们相等时,成本为0)。 - 或者,我们在某个位置插入了
t[j]这个字符,然后让s[1..i]去匹配t[1..j-1]。
但这样又有一个问题:插入
t[j]的位置权重取决于“在s的哪个字符之后插入”,而那个位置在之前的状态中可能已经改变了。为了处理位置权重,我们需要在状态中记录“当前在
s中最后一个被使用的字符的位置”,因为插入成本与这个位置有关。因此,我们扩展状态:
定义dp[i][j][k]表示:将s的前i个字符匹配到t的前j个字符,并且最后一次插入操作是在s的第k个字符之后执行的(k=0表示在开头之前)的最小成本。
但这样状态是O(n*m*n),在n,m ≤ 200时可能过大(800万状态,每个状态转移需 O(n) 可能超时)。我们可以优化:注意到插入位置只与
s的字符索引有关,我们可以将“插入位置”作为状态的一个维度,但我们可以用另一种等价方式:考虑用区间DP,定义
dp[l][r][p]表示:将s的子串s[l..r]转换为t的子串t[p..q]的最小成本,但这样还需要记录q,状态太多。回到线性DP的改进版本:
定义dp[i][j]为:将s[1..i]转换为t[1..j]的最小成本,且我们只记录成本,插入位置信息通过比较i和匹配的字符来隐式处理。正确的状态转移:
对于dp[i][j]:- 如果
s[i] == t[j]:我们可以直接匹配,dp[i][j] = dp[i-1][j-1]。 - 无论如何,我们也可以选择在
s的第i个字符之后插入t[j](这是允许的,因为插入可以在任何位置)。插入成本是cost[i][j]。插入后,t[j]被匹配,而s的前 i 个字符仍然需要匹配t的前 j-1 个字符,所以:
dp[i][j] = min(dp[i][j], dp[i][j-1] + cost[i][j])。 - 此外,我们还可以在更早的位置(比如在第 i-1 个字符之后)插入
t[j],但这种情况已经被dp[i][j-1]覆盖了吗?不一定,因为dp[i][j-1]表示s[1..i]匹配t[1..j-1],它的最后一次插入位置可能是 i 或更小。如果我们在位置 k 插入t[j],那相当于从dp[i][j-1]的状态再插入一个字符,但插入位置权重是cost[k][j],而这个 k 是之前的状态决定的,我们不知道。
所以我们需要在状态中记录“最后一个字符在原串 s 中的匹配位置”,才能确定下一次插入的位置权重。
重新设计状态:
定义dp[i][j][k]表示:将s[1..i]转换为t[1..j],并且在 s 中最后一个被匹配的字符是 s[k](即下一个插入位置将在 s[k] 之后)的最小成本。i和j含义同上。k范围 0 到 i,表示最后一个匹配的 s 的字符索引(k=0 表示还没匹配任何 s 的字符,即全在开头之前插入)。
初始状态:
dp[0][0][0] = 0,其他为无穷大。转移:
- 如果
s[i] == t[j]且 i>0, j>0:
我们可以让s[i]匹配t[j],那么最后一个匹配的 s 字符变为 i,所以:
dp[i][j][i] = min(dp[i][j][i], dp[i-1][j-1][k])对所有 k。 - 我们可以在位置 k(0 ≤ k ≤ i)之后插入字符
t[j],那么状态会从dp[i][j-1][k]转移来,因为插入前匹配到 t 的前 j-1 个字符,且最后一个匹配的 s 字符是 k,在 k 后插入 t[j] 的成本是cost[k][j],插入后最后一个匹配的 s 字符不变(因为插入的不是 s 的字符),所以:
dp[i][j][k] = min(dp[i][j][k], dp[i][j-1][k] + cost[k][j])。
注意:插入操作不改变 i,因为 s 的长度没变(插入的字符是额外的,不算在 s 的原始字符里)。
这个状态是
O(n*m*n),在 n=m=200 时是 800 万,每个状态转移是 O(1),可以接受。最终答案:
min(dp[n][m][k])对所有 k。 - 匹配了
逐步演算(小例子)
设 s = "ab", t = "acb", n=2, m=3。
cost 表(简化,假设 cost 都=1 除非特殊):
cost[0][1]=1, cost[0][2]=1, cost[0][3]=1
cost[1][1]=1, cost[1][2]=1, cost[1][3]=1
cost[2][1]=1, cost[2][2]=1, cost[2][3]=1
初始化:dp[0][0][0]=0。
-
从 dp[0][0][0],考虑插入 t[1]='a' 在位置 0 后:
dp[0][1][0] = dp[0][0][0] + cost[0][1] = 1。 -
从 dp[0][1][0],插入 t[2]='c' 在位置 0 后:
dp[0][2][0] = 1 + cost[0][2] = 2。 -
从 dp[0][2][0],插入 t[3]='b' 在位置 0 后:
dp[0][3][0] = 2 + cost[0][3] = 3。 -
现在考虑 s[1]='a' 匹配 t[1]='a':
从 dp[0][0][0] 转移:dp[1][1][1] = 0。 -
从 dp[1][1][1],插入 t[2]='c' 在位置 1 后:
dp[1][2][1] = 0 + cost[1][2] = 1。 -
从 dp[1][2][1],插入 t[3]='b' 在位置 1 后:
dp[1][3][1] = 1 + cost[1][3] = 2。 -
但 s[2]='b' 匹配 t[3]='b':
可以从 dp[1][2][1] 转移:dp[2][3][2] = 1。
比较:最终 dp[2][3][2]=1 更小,所以最小成本=1。
算法步骤
- 初始化三维数组 dp[n+1][m+1][n+1],值设为无穷大,dp[0][0][0]=0。
- 循环 i 从 0 到 n:
循环 j 从 0 到 m:
循环 k 从 0 到 i:
如果 dp[i][j][k] 无穷大,跳过。
如果 i < n 且 j < m 且 s[i+1]==t[j+1]:
dp[i+1][j+1][i+1] = min(dp[i+1][j+1][i+1], dp[i][j][k])。
如果 j < m:
dp[i][j+1][k] = min(dp[i][j+1][k], dp[i][j][k] + cost[k][j+1])。 - 最终答案 = min(dp[n][m][k]) 对所有 k。
时间复杂度 O(n²*m),空间可优化为滚动数组 O(n²)。