2026-10-18:转换字符串的最小成本Ⅲ。用go语言,给定两个字符串 source 和 target,以及若干条替换规则。每条规则包含一个模式串、一个替换串和一个基础代价;模式串与替换串长度相同,代价数组中对应给出该规则的基础代价。
可以按任意顺序、任意次数使用这些规则。每次使用时,在当前字符串中选一段连续区间,区间长度必须等于所选规则的模式串长度,并且这段区间里的每个位置此前都没有被任何已执行的操作覆盖过。随后逐位检查:当前字符必须与模式串对应字符相同,或者模式串该位是星号通配符。满足后,把整段字符替换成该规则的替换串;替换串按原样使用,不含通配符。一次操作的总代价等于该规则的基础代价加上模式串中星号的数量。
某个位置只要在某次操作中被覆盖,之后就不能再参与任何操作。由于模式和替换串等长,每次操作后字符串长度不变,字符位置也保持不变。
目标是求把 source 变成 target 所需的最小总代价。如果无论怎样安排都无法完成转换,则返回负一。
1 <= source.length, target.length <= 5000。
source 和 target 仅由小写英文字母组成。
1 <= rules.length == costs.length <= 200。
rulesi = patterni, replacementi。
1 <= patterni.length == replacementi.length <= 20。
patterni 至少包含一个小写英文字母,且最多包含 5 个 '*' 字符。
replacementi 仅包含小写英文字母。
1 <= costsi <= 1000。
输入: source = "hello", target = "world", rules = \["he","wo","llo","rld"], costs = 3,4。
输出: 7。
解释:
应用 rules0,将 "he" 替换为 "wo",成本为 3,字符串变为 "wollo"。
应用 rules1,将 "llo" 替换为 "rld",成本为 4,字符串变为 "world"。
总成本为 3 + 4 = 7。
题目来自力扣3995。
大体步骤如下:
一、预处理每条规则的成本
代码一开始遍历所有规则。对于第 k 条规则,它的基础成本是 costsk。由于每次应用规则时,实际成本还要加上模式串 pattern 中星号 '*' 的个数,所以代码直接把这个星号数量累加到 costsk 上。
这样处理后,后面动态规划里一旦决定使用第 k 条规则,只需要加上 costsk 即可,不需要每次重新统计星号数量。
这一步不改变规则内容,只改变成本数组的数值。因为 costs 是切片,代码是在原切片上累加。
二、定义模式匹配判断
代码定义了一个内部匹配函数 isMatch,用来判断某个模式串 pattern 是否能匹配从某个起始位置开始的字符串片段。
判断规则是:逐位比较 pattern 和当前字符串片段。如果 pattern 当前位不是星号,那么它必须与字符串片段的对应字符完全相同;如果 pattern 当前位是星号,则它可以匹配任意字符,直接通过。
需要注意,代码在转移时传给匹配函数的是原始 source 从位置 i 开始的片段。原因是动态规划按从左到右处理,位置 i 及其右边尚未被任何规则覆盖,所以这些位置上的字符仍然是 source 的原始字符。只有已经被处理过的前缀才不再参与后续操作。
三、定义动态规划状态
设 n 为 source 的长度。代码定义数组 f,长度为 n+1。
fi 表示:把 source 的前 i 个字符处理成 target 的前 i 个字符,并且前 i 个位置以后不再参与任何操作时,所需要的最小总成本。
换句话说,fi 表示当前已经处理到下标 i,接下来从下标 i 开始考虑。fn 就是整个字符串处理完成后的最小成本。
初始化时,f0 等于 0,表示还没有处理任何字符时成本为 0。f1 到 fn 初始化为一个非常大的值,表示暂时不可达。
代码隐含以 source 的长度作为 DP 长度,因此默认 source 和 target 等长。按题意,如果长度不同,显然无法通过这种等长替换完成转换,应当返回 -1;这段代码没有显式写长度判断。
四、外层按位置从左到右推进
外层循环让 i 从 0 到 n-1 依次遍历。
对于每个位置 i,先检查 fi 是否仍然是最大值。如果是,说明前 i 个字符无法以任何方式处理完成,当前位置不可能作为后续操作的起点,于是直接跳过。
如果 fi 可达,就从位置 i 出发考虑两种选择:保留位置 i 的字符不覆盖,或者从位置 i 开始应用某条规则覆盖一段连续区间。
五、选择不覆盖当前位置 i
如果 sourcei 和 targeti 相同,那么位置 i 可以保持不变。
此时前 i 个字符已经处理完成,并且位置 i 也不需要被覆盖,所以可以推进到 i+1。新的成本仍然是 fi。
代码执行的是松弛操作:如果 fi 比当前记录的 fi+1 更小,就把 fi+1 更新为 fi。
如果 sourcei 和 targeti 不同,那么不覆盖位置 i 就无法让它变成目标字符,因此不能通过"保留"这种方式推进。
六、选择用某条规则覆盖从 i 开始的一段
接下来代码遍历每一条规则 rulek。设这条规则的替换串为 replacement,模式串为 pattern。
因为题目保证 pattern 和 replacement 长度相同,所以 replacement 的长度就是这次覆盖区间的长度。设这个长度为 len,则这次覆盖的区间是 [i, i+len)。右端点为 r = i + len。
只有满足下面几个条件,才可能用这条规则从 i 开始覆盖:
第一,r 不能超过 n,也就是覆盖区间必须完全落在当前字符串范围内。
第二,使用这条规则后的新成本 fi + costsk 必须比当前记录的 fr 更小。如果不更小,就没有更新必要,代码会跳过后续判断。这个条件也起到提前剪枝的作用。
第三,替换串 replacement 必须完全等于 target 从 i 到 r 的子串。原因是:这一段一旦被覆盖,其中的位置以后就不能再被任何规则使用,因此这段字符的最终结果只能由这次替换决定。如果 replacement 不等于目标子串,那么即使这次替换成功,这段最终也无法变成 target,方案不可行。
第四,模式串 pattern 必须能够匹配 source 从 i 开始的长度为 len 的片段。这里使用的是原始 source 的对应片段,因为位置 i 到 r-1 之前还没有被覆盖,仍然保持 source 原始字符。匹配时,pattern 中非星号位必须与 source 对应字符相同,星号位可以匹配任意字符。
以上条件全部满足时,说明可以从 i 开始用这条规则覆盖到 r,并且这段覆盖后直接变成 target 的对应部分。于是把 fr 更新为 fi + costsk,表示处理完前 r 个字符的最小成本。
七、为什么这样按左端点推进是正确的
一次规则应用会覆盖一段连续区间,且被覆盖过的位置以后不能再使用。因此,任意一个合法方案中,所有被覆盖的区间两两不重叠。
把这些覆盖区间按左端点从左到右排序,就可以看成:从字符串最左边开始,每个位置要么保持原字符不覆盖,要么作为某次规则覆盖的起点,覆盖一段长度等于替换串长度的区间,然后跳到这段区间的右端点继续处理。
未覆盖的位置必须满足 source 与 target 在该位置相同,否则最终无法变成 target。被覆盖的区间由于以后不能再改,替换串必须直接等于 target 对应区间。模式匹配检查的是原始 source,因为该区间在覆盖前还没有被使用过。
因此,动态规划按 i 从左到右推进,枚举"保留一个字符"或"用某条规则覆盖一段",不会漏掉合法方案,也能保证区间不重叠。
八、用示例走一遍
source = "hello",target = "world",规则为 "he","wo" 成本 3,"llo","rld" 成本 4。两个模式串都没有星号,所以预处理后成本仍是 3 和 4。
初始 f0 = 0,其余为无穷大。
处理 i = 0 时,source0 是 'h',target0 是 'w',不相等,所以不能通过保留推进。
尝试规则 0:replacement 是 "wo",长度 2,r = 2。target0:2 是 "wo",与 replacement 相等;pattern "he" 与 source0:2 "he" 匹配。于是 f2 更新为 3。
尝试规则 1:replacement 是 "llo",长度 3,r = 3。target0:3 是 "wor",不等于 "llo",不能使用。
处理 i = 1 时,f1 不可达,跳过。
处理 i = 2 时,f2 = 3。source2 是 'l',target2 是 'r',不相等,不能保留。
尝试规则 0:replacement "wo" 与 target2:4 "rl" 不等,不能使用。
尝试规则 1:replacement "rld",长度 3,r = 5。target2:5 是 "rld",与 replacement 相等;pattern "llo" 与 source2:5 "llo" 匹配。于是 f5 更新为 f2 + 4 = 7。
最后 f5 为 7,返回 7。
九、返回结果
循环结束后,如果 fn 仍然是最大值,说明无法把整个 source 转换成 target,返回 -1。
如果 fn 小于最大值,说明存在可行方案,返回 fn,即最小总成本。
十、复杂度分析
设 n 为 source 的长度,m 为规则条数,L 为规则中模式串或替换串的最大长度。题目中 L 不超过 20。
时间复杂度:
预处理阶段需要遍历每条规则并统计星号数量,耗时 O(mL)。动态规划外层遍历 n 个位置,内层遍历 m 条规则;对每条规则需要比较替换串与 target 子串,长度最多 L,还需要用模式串匹配 source 片段,长度也最多 L。因此动态规划部分为 O(nmL)。由于 n 至少为 1,总时间复杂度为 O(nmL)。
额外空间复杂度:
主要额外空间是动态规划数组 f,长度为 n+1,所以是 O(n)。字符串切片 targeti:r 和 sourcei: 在 Go 中只是视图,不会复制出长度为 L 的新字符串;匹配和比较是逐字符进行。成本数组是在原数组上累加,不额外分配大数组。因此不计输入字符串和规则本身时,总额外空间复杂度为 O(n)。
Go完整代码如下:
go
package main
import (
"fmt"
"math"
"strings"
)
func minCost(source, target string, rules [][]string, costs []int) int {
// 把成本加上 '*' 的个数,这样在 DP 中无需反复统计 '*' 的个数
for i, rule := range rules {
costs[i] += strings.Count(rule[0], "*")
}
isMatch := func(pattern, s string) bool {
for j, ch := range pattern {
if ch != '*' && byte(ch) != s[j] {
return false
}
}
return true
}
n := len(source)
f := make([]int, n+1)
for i := 1; i <= n; i++ {
f[i] = math.MaxInt
}
for i := range n {
if f[i] == math.MaxInt {
continue
}
// 不替换下标 i
if source[i] == target[i] {
f[i+1] = min(f[i+1], f[i])
}
// 替换子串 [i, i+len(replacement)-1]
for k, rule := range rules {
replacement := rule[1]
r := i + len(replacement)
if r <= n && f[i]+costs[k] < f[r] && replacement == target[i:r] && isMatch(rule[0], source[i:]) {
f[r] = f[i] + costs[k]
}
}
}
if f[n] < math.MaxInt {
return f[n]
}
return -1
}
func main() {
source := "hello"
target := "world"
rules := [][]string{{"he", "wo"}, {"llo", "rld"}}
costs := []int{3, 4}
result := minCost(source, target, rules, costs)
fmt.Println(result)
}

