Skip to content

Conversation

@github-actions
Copy link

LeetCode 每日一题:2977. Minimum Cost to Convert String II

题目链接: https://leetcode.com/problems/minimum-cost-to-convert-string-ii/
难度: Hard

解题过程

思考过程

用户想让我解决 LeetCode 每日一题,ID 是 2977。我需要先获取题目信息,然后分析题目要求,设计算法,最后生成三个文件。

首先,调用 get_problem_info 获取题目详情。

思考过程

现在有了题目信息。题目是 "Minimum Cost to Convert String II",属于困难级别。我们需要实现一个函数 long long minimumCost(string source, string target, vector<string>& original, vector<string>& changed, vector<int>& cost)

理解一下问题:我们有两个长度相同的字符串 sourcetarget。我们还有一个转换列表:每个转换包括 original[i]changed[i]cost[i]。我们可以将 source 中的子串 x 转换为 y,前提是存在这样的转换(xy),并且成本为 z。操作次数不限,但操作必须满足以下两个条件之一:

  1. 两次操作选取的子串索引不重叠(即 b < cd < a)。
  2. 两次操作选取的子串索引完全相同(即 a == cb == d)。

这意味着操作不能部分重叠。换句话说,我们可以选择不相交的子串,或者完全相同的子串(从而允许多次转换)。这类似于对字符串进行分段转换,其中每个段可以独立转换,但段之间不能重叠。

目标:将 source 转换为 target 的最小成本。如果无法转换,则返回 -1。

注意:originalchanged 中的字符串长度可以不同,但题目说 original[i].length == changed[i].length。等等,题目约束确实写着 1 <= original[i].length == changed[i].length <= source.length。这意味着转换前后的字符串长度相等。所以子串长度在转换前后保持不变。这很重要。

因此,我们只能将子串转换为另一个长度相同的子串。

我们需要计算将 source 转换为 target 的最小成本,使用给定的转换规则,并且操作不能重叠(但可以完全重合)。

这看起来像是一个动态规划问题。设 dp[i] 表示将 source 的前 i 个字符转换为 target 的前 i 个字符的最小成本。我们需要 dp[n],其中 n = source.length()

转换可以在任意位置进行。由于操作不能重叠,我们可以考虑分段 DP:对于每个位置 i,我们可以选择将 sourcei 开始的某个子串转换为 target 的对应子串。但转换必须完全匹配子串。

另一种思路:我们可以将每个位置视为一个节点,并考虑所有可能的转换,这些转换将子串从 source 的某个区间转换为 target 的对应区间。但转换必须恰好匹配子串。

首先,我们需要一个快速的方法来判断一个子串是否可以通过一系列转换从 source 转换为 target。由于转换可以链式进行(例如,a->b 然后 b->c),我们需要计算从任意字符串 u 到任意字符串 v 的最小成本,其中 uv 长度相同,并且转换规则是给定的。

注意:转换规则只适用于长度相等的字符串,并且原始字符串和转换后的字符串长度相同。因此,我们可以将每个可能的字符串(长度为 L)视为节点,转换规则定义了一个有向加权图。我们需要计算任意两个字符串(长度相同)之间的最短路径成本。但字符串的总数可能很多:长度为 L 的字符串有 26^L 种可能,L 最大为 1000,这太大了。

然而,转换规则的数量有限:最多 100 条。这些转换规则中的字符串长度最多为 1000。我们需要处理的是,只有出现在转换规则中的字符串才是相关的。因此,我们可以将每个出现在 originalchanged 中的字符串视为节点。但字符串长度可能不同。

由于我们只关心将 source 的子串转换为 target 的子串,而这些子串的长度可能不同。但转换规则要求长度相等。因此,对于每个长度 L,我们只需要考虑该长度下的字符串转换。

我们可以为每个长度 L 构建一个图,其中节点是长度为 L 的字符串(出现在转换规则中)。然后计算所有节点对之间的最短路径(使用 Floyd-Warshall 或 Dijkstra 多次)。由于转换规则最多有 100 条,节点数量最多为 200(但可能有重复)。实际上,originalchanged 中的字符串总数最多为 200。但字符串长度可能不同,所以我们需要按长度分组。

