ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

轨迹优化算法:前缀和与哈希表应用

轨迹优化算法:前缀和与哈希表应用

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)

因此,我们可以:

  1. 预先计算所有位置的(prefixX, prefixY)
  2. 使用哈希表记录每个(prefixX, prefixY)出现的最早位置
  3. 对于每个j,查找是否存在i使得上述等式成立

2.4 算法步骤

  1. 计算完整序列的总位移(totalX, totalY)
  2. 计算目标位移差(targetX, targetY) = (totalX - x, totalY - y)
  3. 如果targetX和targetY都为0,则无需删除任何子串
  4. 初始化前缀和数组和哈希表
  5. 遍历序列,维护当前前缀和
  6. 对于每个位置j,查找哈希表中是否存在(prefixX[j] - targetX, prefixY[j] - targetY)
  7. 如果存在,则找到一个候选区间[i+1, j]
  8. 记录满足条件的最小区间长度

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)) # 应输出1

4. 算法分析与优化

4.1 时间复杂度

  • 计算总位移:O(n)
  • 构建前缀和并遍历:O(n)
  • 哈希表操作:平均O(1)
  • 总体时间复杂度:O(n)

4.2 空间复杂度

  • 哈希表存储前缀和位置:最坏情况O(n)
  • 总体空间复杂度:O(n)

4.3 边界情况处理

  1. 空输入序列:直接返回-1
  2. 无需删除任何子串(总位移已等于目标):返回0
  3. 无法通过删除任何子串达到目标:返回-1
  4. 多个可行解时选择最短的删除长度

4.4 优化技巧

  1. 使用更紧凑的哈希键表示:可以用(x<<32)|y的方式将坐标对转换为整数键
  2. 提前终止:当找到长度为1的可行解时可以直接返回
  3. 内存优化:不需要存储完整的前缀和数组,只需维护当前前缀和

5. 常见问题与调试技巧

5.1 典型错误

  1. 坐标计算错误:容易混淆x和y轴的方向

    • 确认W/S对应y轴,A/D对应x轴
    • 确认正方向定义(W和D为正,A和S为负)
  2. 哈希表初始化遗漏:

    • 必须初始化(0,0)对应位置-1
    • 否则会错过从序列开头删除的情况
  3. 索引处理错误:

    • 删除区间长度应为j-i,不是j-i+1
    • 因为i是前缀和的前一个位置

5.2 调试建议

  1. 打印关键变量:

    • 打印总位移和目标位移差
    • 打印遍历过程中的当前前缀和
    • 打印哈希表查找结果
  2. 小规模测试用例:

    • 测试空序列
    • 测试无需删除的情况
    • 测试必须删除整个序列的情况
    • 测试无法达成目标的情况
  3. 可视化轨迹:

    • 对于复杂案例,可以绘制原始轨迹和修剪后轨迹
    • 确认最终位置是否符合预期

5.3 性能优化验证

对于n=1e5的大规模数据:

  • Java/C++应能在100ms内完成
  • Python可能需要在500ms内完成
  • 如果超时,检查是否有不必要的拷贝或低效的哈希操作

6. 扩展思考

6.1 变种问题

  1. 允许删除多个不连续的子串

    • 问题复杂度将大幅增加
    • 可能需要动态规划解法
  2. 考虑移动代价最小化

    • 不同方向移动可能有不同代价
    • 需要寻找总代价最小的删除方案
  3. 三维空间轨迹

    • 增加z轴移动指令
    • 算法思路可以类似扩展

6.2 实际应用场景

  1. 机器人路径优化:

    • 删除冗余移动指令
    • 优化执行效率
  2. 游戏AI路径校正:

    • 修正错误路径指令
    • 确保到达目标位置
  3. 物流配送路线优化:

    • 调整配送路线
    • 避开拥堵路段

6.3 算法选择对比

  1. 暴力法:

    • 枚举所有可能删除区间
    • O(n^2)时间复杂度
    • 适合n较小的情况
  2. 前缀和+哈希表:

    • O(n)时间复杂度
    • 需要O(n)额外空间
    • 适合大规模数据
  3. 滑动窗口:

    • 可能适用于某些约束变种
    • 但本题不保证单调性,难以应用

在实际面试中,面试官通常会期待候选人能够从暴力法出发,逐步优化到线性时间复杂度的解法,并清楚地解释每一步优化的思路和原理。

返回列表