一、引言
在动态规划与贪心算法结合 的面试题库中,LeetCode 2463 最小移动总距离是一道极具代表性的经典难题。它以「X 轴上机器人与工厂的最优匹配」为目标,既不依赖复杂数学推导,也不涉及冷门数据结构,却精准考察了贪心排序思想、状态定义设计、分组背包模型转化以及空间优化四大核心能力。看似简单的机器人分配问题,背后藏着从暴力枚举到最优子结构的完整算法演进逻辑,也是面试官常用来检验算法思维是否严谨的高频考题。
本文围绕两种最主流、最实用的解法展开:贪心排序 + 动态规划(分组背包转化法),同时补充暴力回溯的思路对比,帮助你彻底吃透这类「带容量限制的最优匹配」问题。
全文从题目本质出发,先明确问题模型与核心思路,再逐步推导状态转移逻辑,最后给出可直接提交的完整代码,力求让你不仅 AC 题目,更能清晰讲清思路、从容应对同类集和与目标和问题。

二、贪心排序
2.1 贪心
贪心 = 每一步都做当前看起来最好的选择,不回头、不反悔,希望最后整体也最优。
特点:
- 只看眼前局部最优
- 不考虑未来会不会吃亏
- 能不能成功,取决于问题本身是否满足 "贪心选择性质"
2.2 顺序决策、交叉选择
顺序决策 = 把东西排好队,一个接一个处理,前面的不绕到后面去,后面的不插到前面来。
交叉选择 = 左边的点配右边的,右边的点配左边的,路线交叉了。
2.3 贪心排序
贪心排序 是贪心算法的经典应用场景,核心逻辑是:通过对问题中的元素按照特定规则排序,将原问题转化为「顺序决策」的子问题,从而用局部最优的选择推导出全局最优解。
它的本质是通过排序消除「交叉选择」的可能性,把原本需要枚举所有匹配 / 分配方案的复杂问题,简化为仅需按顺序处理的线性问题,大幅降低算法复杂度。
核心特征
- 排序是前提:必须先对元素按「最优决策规则」排序,否则贪心选择不成立
- 局部最优 = 全局最优:排序后,每一步的局部最优选择(如顺序匹配)最终会汇聚成全局最优解
- 反证法验证:贪心排序的正确性必须通过「反证法」证明:若存在非顺序的最优解,交换后总代价更小,因此顺序解才是最优
2.2 思路
我们题目的前提是最小化所有机器人的移动总距离(|机器人位置 - 工厂位置|之和)
我们对数组进行排序
java
public long minimumTotalDistance(List<Integer> robot, int[][] factory) {
Collections.sort(robot);
Arrays.sort(factory, (a, b) -> a[0] - b[0]);
// 省略
}
为什么要这样排序,在这种排序中怎么样是顺序决策,交叉选择?
- 位置靠左的机器人 → 尽量分配给靠左的工厂
- 位置靠右的机器人 → 尽量分配给靠右的工厂
- 左机器人 a<右机器人 b
- 左工厂 c < 右工厂 d
- 交叉分配:a→d,b→c
- 顺序分配:a→c,b→d
对任意实数 a<b, c<d,恒有: ∣a−d∣+∣b−c∣≥∣a−c∣+∣b−d∣
数学公式我们这里就不推导了,感兴趣的可以自己分类讨论求证一下
三、动态规划
3.1 定义
把大问题拆成一堆小问题,先把小问题的最优答案记下来,再用小答案拼出大答案。
特点:
- 不暴力枚举所有可能(不然会超时)
- 一步一步推,后面的结果依赖前面的结果
- 每一步都只保留 "当前最优",不保留垃圾方案
四、二维代码实现
4.1 思路
我们已经排好序了:机器人:左 → 右 工厂:左 → 右
现在问题变成:前 i 个机器人,交给前 j 个工厂修,最小总距离是多少?
dp[i][j]= 前 i 个机器人,分配给前 j 个工厂,能得到的最小总移动距离
这里也体现了我们的贪心排序,左边的机器人尽量交给左边的工厂
4.2 递推公式
对第 j 个工厂,只有两种选择:
选择 1:这个工厂不用
那前 i 个机器人,就只能用前 j-1 个工厂。所以:dp[i][j] = dp[i][j-1]
选择 2:这个工厂要用
那我们可以让它修:
- 1 个机器人
- 2 个机器人
- ......
- 最多不超过它的上限 limit 个
然后看哪种最省距离。
dp[i][j] = min( dp[i][j - 1], dp[k][j−1] + 这k ~ i -1【索引】个机器人走到第j个工厂的总距离 )
注意:我们k从i - 1开始往0遍历,因为我们要把右边的机器人尽量分配给右边的厂
4.3 初始化
java
int robotNum = robot.size();
int factoryNum = factory.length;
// dp[i][j] 代表前 i 个机器人,分配给前 j 个工厂,能得到的最小总移动距离
long[][] dp = new long[robotNum + 1][factoryNum + 1];
for (int i = 1; i <= robotNum; i++) {
Arrays.fill(dp[i], Long.MAX_VALUE / 2);
}
当i = 0时 前 0 个机器人,交给前 j 个工厂修,最小总距离是0所以不需要初始化
然后针对其他数据,我们都要Long.MAX_VALUE / 2,这是因为我们的递推公式中有取min
如果初始化为0,不就一直取0了
那为什么第一行不需要? 不仅是因为语义上不符合,我们再来看一下递推公式
dp[i][j] = min( dp[i][j - 1], dp[k][j−1] + 这k ~ i -1【索引】个机器人走到第j个工厂的总距离 )
当k = 0时此时我们要把所有的机器人都要当前j工厂处理
那么dp[i][j] = min( dp[i][j - 1], 这0 ~ i -1【索引】个机器人走到第j个工厂的总距离 )
dp[0][j−1] = 0,所以才要跳过i - 1
那为什么我们初始化是Long.MAX_VALUE / 2,为什么要除2?
dp[k][j−1] + 这k ~ i -1【索引】个机器人走到第j个工厂的总距离
如果dp[k][j−1] = Long.MAX_VALUE ,那不就超出最大范围了吗
当然上面是为了方便理解,我们也可以直接简化为下图
java
Collections.sort(robot);
Arrays.sort(factory, (a, b) -> a[0] - b[0]);
int robotNum = robot.size();
int factoryNum = factory.length;
// dp[i][j] 代表前 i 个机器人,分配给前 j 个工厂,能得到的最小总移动距离
long[][] dp = new long[robotNum + 1][factoryNum + 1];
for (int i = 1 ; i <= robotNum ; i++){
dp[i][0] = Long.MAX_VALUE/2;
for (int j = 1 ; j <= factoryNum ; j++){
...
}
}
因为我们在递推公式里面dp[i][j] 每次都要和dp[i][j - 1]进行比较,所以只需要初始化dp[i][0]就可以了
4.4 代码实现
java
class Solution {
public long minimumTotalDistance(List<Integer> robot, int[][] factory) {
Collections.sort(robot);
Arrays.sort(factory, (a, b) -> a[0] - b[0]);
int robotNum = robot.size();
int factoryNum = factory.length;
// dp[i][j] 代表前 i 个机器人,分配给前 j 个工厂,能得到的最小总移动距离
long[][] dp = new long[robotNum + 1][factoryNum + 1];
for (int i = 1 ; i <= robotNum ; i++){
dp[i][0] = Long.MAX_VALUE/2;
for (int j = 1 ; j <= factoryNum ; j++){
// 前 i 个机器人,分配给前 j - 1 个工厂,能得到的最小总移动距离
dp[i][j] = dp[i][j - 1];
long cost = 0;
// 接下来,第j工厂要开始修理机器人
for (int k = i - 1; k >= 0 && factory[j - 1][1] >= i - k; k--){
// 修理k ~ i - 1个机器人的总花费,注意这里的k,i, j都是个数不是索引
cost += (long)Math.abs(factory[j - 1][0] - robot.get(k));
//最小花费 = min(把当前这个机器人交给别的工厂修 ,把k ~i - 1个机器人交给j工厂修)
dp[i][j] = Math.min(dp[i][j] , dp[k][j - 1] + cost);
}
}
}
return dp[robotNum][factoryNum];
}
}
五、一维代码实现
5.1 思考
我们在二维实现中,将机器人作为外循环,把工厂作为内循环。
那我们可不可以把工厂作为外循环,机器人作为内循环?
答案是可以的。
dp[i] = 前 i 个机器人全部修好的最小总距离
5.2 递推公式
dp[i] = min( dp[i], dp[k] + cost );
可以看到这个公式其实和我们之前的公式长得挺像的只是换了一下行列,那我们来解释一下吧
用前j个工厂全部修好前 i 个机器人的最小总距离
= min(用前j - 1个工厂全部修好前 i 个机器人的最小总距离 , 用前j - 1个工厂全部修好前 k 个机器人的最小总距离 + 用j 工厂全部修好k + 1 ~ i 个机器人的最小总距离)
5.3 遍历顺序
在很多一维数组dp遍历中,我们采用逆序遍历,那么这里需不需要逆序遍历呢?
我们逆序的又是哪一个呢?
dp[i] = min( dp[i], dp[k] + cost );
k < i,如果我们先处理k,最终dp[k]存放的是用前j 个工厂全部修好前 k 个机器人的最小总距离
但是我们需要的是dp[k] = 用前j - 1个工厂全部修好前 i 个机器人的最小总距离
所以,我们要逆序遍历,防止用到最新的dp[k]
java
long[] dp = new long[robotNum + 1];
for (int[] f : factory){
for (int i = robotNum ; i >= 1 ; i--){
long cost = 0;
for (int k = i - 1 ;k >= 0 && i - k <= f[1] ; k--){
cost += (long) Math.abs(robot.get(k) - f[0]);
dp[i] = Math.min(dp[i] , dp[k + 1] + cost);
}
}
}
5.4 初始化
我们上面的代码还没有进行初始化
我们原来初始化的逻辑是当dp[0][j]=0换算到这里就是dp[0] = 0;
要对dp[i (i>=1)][0] = Long.MAX_VALUE/2;那么这些初始化转换为代码就是
java
Arrays.fill(dp, Long.MAX_VALUE/2);
dp[0] = 0;
5.5 完整代码
java
class Solution {
public long minimumTotalDistance(List<Integer> robot, int[][] factory) {
Collections.sort(robot);
Arrays.sort(factory, (a, b) -> a[0] - b[0]);
int robotNum = robot.size();
long[] dp = new long[robotNum + 1];
Arrays.fill(dp, Long.MAX_VALUE/2);
dp[0] = 0;
for (int[] f : factory){
for (int i = robotNum ; i >= 1 ; i--){
long cost = 0;
for (int k = i - 1 ;k >= 0 && i - k <= f[1] ; k--){
cost += (long) Math.abs(robot.get(k) - f[0]);
dp[i] = Math.min(dp[i] , dp[k] + cost);
}
}
}
return dp[robotNum];
}
}