【华为机试】1208. 尽可能使字符串相等
文章目录
- 1208. 尽可能使字符串相等
- 描述
- 示例 1
- 示例 2
- 示例 3
- 提示
- 解题思路
- 方法一:滑动窗口(推荐)
- 方法二:前缀和 + 二分查找
- 代码实现
- 测试用例
- 关键点总结
- 相似题目
- 完整题解代码
1208. 尽可能使字符串相等
描述
给你两个长度相同的字符串,s 和 t。
将 s 中的第 i 个字符变到 t 中的第 i 个字符需要 |s[i] - t[i]| 的开销(开销可能为 0),也就是两个字符的 ASCII 码值的差的绝对值。
用于变更字符串的最大预算是 maxCost。在转化字符串时,总开销应当小于等于该预算,这也意味着字符串的转化可能是不完全的。
如果你可以将 s 的子字符串转化为它在 t 中对应的子字符串,则返回可以转化的最大长度。
如果 s 中没有子字符串可以转化成 t 中对应的子字符串,则返回 0。
示例 1
输入:s = “abcd”, t = “bcdf”, maxCost = 3
输出:3
解释:s 中的 “abc” 可以变为 “bcd”。开销为 3,所以最大长度为 3。
示例 2
输入:s = “abcd”, t = “cdef”, maxCost = 3
输出:1
解释:s 中的任一字符要想变成 t 中对应的字符,其开销都是 2。因此,最大长度为 1。
示例 3
输入:s = “abcd”, t = “acde”, maxCost = 0
输出:1
解释:a -> a, cost = 0,字符串未发生变化,所以最大长度为 1。
提示
- 1 <= s.length, t.length <= 10^5
- 0 <= maxCost <= 10^6
- s 和 t 都只含小写英文字母。
解题思路
方法一:滑动窗口(推荐)
这道题的本质是找到一个最长的连续子数组,使得子数组中所有元素的和不超过 maxCost
。
算法步骤:
- 计算开销数组:首先计算每个位置字符转换的开销
costs[i] = |s[i] - t[i]|
- 滑动窗口:使用双指针维护一个滑动窗口,窗口内的开销总和不超过
maxCost
- 更新答案:在滑动过程中记录最大窗口长度
算法流程:
时间复杂度: O(n),其中 n 是字符串长度
空间复杂度: O(1),只使用常数额外空间
方法二:前缀和 + 二分查找
算法思路:
- 计算开销数组的前缀和
- 对于每个起始位置,使用二分查找找到最远的结束位置
时间复杂度: O(n log n)
空间复杂度: O(n)
代码实现
// 方法一:滑动窗口
func equalSubstring(s string, t string, maxCost int) int {n := len(s)left, right := 0, 0currentCost := 0maxLength := 0for right < n {// 计算当前字符的开销cost := abs(int(s[right]) - int(t[right]))// 扩展窗口currentCost += costright++// 如果开销超过预算,收缩窗口for currentCost > maxCost {currentCost -= abs(int(s[left]) - int(t[left]))left++}// 更新最大长度maxLength = max(maxLength, right - left)}return maxLength
}func abs(x int) int {if x < 0 {return -x}return x
}func max(a, b int) int {if a > b {return a}return b
}
测试用例
func testEqualSubstring() {testCases := []struct {s, t stringmaxCost intexpected int}{{"abcd", "bcdf", 3, 3},{"abcd", "cdef", 3, 1},{"abcd", "acde", 0, 1},{"krrgw", "zjxss", 19, 2},{"abcd", "abcd", 0, 4},{"a", "b", 1, 1},{"a", "b", 0, 0},}for i, tc := range testCases {result := equalSubstring(tc.s, tc.t, tc.maxCost)if result == tc.expected {fmt.Printf("测试用例 %d 通过: s=%s, t=%s, maxCost=%d, 结果=%d\n", i+1, tc.s, tc.t, tc.maxCost, result)} else {fmt.Printf("测试用例 %d 失败: s=%s, t=%s, maxCost=%d, 期望=%d, 实际=%d\n", i+1, tc.s, tc.t, tc.maxCost, tc.expected, result)}}
}
关键点总结
- 问题转化:将字符串转换问题转化为子数组和不超过阈值的最大长度问题
- 滑动窗口:使用双指针维护满足条件的连续子数组
- 开销计算:使用 ASCII 码差的绝对值计算转换开销
- 边界处理:注意处理
maxCost = 0
的特殊情况 - 优化技巧:滑动窗口可以在 O(n) 时间内找到最优解
相似题目
- 209. 长度最小的子数组 - 滑动窗口找最小长度
- 1004. 最大连续1的个数 III - 滑动窗口找最大长度
- 3. 无重复字符的最长子串 - 滑动窗口经典应用
完整题解代码
package mainimport "fmt"// 方法一:滑动窗口(推荐)
func equalSubstring(s string, t string, maxCost int) int {n := len(s)left, right := 0, 0currentCost := 0maxLength := 0for right < n {// 计算当前字符的开销cost := abs(int(s[right]) - int(t[right]))// 扩展窗口currentCost += costright++// 如果开销超过预算,收缩窗口for currentCost > maxCost {currentCost -= abs(int(s[left]) - int(t[left]))left++}// 更新最大长度maxLength = max(maxLength, right-left)}return maxLength
}// 方法二:前缀和 + 二分查找
func equalSubstring2(s string, t string, maxCost int) int {n := len(s)// 计算前缀和prefixSum := make([]int, n+1)for i := 0; i < n; i++ {cost := abs(int(s[i]) - int(t[i]))prefixSum[i+1] = prefixSum[i] + cost}maxLength := 0// 对于每个起始位置,二分查找最远的结束位置for i := 0; i < n; i++ {// 二分查找最远的j,使得prefixSum[j+1] - prefixSum[i] <= maxCostleft, right := i, n-1for left <= right {mid := left + (right-left)/2cost := prefixSum[mid+1] - prefixSum[i]if cost <= maxCost {maxLength = max(maxLength, mid-i+1)left = mid + 1} else {right = mid - 1}}}return maxLength
}// 方法三:暴力解法(用于验证)
func equalSubstring3(s string, t string, maxCost int) int {n := len(s)maxLength := 0for i := 0; i < n; i++ {currentCost := 0for j := i; j < n; j++ {cost := abs(int(s[j]) - int(t[j]))currentCost += costif currentCost <= maxCost {maxLength = max(maxLength, j-i+1)} else {break}}}return maxLength
}func abs(x int) int {if x < 0 {return -x}return x
}func max(a, b int) int {if a > b {return a}return b
}// 测试函数
func testEqualSubstring() {testCases := []struct {s, t stringmaxCost intexpected int}{{"abcd", "bcdf", 3, 3},{"abcd", "cdef", 3, 1},{"abcd", "acde", 0, 1},{"krrgw", "zjxss", 19, 2},{"abcd", "abcd", 0, 4},{"a", "b", 1, 1},{"a", "b", 0, 0},{"abcdef", "bcdefg", 5, 5},{"xyz", "abc", 10, 3}, // x->a: 23, y->b: 23, z->c: 23, 总开销=69 > 10,所以只能转换1个字符{"xyz", "abc", 2, 0}, // 任何字符转换开销都超过2,所以结果为0}fmt.Println("=== 测试滑动窗口算法 ===")for i, tc := range testCases {result := equalSubstring(tc.s, tc.t, tc.maxCost)if result == tc.expected {fmt.Printf("✅ 测试用例 %d 通过: s=%s, t=%s, maxCost=%d, 结果=%d\n",i+1, tc.s, tc.t, tc.maxCost, result)} else {fmt.Printf("❌ 测试用例 %d 失败: s=%s, t=%s, maxCost=%d, 期望=%d, 实际=%d\n",i+1, tc.s, tc.t, tc.maxCost, tc.expected, result)}}fmt.Println("\n=== 测试前缀和+二分查找算法 ===")for i, tc := range testCases {result := equalSubstring2(tc.s, tc.t, tc.maxCost)if result == tc.expected {fmt.Printf("✅ 测试用例 %d 通过: s=%s, t=%s, maxCost=%d, 结果=%d\n",i+1, tc.s, tc.t, tc.maxCost, result)} else {fmt.Printf("❌ 测试用例 %d 失败: s=%s, t=%s, maxCost=%d, 期望=%d, 实际=%d\n",i+1, tc.s, tc.t, tc.maxCost, tc.expected, result)}}fmt.Println("\n=== 测试暴力解法 ===")for i, tc := range testCases {result := equalSubstring3(tc.s, tc.t, tc.maxCost)if result == tc.expected {fmt.Printf("✅ 测试用例 %d 通过: s=%s, t=%s, maxCost=%d, 结果=%d\n",i+1, tc.s, tc.t, tc.maxCost, result)} else {fmt.Printf("❌ 测试用例 %d 失败: s=%s, t=%s, maxCost=%d, 期望=%d, 实际=%d\n",i+1, tc.s, tc.t, tc.maxCost, tc.expected, result)}}
}// 性能对比测试
func benchmarkTest() {s := "abcdefghijklmnopqrstuvwxyz"t := "bcdefghijklmnopqrstuvwxyza"maxCost := 100fmt.Println("\n=== 性能对比测试 ===")fmt.Printf("输入: s=%s, t=%s, maxCost=%d\n", s, t, maxCost)// 测试滑动窗口result1 := equalSubstring(s, t, maxCost)fmt.Printf("滑动窗口结果: %d\n", result1)// 测试前缀和+二分查找result2 := equalSubstring2(s, t, maxCost)fmt.Printf("前缀和+二分查找结果: %d\n", result2)// 测试暴力解法result3 := equalSubstring3(s, t, maxCost)fmt.Printf("暴力解法结果: %d\n", result3)if result1 == result2 && result2 == result3 {fmt.Println("✅ 所有算法结果一致")} else {fmt.Println("❌ 算法结果不一致")}
}// 调试函数:手动验证测试用例
func debugTestCase() {fmt.Println("\n=== 调试测试用例 ===")// 测试用例9: "xyz" -> "abc", maxCost=10s1, t1 := "xyz", "abc"maxCost1 := 10fmt.Printf("测试用例9: s=%s, t=%s, maxCost=%d\n", s1, t1, maxCost1)for i := 0; i < len(s1); i++ {cost := abs(int(s1[i]) - int(t1[i]))fmt.Printf(" %c -> %c: |%d - %d| = %d\n", s1[i], t1[i], int(s1[i]), int(t1[i]), cost)}// 计算所有可能的子字符串开销fmt.Println(" 所有可能的子字符串开销:")for i := 0; i < len(s1); i++ {for j := i; j < len(s1); j++ {totalCost := 0for k := i; k <= j; k++ {totalCost += abs(int(s1[k]) - int(t1[k]))}fmt.Printf(" [%d,%d]: %s -> %s, 开销=%d\n", i, j, s1[i:j+1], t1[i:j+1], totalCost)}}result1 := equalSubstring(s1, t1, maxCost1)fmt.Printf(" 结果: %d\n", result1)// 测试用例10: "xyz" -> "abc", maxCost=2maxCost2 := 2fmt.Printf("\n测试用例10: s=%s, t=%s, maxCost=%d\n", s1, t1, maxCost2)// 计算所有可能的子字符串开销fmt.Println(" 所有可能的子字符串开销:")for i := 0; i < len(s1); i++ {for j := i; j < len(s1); j++ {totalCost := 0for k := i; k <= j; k++ {totalCost += abs(int(s1[k]) - int(t1[k]))}fmt.Printf(" [%d,%d]: %s -> %s, 开销=%d\n", i, j, s1[i:j+1], t1[i:j+1], totalCost)}}result2 := equalSubstring(s1, t1, maxCost2)fmt.Printf(" 结果: %d\n", result2)
}func main() {fmt.Println("1208. 尽可能使字符串相等")fmt.Println("========================")// 运行调试函数debugTestCase()// 运行测试用例testEqualSubstring()// 运行性能对比benchmarkTest()// 交互式测试fmt.Println("\n=== 交互式测试 ===")fmt.Println("请输入测试用例 (格式: s t maxCost,例如: abcd bcdf 3)")var s, t stringvar maxCost intfmt.Print("请输入 s: ")fmt.Scanln(&s)fmt.Print("请输入 t: ")fmt.Scanln(&t)fmt.Print("请输入 maxCost: ")fmt.Scanln(&maxCost)if len(s) != len(t) {fmt.Println("❌ 错误:字符串长度必须相同")return}result := equalSubstring(s, t, maxCost)fmt.Printf("结果: %d\n", result)
}