Python完整代码如下:
python
# -*-coding:utf-8-*-
def minCost(source, target, rules, costs):
# 复制成本数组,避免修改原数组
costs = costs[:]
# 将规则中 '*' 的数量加入对应成本
for i, rule in enumerate(rules):
costs[i] += rule[0].count('*')
def is_match(pattern, s, start):
for j, ch in enumerate(pattern):
if ch != '*' and ch != s[start + j]:
return False
return True
n = len(source)
INF = float('inf')
f = [INF] * (n + 1)
f[0] = 0
for i in range(n):
if f[i] == INF:
continue
# 不替换下标 i
if source[i] == target[i]:
if f[i] < f[i + 1]:
f[i + 1] = f[i]
# 尝试用某条规则替换从 i 开始的子串
for k, rule in enumerate(rules):
replacement = rule[1]
r = i + len(replacement)
if (
r <= n
and f[i] + costs[k] < f[r]
and replacement == target[i:r]
and is_match(rule[0], source, i)
):
f[r] = f[i] + costs[k]
return f[n] if f[n] < INF else -1
if __name__ == "__main__":
source = "hello"
target = "world"
rules = [["he", "wo"], ["llo", "rld"]]
costs = [3, 4]
result = minCost(source, target, rules, costs)
print(result)

C++完整代码如下:
cpp
#include <iostream>
#include <vector>
#include <string>
#include <climits>
#include <algorithm>
using namespace std;
int minCost(const string& source, const string& target,
vector<vector<string>> rules, vector<int> costs) {
// 把 '*' 的个数累加到成本上
for (size_t i = 0; i < rules.size(); i++) {
costs[i] += count(rules[i][0].begin(), rules[i][0].end(), '*');
}
auto isMatch = [](const string& pattern, const string& s, int start) -> bool {
for (size_t j = 0; j < pattern.size(); j++) {
if (pattern[j] != '*' && pattern[j] != s[start + j]) {
return false;
}
}
return true;
};
int n = (int)source.size();
const int INF = INT_MAX;
vector<int> f(n + 1, INF);
f[0] = 0;
for (int i = 0; i < n; i++) {
if (f[i] == INF) continue;
// 不替换下标 i
if (source[i] == target[i]) {
f[i + 1] = min(f[i + 1], f[i]);
}
// 尝试用某条规则替换从 i 开始的子串
for (size_t k = 0; k < rules.size(); k++) {
const string& replacement = rules[k][1];
int len = (int)replacement.size();
int r = i + len;
if (r <= n
&& f[i] + costs[k] < f[r]
&& target.compare(i, len, replacement) == 0
&& isMatch(rules[k][0], source, i)) {
f[r] = f[i] + costs[k];
}
}
}
return f[n] < INF ? f[n] : -1;
}
int main() {
string source = "hello";
string target = "world";
vector<vector<string>> rules = {{"he", "wo"}, {"llo", "rld"}};
vector<int> costs = {3, 4};
cout << minCost(source, target, rules, costs) << endl;
return 0;
}