一种方法是:为每个长度构建一个图。对于每个长度 L,收集所有长度为 L 的字符串(来自 originalchanged)。然后计算这些字符串之间的最短路径成本。由于节点数量有限(最多 200),我们可以使用 Floyd-Warshall。

然后,对于 sourcetarget,我们考虑每个子串 source[i..j]target[i..j]。如果它们相等,则成本为 0。如果它们不相等,我们需要检查是否存在从 source[i..j]target[i..j] 的转换路径。这可以通过查询该长度下的最短路径成本来实现。如果不存在,则无法转换该子串。

然后,问题简化为:将字符串分割成若干不重叠的子串(或者允许完全重合?但完全重合意味着我们可以多次转换同一个子串,但最终结果必须是 target)。由于操作不能部分重叠,我们可以将每个位置视为要么保持不变(如果 source[i]==target[i],成本为 0),要么转换为一个子串。但转换必须覆盖整个子串。

这类似于经典的“分段 DP”:dp[i] 表示前 i 个字符的最小成本。对于每个位置 i,我们可以尝试所有以 i 结尾的子串 [k, i](其中 k <= i),如果存在从 source[k..i]target[k..i] 的转换,则 dp[i] = min(dp[i], dp[k-1] + cost(k, i))。其中 cost(k, i) 是从 source[k..i] 转换为 target[k..i] 的最小成本。

因此,我们需要预先计算所有可能子串的转换成本。子串数量为 O(n^2),n 最大为 1000,即 500k,这是可行的。对于每个子串,我们需要查询转换成本。我们可以使用哈希表来存储转换成本。

但转换可能涉及中间字符串。例如,a->b 然后 b->c,成本是累加的。因此,我们需要计算任意两个字符串之间的最短路径成本。

由于字符串数量有限,我们可以为每个长度 L 构建一个图。但子串可能没有出现在图中。然而,如果子串没有出现在图中,它可能仍然可以通过一系列转换进行转换?前提是图中存在路径。但路径上的节点也必须是图中的字符串。如果子串本身不在图中,但我们可以将其转换为图中的某个字符串吗?不,转换规则只适用于图中的字符串(即出现在 originalchanged 中的字符串)。如果子串不在图中,我们不能直接应用转换规则。但我们可以先将其转换为图中的某个字符串吗?不行,因为转换规则要求原始字符串必须完全匹配 original[i]。因此,只有出现在 original 中的字符串才能作为转换的起点。类似地,只有出现在 changed 中的字符串才能作为转换的终点?实际上,转换规则定义了从 original[i]changed[i] 的边。这意味着只有 original 中的字符串才能作为转换的起点。但我们可以链式转换:从 original[i]changed[i],然后 changed[i] 可能作为另一个转换的起点(如果它出现在 original 中)。因此,图中的节点是所有出现在 originalchanged 中的字符串(因为它们是转换的一部分)。

因此,如果子串 s 不在图中,我们无法直接将其转换为任何其他字符串,因为没有以 s 为起点的转换规则。但我们可以保持 s 不变吗?如果 s 等于 target 子串,则成本为 0。否则,无法转换。因此,只有当一个子串在图中时(即它至少出现在 originalchanged 中),我们才能考虑将其转换为其他字符串。

但等等:题目说“如果存在任意索引 j 满足 cost[j]==z, original[j]==x, and changed[j]==y”,则可以将子串 x 转换为 y。这意味着 x 必须完全等于某个 original[j]。因此,只有出现在 original 中的字符串才能作为转换的起点。类似地,y 必须完全等于某个 changed[j]。因此,只有出现在 changed 中的字符串才能作为转换的终点。

因此,转换只能发生在图中定义的节点之间。但我们可以链式转换,所以如果存在一条路径从 x 到 y,其中每条边都对应一条转换规则,那么就可以实现转换。

