1. 题目解析:配送轨迹的最优修剪
这道题目描述了一个典型的轨迹优化问题:给定一个由字符组成的移动指令序列(如"WASD"分别代表上、左、下、右移动),我们需要从中删除一段连续的子串,使得剩余的指令序列能够将物体从原点(0,0)移动到指定的目标位置(x,y)。如果存在多个可行解,我们需要找到删除长度最短的那个。
1.1 问题建模
首先我们需要明确几个关键要素:
- 原始指令序列:长度为n的字符串,每个字符代表一个移动方向
- 删除区间:需要删除的连续子串[start, end]
- 剩余指令:原始序列去掉删除区间后的部分
- 目标位置:剩余指令执行后应到达的坐标(x,y)
举个例子,假设原始指令是"WASD",目标位置是(0,1):
- 删除"AS"子串(第1-2个字符),剩余"WD":W(上)到达(0,1),D(右)到达(1,1) → 不符合
- 删除"W"子串,剩余"ASD":A(左)到达(-1,0),S(下)到达(-1,-1),D(右)到达(0,-1) → 不符合
- 删除"A"子串,剩余"WSD":W(上)到达(0,1),S(下)回到(0,0),D(右)到达(1,0) → 不符合
- 删除"S"子串,剩余"WAD":W(上)到达(0,1),A(左)到达(-1,1),D(右)回到(0,1) → 符合条件
1.2 核心难点
这个问题的挑战在于如何高效地找到满足条件的删除区间。直接暴力枚举所有可能的删除区间(O(n^2)时间复杂度)在n较大时(比如1e5)会非常低效。我们需要找到一种线性时间复杂度的解法。
2. 算法设计:前缀和与哈希表优化
2.1 关键观察
设原始指令序列的总位移为(totalX, totalY),要删除的子串位移为(delX, delY),则剩余位移应满足: totalX - delX = x totalY - delY = y
因此,我们需要找到的子串应该满足: delX = totalX - x delY = totalY - y
2.2 前缀和预处理
我们可以预先计算每个位置的前缀位移:
- prefixX[i]:前i个指令在x轴上的位移
- prefixY[i]:前i个指令在y轴上的位移
这样,任意子串s[i..j]的位移可以表示为: delX = prefixX[j] - prefixX[i-1] delY = prefixY[j] - prefixY[i-1]
2.3 哈希表优化查找
我们需要找到满足: prefixX[j] - prefixX[i] = totalX - x prefixY[j] - prefixY[i] = totalY - y
可以转换为: prefixX[j] = prefixX[i] + (totalX - x) prefixY[j] = prefixY[i] + (totalY - y)
因此,我们可以:
- 预先计算所有位置的(prefixX, prefixY)
- 使用哈希表记录每个(prefixX, prefixY)出现的最早位置
- 对于每个j,查找是否存在i使得上述等式成立
2.4 算法步骤
- 计算完整序列的总位移(totalX, totalY)
- 计算目标位移差(targetX, targetY) = (totalX - x, totalY - y)
- 如果targetX和targetY都为0,则无需删除任何子串
- 初始化前缀和数组和哈希表
- 遍历序列,维护当前前缀和
- 对于每个位置j,查找哈希表中是否存在(prefixX[j] - targetX, prefixY[j] - targetY)
- 如果存在,则找到一个候选区间[i+1, j]
- 记录满足条件的最小区间长度
3. 代码实现与细节处理
3.1 Java实现
import java.util.HashMap; import java.util.Map; public class Main { public static void main(String[] args) { // 示例测试 System.out.println(minDeleteLength("WASD", 0, 1)); // 应返回1 } public static int minDeleteLength(String s, int x, int y) { int n = s.length(); int totalX = 0, totalY = 0; // 计算总位移 for (char c : s.toCharArray()) { switch (c) { case 'W': totalY++; break; case 'A': totalX--; break; case 'S': totalY--; break; case 'D': totalX++; break; } } int targetX = totalX - x; int targetY = totalY - y; // 特殊情况处理 if (targetX == 0 && targetY == 0) { return 0; // 无需删除 } if (n == 0) { return -1; // 空序列无法到达非零点 } Map<String, Integer> map = new HashMap<>(); map.put("0,0", -1); // 初始位置 int minLen = Integer.MAX_VALUE; int currX = 0, currY = 0; for (int j = 0; j < n; j++) { char c = s.charAt(j); switch (c) { case 'W': currY++; break; case 'A': currX--; break; case 'S': currY--; break; case 'D': currX++; break; } String key = (currX - targetX) + "," + (currY - targetY); if (map.containsKey(key)) { int i = map.get(key); minLen = Math.min(minLen, j - i); } String currKey = currX + "," + currY; if (!map.containsKey(currKey)) { map.put(currKey, j); } } return minLen == Integer.MAX_VALUE ? -1 : minLen; } }3.2 C++实现
#include <iostream> #include <unordered_map> #include <string> #include <climits> using namespace std; int minDeleteLength(const string& s, int x, int y) { int n = s.size(); int totalX = 0, totalY = 0; // 计算总位移 for (char c : s) { switch (c) { case 'W': totalY++; break; case 'A': totalX--; break; case 'S': totalY--; break; case 'D': totalX++; break; } } int targetX = totalX - x; int targetY = totalY - y; // 特殊情况处理 if (targetX == 0 && targetY == 0) return 0; if (n == 0) return -1; unordered_map<string, int> map; map["0,0"] = -1; int minLen = INT_MAX; int currX = 0, currY = 0; for (int j = 0; j < n; ++j) { switch (s[j]) { case 'W': currY++; break; case 'A': currX--; break; case 'S': currY--; break; case 'D': currX++; break; } string key = to_string(currX - targetX) + "," + to_string(currY - targetY); if (map.count(key)) { int i = map[key]; minLen = min(minLen, j - i); } string currKey = to_string(currX) + "," + to_string(currY); if (!map.count(currKey)) { map[currKey] = j; } } return minLen == INT_MAX ? -1 : minLen; } int main() { cout << minDeleteLength("WASD", 0, 1) << endl; // 应输出1 return 0; }3.3 Python实现
def min_delete_length(s: str, x: int, y: int) -> int: n = len(s) total_x, total_y = 0, 0 # 计算总位移 for c in s: if c == 'W': total_y += 1 elif c == 'A': total_x -= 1 elif c == 'S': total_y -= 1 elif c == 'D': total_x += 1 target_x, target_y = total_x - x, total_y - y # 特殊情况处理 if target_x == 0 and target_y == 0: return 0 if n == 0: return -1 pos_map = {} pos_map[(0, 0)] = -1 min_len = float('inf') curr_x, curr_y = 0, 0 for j in range(n): c = s[j] if c == 'W': curr_y += 1 elif c == 'A': curr_x -= 1 elif c == 'S': curr_y -= 1 elif c == 'D': curr_x += 1 key = (curr_x - target_x, curr_y - target_y) if key in pos_map: i = pos_map[key] min_len = min(min_len, j - i) curr_key = (curr_x, curr_y) if curr_key not in pos_map: pos_map[curr_key] = j return min_len if min_len != float('inf') else -1 # 测试示例 print(min_delete_length("WASD", 0, 1)) # 应输出14. 算法分析与优化
4.1 时间复杂度
- 计算总位移:O(n)
- 构建前缀和并遍历:O(n)
- 哈希表操作:平均O(1)
- 总体时间复杂度:O(n)
4.2 空间复杂度
- 哈希表存储前缀和位置:最坏情况O(n)
- 总体空间复杂度:O(n)
4.3 边界情况处理
- 空输入序列:直接返回-1
- 无需删除任何子串(总位移已等于目标):返回0
- 无法通过删除任何子串达到目标:返回-1
- 多个可行解时选择最短的删除长度
4.4 优化技巧
- 使用更紧凑的哈希键表示:可以用(x<<32)|y的方式将坐标对转换为整数键
- 提前终止:当找到长度为1的可行解时可以直接返回
- 内存优化:不需要存储完整的前缀和数组,只需维护当前前缀和
5. 常见问题与调试技巧
5.1 典型错误
坐标计算错误:容易混淆x和y轴的方向
- 确认W/S对应y轴,A/D对应x轴
- 确认正方向定义(W和D为正,A和S为负)
哈希表初始化遗漏:
- 必须初始化(0,0)对应位置-1
- 否则会错过从序列开头删除的情况
索引处理错误:
- 删除区间长度应为j-i,不是j-i+1
- 因为i是前缀和的前一个位置
5.2 调试建议
打印关键变量:
- 打印总位移和目标位移差
- 打印遍历过程中的当前前缀和
- 打印哈希表查找结果
小规模测试用例:
- 测试空序列
- 测试无需删除的情况
- 测试必须删除整个序列的情况
- 测试无法达成目标的情况
可视化轨迹:
- 对于复杂案例,可以绘制原始轨迹和修剪后轨迹
- 确认最终位置是否符合预期
5.3 性能优化验证
对于n=1e5的大规模数据:
- Java/C++应能在100ms内完成
- Python可能需要在500ms内完成
- 如果超时,检查是否有不必要的拷贝或低效的哈希操作
6. 扩展思考
6.1 变种问题
允许删除多个不连续的子串
- 问题复杂度将大幅增加
- 可能需要动态规划解法
考虑移动代价最小化
- 不同方向移动可能有不同代价
- 需要寻找总代价最小的删除方案
三维空间轨迹
- 增加z轴移动指令
- 算法思路可以类似扩展
6.2 实际应用场景
机器人路径优化:
- 删除冗余移动指令
- 优化执行效率
游戏AI路径校正:
- 修正错误路径指令
- 确保到达目标位置
物流配送路线优化:
- 调整配送路线
- 避开拥堵路段
6.3 算法选择对比
暴力法:
- 枚举所有可能删除区间
- O(n^2)时间复杂度
- 适合n较小的情况
前缀和+哈希表:
- O(n)时间复杂度
- 需要O(n)额外空间
- 适合大规模数据
滑动窗口:
- 可能适用于某些约束变种
- 但本题不保证单调性,难以应用
在实际面试中,面试官通常会期待候选人能够从暴力法出发,逐步优化到线性时间复杂度的解法,并清楚地解释每一步优化的思路和原理。