移动所有球到每个盒子所需的最小操作数 图解题解
这道题到底在问什么
- 输入
- boxes="110"
- 输出
- [1,1,3]
- 输入
- boxes="001011"
- 输出
- [11,8,5,4,3,4]
- 输入
- boxes="011"
- 输出
- [3,1,1]
先想最直接的笨办法
两遍都扫完了。left = [0,0,0,1,2,4] 是每个盒子左半边的代价,right = [11,8,5,3,1,0] 是右半边的代价。现在只要把两张表对应位置相加,每个盒子的最终答案就出来了。一个一个来。(动画第 17 步)
最优解:一步一步想明白
- 3记牢:目标右移一格,左边每个球多走一步、右边每个球少走一步。左遍 left[i]=left[i-1]+左边球数,右遍 right[i]=right[i+1]+右边球数,合起来 answer[i]=left[i]+right[i]。
- 4先看清画面。上面这排是盒子 boxes,值是 0 或 1,绿色的三格下标 2、4、5 里各有一个小球,其余是空盒。右边这张表要填的是 left,表示每个盒子左半边球搬过来的代价,现在还空着。我们先从左往右扫一遍把 left 填满,再从右往左扫一遍算 right,最后两个相加就是答案。先看第 0 个盒子。
- 5左遍从第 0 个盒子开始。它是最左边,左边一个盒子都没有,自然没有球需要从左边搬过来,所以 left[0] 就是 0。计数器 cnt 记的是当前盒子左边有几个球,现在也是 0。把 0 记进 left 表的第 0 格。
- 6目标挪到第 1 个盒子。先看刚跨过的下标 0 这一格,它是空盒,没有新球加入,cnt 还是 0。目标从 0 右移到 1,左边这 0 个球每个都要多走一步,所以在上一格 left[0] = 0 的基础上再加 cnt = 0,得到 left[1] = 0。
- 7目标挪到第 2 个盒子。先看刚跨过的下标 1 这一格,它是空盒,没有新球加入,cnt 还是 0。目标从 1 右移到 2,左边这 0 个球每个都要多走一步,所以在上一格 left[1] = 0 的基础上再加 cnt = 0,得到 left[2] = 0。
- 8目标挪到第 3 个盒子。先看刚跨过的下标 2 这一格,它是个球,现在它落到了目标的左边,cnt 加一变成 1。目标从 2 右移到 3,左边这 1 个球每个都要多走一步,所以在上一格 left[2] = 0 的基础上再加 cnt = 1,得到 left[3] = 1。
- 9目标挪到第 4 个盒子。先看刚跨过的下标 3 这一格,它是空盒,没有新球加入,cnt 还是 1。目标从 3 右移到 4,左边这 1 个球每个都要多走一步,所以在上一格 left[3] = 1 的基础上再加 cnt = 1,得到 left[4] = 2。
- 10目标挪到第 5 个盒子。先看刚跨过的下标 4 这一格,它是个球,现在它落到了目标的左边,cnt 加一变成 2。目标从 4 右移到 5,左边这 2 个球每个都要多走一步,所以在上一格 left[4] = 2 的基础上再加 cnt = 2,得到 left[5] = 4。
- 11左表填满后,换个方向。右遍从最右的第 5 个盒子开始,它右边没有盒子了,没有球要从右边搬过来,所以 right[5] 是 0,记右边球数的 cnt 也从 0 起。这回我们从右往左走。
- 12目标挪到第 4 个盒子。看刚跨过的右邻居下标 5,它是个球,现在落到目标右边,cnt 加一变成 1。目标从 5 左移到 4,右边这 1 个球每个都要多走一步,所以在 right[5] = 0 上加 cnt = 1,得到 right[4] = 1。
- 13目标挪到第 3 个盒子。看刚跨过的右邻居下标 4,它是个球,现在落到目标右边,cnt 加一变成 2。目标从 4 左移到 3,右边这 2 个球每个都要多走一步,所以在 right[4] = 1 上加 cnt = 2,得到 right[3] = 3。
- 14目标挪到第 2 个盒子。看刚跨过的右邻居下标 3,它是空盒,cnt 保持 2。目标从 3 左移到 2,右边这 2 个球每个都要多走一步,所以在 right[3] = 3 上加 cnt = 2,得到 right[2] = 5。
- 15目标挪到第 1 个盒子。看刚跨过的右邻居下标 2,它是个球,现在落到目标右边,cnt 加一变成 3。目标从 2 左移到 1,右边这 3 个球每个都要多走一步,所以在 right[2] = 5 上加 cnt = 3,得到 right[1] = 8。
- 16目标挪到第 0 个盒子。看刚跨过的右邻居下标 1,它是空盒,cnt 保持 3。目标从 1 左移到 0,右边这 3 个球每个都要多走一步,所以在 right[1] = 8 上加 cnt = 3,得到 right[0] = 11。
- 17两遍都扫完了。left = [0,0,0,1,2,4] 是每个盒子左半边的代价,right = [11,8,5,3,1,0] 是右半边的代价。现在只要把两张表对应位置相加,每个盒子的最终答案就出来了。一个一个来。
- 18第 0 个盒子:左半边代价 left[0] = 0,右半边代价 right[0] = 11,相加得 11。意思是把三个球全搬到最左边的 0 号盒子,一共要走 11 步。
- 19第 1 个盒子:左半边代价 left[1] = 0,右半边代价 right[1] = 8,相加得 8。记进 answer 的第 1 位。
- 20第 2 个盒子:左半边代价 left[2] = 0,右半边代价 right[2] = 5,相加得 5。记进 answer 的第 2 位。
- 21第 3 个盒子:左半边代价 left[3] = 1,右半边代价 right[3] = 3,相加得 4。记进 answer 的第 3 位。
- 22第 4 个盒子:左半边代价 left[4] = 2,右半边代价 right[4] = 1,相加得 3。记进 answer 的第 4 位。
- 23第 5 个盒子:左半边代价 left[5] = 4,右半边代价 right[5] = 0,相加得 4。最后一个也算好了。
- 24六个盒子的答案都填好了,answer = [11,8,5,4,3,4]。回看整套办法:不去为每个盒子从头加一遍,而是抓住相邻答案的差是左右两侧的球数,左扫一遍算 left、右扫一遍算 right,再逐位相加。总共只扫了三趟数组,线性时间就全部拿下。
⚠️ 容易写错的地方
✗ 错:对每个盒子 i,都把所有球到 i 的距离从头加一遍,写成两层循环
✓ 对:利用相邻答案的差,左右各扫一遍前缀累加,再逐位相加
相邻两个盒子的答案只差左右两侧的球数,不必重算。目标右移一格,左边每个球多一步、右边每个球少一步,这个增量能一遍扫出来。从头重加会让时间退化成 O(n^2)
✗ 错:把 cnt 当成「到目前为止的距离」,直接拿它当答案
✓ 对:cnt 是当前盒子一侧的球数;答案是在上一格代价上再加这个球数
cnt 记的是某一侧有几个球,它是每一步答案要增加的量,不是答案本身。真正的代价是把这些 cnt 一路累加起来的 left[i] 或 right[i]
✗ 错:累加 cnt 的时机搞反,先算 left[i] 再更新 cnt,或反过来
✓ 对:左遍先看 boxes[i-1] 更新 cnt,再写 left[i];右遍先看 boxes[i+1] 再写 right[i]
走到 i 时,新跨过的那格球(左遍是 i-1,右遍是 i+1)已经落到目标这一侧了,必须先把它计入 cnt,再用更新后的 cnt 累加。顺序错一步,球数会差一个
完整代码(Python / C++ / Java)
Python
from __future__ import annotations
from typing import *
from collections import *
from functools import *
from itertools import *
from math import *
from heapq import *
from bisect import *
class Solution:
def minOperations(self, boxes: str) -> List[int]:
n = len(boxes)
left = [0] * n
right = [0] * n
cnt = 0
for i in range(1, n):
if boxes[i - 1] == '1':
cnt += 1
left[i] = left[i - 1] + cnt
cnt = 0
for i in range(n - 2, -1, -1):
if boxes[i + 1] == '1':
cnt += 1
right[i] = right[i + 1] + cnt
return [a + b for a, b in zip(left, right)]C++
#include <algorithm>
#include <array>
#include <cctype>
#include <climits>
#include <cmath>
#include <deque>
#include <functional>
#include <iostream>
#include <limits>
#include <map>
#include <numeric>
#include <queue>
#include <set>
#include <sstream>
#include <stack>
#include <string>
#include <tuple>
#include <unordered_map>
#include <unordered_set>
#include <utility>
#include <vector>
using namespace std;
class Solution {
public:
vector<int> minOperations(string boxes) {
int n = boxes.size();
int left[n];
int right[n];
memset(left, 0, sizeof left);
memset(right, 0, sizeof right);
for (int i = 1, cnt = 0; i < n; ++i) {
cnt += boxes[i - 1] == '1';
left[i] = left[i - 1] + cnt;
}
for (int i = n - 2, cnt = 0; ~i; --i) {
cnt += boxes[i + 1] == '1';
right[i] = right[i + 1] + cnt;
}
vector<int> ans(n);
for (int i = 0; i < n; ++i) ans[i] = left[i] + right[i];
return ans;
}
};Java
import java.util.*;
class Solution {
public int[] minOperations(String boxes) {
int n = boxes.length();
int[] left = new int[n];
int[] right = new int[n];
for (int i = 1, cnt = 0; i < n; ++i) {
if (boxes.charAt(i - 1) == '1') {
++cnt;
}
left[i] = left[i - 1] + cnt;
}
for (int i = n - 2, cnt = 0; i >= 0; --i) {
if (boxes.charAt(i + 1) == '1') {
++cnt;
}
right[i] = right[i + 1] + cnt;
}
int[] ans = new int[n];
for (int i = 0; i < n; ++i) {
ans[i] = left[i] + right[i];
}
return ans;
}
}复杂度
时间
O(n)
n 是盒子数。左扫一遍 n 次、右扫一遍 n 次、最后逐位相加又 n 次,总共三趟线性扫描,是 O(n)。相比之下,若对每个盒子都把所有球的距离从头加一遍,是两层循环 O(n^2),数据一大就慢。本题 n 最大 2000,线性做法轻松
空间
O(n)
按峰值算。额外开了 left、right 两个长度 n 的数组,还有输出数组 answer,都是 O(n)。若把 left 累加进结果、右遍时边扫边把 right 直接加到 answer 上,可以省掉一个数组,但量级仍是 O(n)(要交出去的 answer 本身就 O(n))。计数器 cnt 是常数
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 移动所有球到每个盒子所需的最小操作数 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
这题的核心思路能一句话说清吗?+
能。答案 answer[i] 是所有球到第 i 个盒子的距离之和,直接对每个盒子加一遍是 O(n^2)。关键在于相邻两个盒子的答案只差左右两侧的球数:目标右移一格,左边每个球多走一步、右边每个球少走一步。于是把答案拆成左右两半,各用一个计数器记住这一侧的球数,从两个方向各扫一遍前缀累加,最后逐位相加,整体降到 O(n)。
为什么要分左右两遍,不能一遍搞定吗?+
因为一个盒子的代价由两部分组成:左边的球往右搬、右边的球往左搬。这两部分的累加方向正好相反。左边的代价要从左往右滚(左边球越走越多),右边的代价要从右往左滚。方向相反,自然分成两遍各扫一次,再把两张表对应相加。也可以只用一个答案数组:第一遍从左往右把 left 直接写进 answer,第二遍从右往左把 right 加到 answer 上,省掉一个数组。
三种语言实现上有差别吗?+
思路完全一样,都是两遍累加。差别只在收尾:Python 用一行列表推导把 left 和 right 逐位相加生成结果;Java 和 C++ 另开一个 ans 数组,再用一个循环逐位相加。另外 C++ 里判断 boxes 某位等于 1 的表达式会直接当成 0 或 1 加进 cnt,写得更紧凑,和另两种语言先判断再自增效果相同。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄声线把 移动所有球到每个盒子所需的最小操作数 一步步讲透(全站 0 节语音精讲,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。