因此,我们需要计算所有节点对(字符串)之间的最短路径成本。节点是所有出现在 originalchanged 中的字符串。但注意:同一个字符串可能同时出现在 originalchanged 中。

因此,我们可以为所有字符串(无论长度如何)构建一个图。但转换规则要求长度相等,所以实际上,只有长度相等的字符串之间才有边。因此,我们可以按长度分组,但构建一个统一的图也可以,只需在添加边时检查长度是否相等(题目已经保证相等)。

因此,算法步骤如下:

  1. 收集所有出现在 originalchanged 中的字符串,分配唯一的索引。使用哈希表 str_to_idx 进行映射。令 m 为节点数量(最多 200)。
  2. 构建一个邻接矩阵 dist,初始化为无穷大。对于每个节点 idist[i][i] = 0
  3. 对于每条转换规则 (original[i], changed[i], cost[i]),获取节点索引 uv,如果 cost[i] < dist[u][v],则更新 dist[u][v]
  4. 使用 Floyd-Warshall 计算所有节点对之间的最短路径。
  5. 现在,我们需要计算 sourcetarget 的转换成本。设 n = source.length()。定义 dp[i] 为将 source 的前 i 个字符转换为 target 的前 i 个字符的最小成本。初始化 dp[0] = 0
  6. 对于每个位置 i(从 1 到 n),初始化 dp[i] = INF。如果 source[i-1] == target[i-1],则 dp[i] = dp[i-1](即不转换)。
  7. 对于每个起始位置 k(从 1 到 i),考虑子串 s = source.substr(k-1, i-k+1)t = target.substr(k-1, i-k+1)。如果 s == t,则转换成本为 0。否则,检查 st 是否都在图中(即 str_to_idx 中存在)。如果都存在,则获取 u = str_to_idx[s]v = str_to_idx[t]。如果 dist[u][v] < INF,则成本为 dist[u][v]。否则,无法转换。如果 st 不在图中,则无法转换(除非它们相等)。
  8. 如果可转换,则 dp[i] = min(dp[i], dp[k-1] + cost)
  9. 最终,如果 dp[n] 为 INF,则返回 -1,否则返回 dp[n]

复杂度分析:Floyd-Warshall 节点数最多 200,因此 O(200^3) = 8e6,可以接受。DP 部分:O(n^2) 个子串,n=1000,即 1e6。对于每个子串,需要计算子串(O(L))并查询哈希表。但计算子串本身是 O(L) 的,如果对每个子串都重新计算,总复杂度将是 O(n^3),即 1e9,可能太高。

我们需要优化子串提取和查询。我们可以预先计算所有子串的哈希值,或者使用滚动哈希。但更简单的方法是:对于每个起始位置 k,我们可以逐个扩展子串,并逐步构建字符串。这样,对于每个起始位置 k,我们遍历结束位置 i,并在扩展时构建字符串。这可以将复杂度降至 O(n^2 * L),但 L 是子串长度,最坏情况下仍然是 O(n^3)。

但我们可以利用以下事实:图中字符串的数量有限,并且长度可能不同。我们可以只考虑那些是图中字符串的子串。因为只有这些子串才能作为转换的起点或终点。因此,我们可以枚举所有可能的转换,这些转换适用于 source 中与 original 字符串匹配的子串。

original 字符串的长度最多为 1000,数量最多为 100。我们可以对于每个 original 字符串,在 source 中寻找所有匹配的位置(使用字符串匹配算法)。类似地,对于 target 中的 changed 字符串。

但转换可能涉及多个步骤,因此我们需要图中任意两个字符串之间的最短路径。

另一种方法:对于每个起始位置 k,我们可以使用 DP 来计算将 sourcek 开始的某个后缀转换为 target 的对应后缀的最小成本。但也许我们可以使用区间 DP。

考虑到约束条件:n=1000O(n^2) 是可行的,但 O(n^3) 则不行。我们需要更高效地计算子串转换成本。

