2026-10-18:转换字符串的最小成本Ⅲ。用go语言,给定两个字符串 source 和 target,以及若干条替换规则。每条规则包含一个模式串、一个替换串

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;
}
相关推荐
韩振方1 小时前
Kubernetes Secret 让数据库密码离开镜像和 Pod 配置
后端
闽林致远1 小时前
基于标准 C++ 的插件化多线程通用数据采集框架|支持自定义业务模块,Oracle/MySQL 入库、FTP 图片上传,Windows/Linux 跨平台部署
后端
doujiaozh1 小时前
协议演进实录:从明文哈希到 AES-GCM 加密,如何平滑升级?
后端·node.js
花卷持续成长1 小时前
慢SQL的整体优化思路
后端·mysql
Sashtok1 小时前
cpp基础知识(4)
后端
Sashtok1 小时前
cpp基础知识(6)
后端
用户2145549310151 小时前
VeapAI 自然语言操作实战|第 02 篇:语义层——把系统讲给模型听的四个构件
前端·后端
斑鸠喳喳1 小时前
期货模式 FutureTask
后端
Thneonl1 小时前
一次 rebalance,全组停摆:Kafka lag 从 0 到百万的事故复盘
后端·架构