注意:我们只需要知道子串 st 是否在图中,如果在,则获取其索引。我们可以预先计算所有子串的哈希值,并将其映射到索引(如果该子串出现在图中)。但图中的字符串最多只有 200 个,长度各不相同。我们可以将每个图中的字符串映射到其所有在 sourcetarget 中出现的位置。

具体来说,我们可以对于每个图中的字符串 w,在 sourcetarget 中找到所有匹配的位置。但 sourcetarget 是固定字符串。我们可以使用 KMP 或字符串哈希来快速找到所有匹配位置。

由于图中的字符串数量有限(最多 200),我们可以为每个字符串预处理其在 sourcetarget 中的所有出现位置。然后,对于每个位置 i,我们可以考虑所有以 i 结尾的子串,这些子串与某个图中的字符串 w 匹配。类似地,对于 target 子串。

但我们需要同时匹配 sourcetarget 的子串。也许我们可以将转换视为:对于图中的每个字符串 u(在 source 中出现)和每个字符串 v(在 target 中出现),如果它们在同一位置出现且长度相同,则可以从 u 转换为 v

因此,我们可以为每个位置 i,维护一个列表,记录所有以 i 结尾的图中字符串(及其起始位置)。类似地,对于 target

然后,DP 可以这样进行:对于每个位置 i,我们可以考虑所有以 i 结尾的转换,其中 source 的子串匹配某个字符串 utarget 的子串匹配某个字符串 v,且长度相同(即起始位置相同)。由于长度相同,起始位置也相同。因此,对于每个起始位置 k,我们需要 source 的子串 starget 的子串 t 都是图中的字符串。我们可以预先计算一个映射:对于每个起始位置 k 和长度 L,如果 source 的子串 s 是图中的字符串,则记录 u;同样,如果 target 的子串 t 是图中的字符串,则记录 v。但我们可以只考虑那些是图中字符串的子串。

由于图中的字符串数量有限,我们可以枚举所有可能的转换对 (u, v),其中 usource 中出现,vtarget 中出现,且位置和长度相同。然后,转换成本为 dist[u][v]

因此,我们可以预处理一个列表 matches[k][L],但 L 可能很大。更高效的方法是:对于 source 中的每个位置 k,我们遍历所有图中的字符串 w,检查 sourcek 开始的子串是否等于 w。这需要 O(n * m * L),其中 m 最多为 200,L 平均长度可能为 500,即 1000 * 200 * 500 = 1e8,可能勉强可行,但可以优化。

我们可以使用字符串哈希(如 Rabin-Karp)来快速检查子串是否等于图中的字符串。为 sourcetarget 预先计算滚动哈希。然后,对于每个图中的字符串 w,计算其哈希值,并在 sourcetarget 中查找所有匹配位置。查找所有匹配位置可以使用滚动哈希在 O(n) 时间内完成(对于每个 w)。由于 m 最多为 200,总复杂度为 O(m * n) = 200 * 1000 = 2e5,可以接受。

因此,步骤如下:

  • sourcetarget 计算滚动哈希(或使用内置的 substr 比较?但 substr 是 O(L) 的)。我们可以使用双哈希来避免冲突。
  • 对于每个图中的字符串 w,计算其哈希值,并在 sourcetarget 中记录所有匹配位置(起始索引)。
  • 然后,构建一个列表 source_matches[k],其中包含所有以 k 开始的字符串(及其长度和节点索引)。类似地,构建 target_matches[k]
  • 然后,DP:dp[i] 表示前 i 个字符的最小成本。初始化 dp[0]=0
  • 对于每个位置 i(从 0 到 n-1),我们可以考虑从 i 开始的转换:对于 source_matches[i] 中的每个条目 (len, u),以及 target_matches[i] 中长度相同的条目 (len, v),如果存在,则转换成本为 dist[u][v]。那么,dp[i+len] = min(dp[i+len], dp[i] + cost)
  • 同时,如果 source[i] == target[i],则 dp[i+1] = min(dp[i+1], dp[i])(即不转换单个字符)。

但需要注意的是,转换可以链式进行,但我们已经通过最短路径计算了任意两个字符串之间的最小转换成本。因此,上述方法应该可行。

然而,我们还需要考虑以下情况:子串可能不是图中的字符串,但如果 source 子串等于 target 子串,则成本为 0。这已经通过单个字符相等的情况覆盖了吗?不一定,因为可能涉及多个字符。例如,sourcetarget 的整个子串相等,但都不在图中。这种情况下,我们可以直接跳过该子串,成本为 0。因此,我们需要允许直接匹配:如果 sourcei 开始的子串等于 targeti 开始的子串(对于任意长度),则我们可以跳过该子串。这可以通过扩展单个字符相等的情况来处理:我们可以将相等子串视为长度为 L 的转换,成本为 0。因此,我们可以将这种情况视为一个特殊的转换,其中 uv 相同,但不在图中。我们可以将每个子串视为一个节点,但这样节点数太多。

相反,我们可以这样处理:在 DP 中,对于每个位置 i,我们可以直接跳过一个字符(如果相等)。但为了跳过多个字符,我们可以通过多次跳过单个字符来实现,因为 DP 允许每一步跳过一个字符。因此,如果 source[i]==target[i],我们可以将 dp[i+1] 更新为 dp[i]。这隐含地允许跳过任意长度的相等子串,因为我们可以逐个字符跳过。

但还有一种情况:source 子串不等于 target 子串,但两者都不在图中。那么无法转换。这已经涵盖,因为没有匹配的转换。

现在,我们需要确保操作不重叠的限制:DP 已经确保了我们选择的转换是不重叠的,因为每次转换覆盖一个区间,并且 DP 按顺序处理。

但操作允许完全相同的子串进行多次转换。这已经通过最短路径涵盖,因为最短路径可能包含多条边,从而允许多次转换同一个子串。但最短路径计算的是图中字符串之间的最小成本,这已经包含了链式转换。因此,我们可以在一次操作中将 u 转换为 v,成本为 dist[u][v],这等价于一系列操作,且这些操作覆盖完全相同的子串(因为每次转换都在同一个子串上进行)。因此,这符合操作条件:它们完全重合。

因此,DP 应该能解决问题。

现在,我们需要考虑以下几点:

  • 可能存在多个转换规则连接两个字符串,因此我们需要最短路径。
  • 可能存在环和负成本吗?成本为正数,因此没有负环。
  • 无穷大值:使用 LLONG_MAX1e18

实现细节:

  1. 收集所有字符串:使用 unordered_map<string, int> 映射到索引。遍历 originalchanged,插入新字符串。
  2. 初始化距离矩阵 distINFINF 可以设置为 LLONG_MAX/2 以避免溢出。
  3. 对于每条边,设置 dist[u][v] = min(dist[u][v], cost[i])
  4. Floyd-Warshall:对于每个中间节点 k,遍历所有节点对 (i,j),如果 dist[i][k] + dist[k][j] < dist[i][j],则更新。
  5. 计算滚动哈希:为简单起见,我们可以使用 substr 进行比较,但这样复杂度较高。考虑到约束条件,也许我们可以接受 O(n^2 * L) 的复杂度?最坏情况下,n=1000,子串数量为 500k,每个子串的平均长度约为 500,因此总操作量为 2.5e8,可能勉强可行,但存在风险。我们尝试优化。

我们可以使用字符串哈希。实现一个简单的滚动哈希:使用 base=131 和 mod=1e9+7。为 sourcetarget 预先计算哈希数组和幂数组。

然后,对于每个图中的字符串 w,计算其哈希值,并在 sourcetarget 中查找所有匹配位置。由于长度可能不同,我们需要为每个长度单独处理。

但我们可以预先计算 sourcetarget 的所有子串哈希值,以便快速查询。这需要 O(n) 时间计算前缀哈希,然后可以在 O(1) 时间内计算任意子串的哈希值。这样,对于每个图中的字符串 w,我们可以计算其哈希值,然后遍历所有起始位置,检查子串哈希是否匹配。复杂度为 O(n * m),可以接受。

因此,实现步骤如下:

  • 计算 sourcetarget 的前缀哈希。
  • 对于每个图中的字符串 w,计算其哈希值,然后遍历 source 中所有可能的起始位置(0 到 n - len),检查哈希是否匹配。如果匹配,则记录 source_matches[start] 包含 (len, idx)
  • 同样处理 target

但需要注意:字符串长度可能不同,因此对于每个 w,我们只检查长度相同的子串。

然后,DP 循环:

vector<long long> dp(n+1, INF);
dp[0] = 0;
for (int i = 0; i < n; ++i) {
    if (source[i] == target[i]) {
        dp[i+1] = min(dp[i+1], dp[i]);
    }
    for (auto& [len, u] : source_matches[i]) {
        // 检查 target 在相同位置是否有相同长度的匹配
        // 我们需要 target_matches[i] 中长度为 len 的条目
        for (auto& [len2, v] : target_matches[i]) {
            if (len2 == len) {
                if (dist[u][v] < INF) {
                    dp[i+len] = min(dp[i+len], dp[i] + dist[u][v]);
                }
            }
        }
    }
}

但上述循环中,对于每个起始位置 i,我们需要遍历 source_matches[i]target_matches[i] 的配对。最坏情况下,每个位置可能有多个匹配(最多 m 个)。因此,复杂度为 O(n * m^2),其中 m=200,n=1000,即 1000 * 40000 = 4e7,可以接受。

但我们可以进一步优化:对于每个起始位置 i 和每个 source_matches[i] 中的条目 (len, u),我们可以直接查找 target_matches[i] 中长度为 len 的条目。我们可以将 target_matches[i] 组织成哈希表,以长度为键。这样,查找成本为 O(1)。

因此,我们可以将 target_matches[i] 定义为 unordered_map<int, vector<int>>,其中键是长度,值是对应的节点索引列表。但由于每个长度可能只有一个节点,我们可以直接存储节点索引。

但注意:在同一个起始位置,同一个长度可能对应多个节点吗?不可能,因为 target 的子串是固定的,它最多只能匹配一个图中的字符串(如果多个字符串相同,则它们对应同一个节点)。因此,我们可以使用 unordered_map<int, int>,将长度映射到节点索引。

类似地,source_matches[i] 也可以使用 unordered_map<int, int>

但可能存在多个字符串具有相同的哈希值?我们使用精确匹配,因此如果两个不同的字符串具有相同的哈希值(冲突),我们需要处理。但我们可以直接比较字符串以避免冲突。

为了简单起见,我们可以不使用哈希,而是直接比较子串,但这样复杂度较高。考虑到约束条件,我们可以接受 O(n * m * L) 的复杂度吗?也许可以,但需要谨慎。

我们决定使用字符串哈希,并在哈希匹配时进行精确比较以确保正确性。

现在,实现步骤总结:

  1. 收集所有字符串节点,分配索引。
  2. 构建距离矩阵并运行 Floyd-Warshall。
  3. 计算 sourcetarget 的前缀哈希。
  4. 对于每个节点字符串 w,计算其哈希值,并在 sourcetarget 中查找所有匹配位置,记录到 source_matchestarget_matches 中。
  5. DP 计算最小成本。

边界情况:如果 source[i]==target[i],我们可以跳过单个字符。但我们也需要处理整个子串相等的情况,这已经通过逐个字符跳过覆盖。

但考虑以下情况:sourcetarget 在位置 i 不相等,但存在一个转换,其中 source 的子串匹配某个 utarget 的子串匹配某个 v,且长度大于 1。这已经通过匹配处理。

现在,我们需要考虑无法转换的情况:如果 dp[n] 为 INF,则返回 -1。

现在,检查示例:

示例1:应该返回 28。

让我们手动验证一下。

现在,开始生成代码。

首先,我们需要确定类名和命名空间。根据题目信息:solution_class_nameMinimumCostToConvertStringIiSolutionsolution_class_baseMinimumCostToConvertStringIitest_class_nameMinimumCostToConvertStringIiTestnamespaceproblem_2977

这是一个普通题目,因此我们需要使用 SolutionBase 模式,类似于 TwoSum 示例。

但我们可以选择实现多个策略。由于这是一个困难问题,可能只有一种主要算法。我们可以实现一个策略,但为了符合模式,我们仍然可以使用 SolutionBase。我们将实现一个解决方案。

我们将生成头文件、源文件和测试文件。

首先,生成头文件。它将包含 SolutionBase 的声明。

我们需要包含 leetcode/core.h

现在,开始编写。

但在编写之前,也许我们应该先查看数据结构实现,但本题不涉及特定的数据结构(如树或链表)。因此,不需要。

现在,生成头文件。

思考过程

现在,文件已经生成。接下来,需要编译项目来检查是否有错误。调用 build_project。

思考过程

编译成功了。很好。现在运行测试来验证算法是否正确。调用 run_tests。

思考过程

所有测试都通过了!很好。

现在,算法应该已经正确了。但我还需要思考一下是否有任何边界情况没有覆盖到。例如,可能存在这样的情况:子串不在图中,但可以通过一系列转换来转换,其中涉及多个子串?不,因为每个转换都必须以图中的字符串作为起点和终点。如果子串不在图中,就不能作为转换的起点。但也许我们可以先转换一个更长的子串,然后再转换其中的一部分?但操作不能重叠,除非完全重合。所以,如果子串不在图中,我们不能直接转换它,但可以通过转换一个更大的子串(包含它)来间接转换?然而,转换一个更大的子串会改变整个区域,这可能使内部子串的转换变得不必要。

但考虑一下:假设 source = "ab", target = "cd"。存在转换 "ab" -> "cd",成本为 10。这没问题。但如果没有直接的转换,而是有 "ab" -> "ef" 和 "ef" -> "cd",那么最短路径会捕捉到这一点。

另一种情况:source 子串和 target 子串相等,但不在图中,我们已经通过逐个字符跳过处理了。

现在,复杂度分析:Floyd-Warshall 是 O(K^3),其中 K ≤ 200,即 800 万次操作,可以接受。DP 是 O(n * (M + N)),其中 M 和 N 是每个位置的匹配数,最坏情况下 O(n * m),即 20 万次操作。字符串匹配是 O(n * m),即 20 万次操作。总体上是可行的。

但我们需要确保滚动哈希不会冲突。我们使用了模 1e9+7,但冲突的可能性很低。为了安全起见,我们可以在哈希匹配后进行字符串比较。我已经在匹配时进行了比较(使用了 substr)。但 substr 会创建新的字符串,可能导致性能下降。不过,由于匹配次数有限,这应该可以接受。

我们也可以使用双哈希来进一步降低冲突概率,但为了简单起见,我们只使用单哈希和直接比较。

现在,思考过程已经完成。我应该输出一个总结,但用户只要求解决问题,所以我可以回复说问题已经解决,并附上解释。

但用户可能也希望看到解题报告。我会用中文写一个简要的思考过程,因为用户是中文的。

解题步骤:

  1. 理解问题:给定两个字符串 source 和 target,以及一组转换规则,可以将子串从 original[i] 转换为 changed[i],成本为 cost[i]。操作可以任意进行,但操作之间不能部分重叠(可以完全重合)。求最小成本。
  2. 关键点:转换只能应用于完全匹配 original[i] 的子串,且转换前后长度相等。因此,可以建立有向图,节点为所有出现在 original 或 changed 中的字符串,边为转换成本。
  3. 计算图中任意两个节点之间的最短路径成本(使用 Floyd-Warshall),因为转换可以链式进行。
  4. 由于操作不能重叠,可以使用动态规划:dp[i] 表示将 source 的前 i 个字符转换为 target 的前 i 个字符的最小成本。
  5. 对于每个位置 i,可以跳过单个字符(如果 source[i]==target[i]),或者尝试将 source 从 i 开始的子串转换为 target 的对应子串,前提是这两个子串都在图中,并且存在转换路径。
  6. 为了快速判断子串是否在图中,可以使用滚动哈希预先计算 source 和 target 中所有与图中字符串匹配的子串位置。
  7. 最终 dp[n] 即为答案,若为无穷大则返回 -1。

时间复杂度:O(K^3 + n * m + n * L),其中 K 为图中节点数(≤200),n 为字符串长度(≤1000),m 为转换规则数(≤100),L 为图中字符串的平均长度。空间复杂度:O(K^2 + n * m)。

现在,问题已经解决,测试通过。我可以回复用户了。

AI 回复

我已经帮你解决了 LeetCode 2977 题 "Minimum Cost to Convert String II"。这是一个困难级别的字符串转换问题,需要使用图的最短路径和动态规划相结合的方法。

算法思路

问题理解

给定两个长度相同的字符串 sourcetarget,以及一组转换规则 (original[i], changed[i], cost[i])。每次操作可以将 source 中的一个子串 x 转换为 y,条件是在转换规则中存在 original[j] == xchanged[j] == y。操作可以多次进行,但任何两个操作要么完全不重叠,要么完全重合。

关键洞察

  1. 转换图的构建:只有出现在 originalchanged 中的字符串才能作为转换的起点或终点。将每个字符串视为图中的一个节点,转换规则作为有向边,边的权重为转换成本。
  2. 最短路径计算:由于转换可以链式进行(如 a→b→c),需要计算图中任意两个节点之间的最短路径成本,使用 Floyd-Warshall 算法。
  3. 非重叠操作约束:这等价于将字符串分割成若干不重叠的段,每段可以独立转换或保持不变。因此可以使用动态规划。
  4. 字符串匹配优化:为了快速判断 sourcetarget 中的子串是否在图中,使用滚动哈希(Rabin-Karp)预先计算所有匹配位置。

算法步骤

  1. 建图与最短路径

    • 收集所有出现在 originalchanged 中的字符串,分配唯一索引。
    • 构建邻接矩阵,初始化边权重。
    • 使用 Floyd-Warshall 计算所有节点对之间的最短路径成本。
  2. 预处理匹配位置

    • sourcetarget 计算滚动哈希。
    • 对于图中的每个字符串,在 sourcetarget 中查找所有匹配位置,记录起始位置和节点索引。
  3. 动态规划

    • dp[i] 表示将 source 的前 i 个字符转换为 target 的前 i 个字符的最小成本。
    • 初始化 dp[0] = 0
    • 对于每个位置 i
      • 如果 source[i] == target[i],可以直接跳过该字符:dp[i+1] = min(dp[i+1], dp[i])
      • 对于所有以 i 开始且在图中存在的 source 子串,检查相同位置是否存在相同长度的 target 子串也在图中。如果存在且最短路径成本有限,则进行转换:dp[i+len] = min(dp[i+len], dp[i] + dist[u][v])
    • 最终 dp[n] 即为答案,如果为无穷大则返回 -1。

复杂度分析

  • 时间复杂度
    • Floyd-Warshall: O(K³),其中 K ≤ 200(节点数),约为 8×10⁶。
    • 字符串匹配: O(n × m),其中 n ≤ 1000,m ≤ 100,约为 10⁵。
    • 动态规划: O(n × M),其中 M 是每个位置的匹配数,最坏情况下 O(n × m),约为 10⁵。
  • 空间复杂度:O(K² + n × m),用于存储距离矩阵和匹配信息。

测试结果

✅ 所有测试用例通过:

  • 示例 1:输出 28
  • 示例 2:输出 9
  • 示例 3:输出 -1
  • 额外测试:单字符转换、无法转换、相同字符串等情况均正确

代码已生成并编译通过,可以直接使用。这个解决方案结合了图论、动态规划和字符串哈希,能够高效处理最大约束条件下的所有情况。

✅ 解题完成!代码已通过编译和测试。


本报告由 AI 自动生成,包含完整的思考过程和解题步骤。

@0xMashiro 0xMashiro merged commit f9f1b4f into main Jan 31, 2026
@0xMashiro 0xMashiro deleted the daily-challenge-2977-21502399459 branch January 31, 2026 07:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant