LC-1687.从仓库到码头运输箱子

题目描述

leetcode 困难题

你有一辆货运卡车,你需要用这一辆车把一些箱子从仓库运送到码头。这辆卡车每次运输有 箱子数目的限制 和 总重量的限制 。

给你一个箱子数组 boxes 和三个整数 portsCount, maxBoxes 和 maxWeight ,其中 boxes[i] = [ports​​i​, weighti] 。

ports​​i 表示第 i 个箱子需要送达的码头, weightsi 是第 i 个箱子的重量。
portsCount 是码头的数目。
maxBoxes 和 maxWeight 分别是卡车每趟运输箱子数目和重量的限制。
箱子需要按照 数组顺序 运输,同时每次运输需要遵循以下步骤:

卡车从 boxes 队列中按顺序取出若干个箱子,但不能违反 maxBoxes 和 maxWeight 限制。
对于在卡车上的箱子,我们需要 按顺序 处理它们,卡车会通过 一趟行程 将最前面的箱子送到目的地码头并卸货。如果卡车已经在对应的码头,那么不需要 额外行程 ,箱子也会立马被卸货。
卡车上所有箱子都被卸货后,卡车需要 一趟行程 回到仓库,从箱子队列里再取出一些箱子。
卡车在将所有箱子运输并卸货后,最后必须回到仓库。

请你返回将所有箱子送到相应码头的 最少行程 次数。

示例1:

1
2
3
4
5
6
7
输入:boxes = [[1,2],[3,3],[3,1],[3,1],[2,4]], portsCount = 3, maxBoxes = 3, maxWeight = 6
输出:6
解释:最优策略如下:
- 卡车首先运输第一个箱子,到达码头 1 ,然后回到仓库,总共 2 趟行程。
- 卡车运输第二、第三、第四个箱子,到达码头 3 ,然后回到仓库,总共 2 趟行程。
- 卡车运输第五个箱子,到达码头 2 ,回到仓库,总共 2 趟行程。
总行程数为 2 + 2 + 2 = 6

提示1:

1
2
3
4
1 <= boxes.length <= 10^5
1 <= portsCount, maxBoxes, maxWeight <= 10^5
1 <= ports​​i <= portsCount
1 <= weightsi <= maxWeight

动态规划

以下前缀和或 $dp$ 数组为了方便下标都从 $1$ 开始到 $n$ 结束。

定义 $w$ 为重量前缀和数组;$diff$ 为“相邻不相等码头”前缀和数组,即 $diff[j]$ 表示 $[0, j]$ 区间箱子的相邻不相等码头的和。

利用 $diff$ 可以 $O(1)$ 的求出一趟车的最小行程,设该趟车第一个箱子为 $x$ ,最后一个箱子为 $y$ ,那么该趟的最小行程为 $diff[y] - diff[x] + 2$,其中 $2$ 是因为汽车离开仓库和返回仓库所消耗行程。

定义 $dp[i]$ 表示 $[0, i - 1]$ 区间箱子的最少行程数,$i - 1$ 显然为某趟车的最后一个箱子下标,那么 $dp[n]$ 为答案。

具体转移过程如下,对于每个 $dp[i]$,枚举上一趟车最后一个箱子的可能性 $dp[j]$,那么其在 $boxes$ 中的下标为 $j - 1$ ,也就是说 $j$ 为该趟车第一个箱子,简略的转移方程为:

$$
\begin{align}
&dp(i) = max(dp[j] + diff[i - 1] - diff[j] + 2)        max(0, i - maxBoxes) <= j < i\\
\end{align}
$$

该解法时间复杂度为 $O(N^2)$ ,会 TLE 。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Solution {
public int boxDelivering(int[][] boxes, int portsCount, int maxBoxes, int maxWeight) {
int n = boxes.length;
int[] w = new int[n + 1];
int[] diff = new int[n + 1]; // 实际运算中,这里 n 下标并没有用到
for(int i = 1; i < n + 1; i++){
w[i] = w[i - 1] + boxes[i - 1][1];
diff[i] = i == n || boxes[i - 1][0] == boxes[i][0] ? diff[i - 1] : diff[i - 1] + 1;
}
// dp[i] 表示 i 个箱子的最少行程数
int[] dp = new int[n + 1];
Arrays.fill(dp, Integer.MAX_VALUE);
dp[0] = 0;
for(int i = 1; i < n + 1; i++){
for(int j = Math.max(0, i - maxBoxes); j < i; j++){
if(w[i] - w[j] > maxWeight){
continue;
}
dp[i] = Math.min(dp[i], dp[j] + diff[i - 1] - diff[j] + 2);
}
}
return dp[n];
}
}

动态规划 + 单调队列

观察上面的转移过程可知,内层循环实际上是在 $[max(0, i - maxBoxes), i - 1]$ 窗口内找到一个满足要求的 $dp[j] - diff[j]$ 最小值,所以我们可以利用单调队列来实现 $O(1)$ 获取最小值。

每个下标最多进队一次出队一次,内层最多总共循环 $n$ 次。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class Solution {
public int boxDelivering(int[][] boxes, int portsCount, int maxBoxes, int maxWeight) {
int n = boxes.length;
int[] w = new int[n + 1];
int[] diff = new int[n + 1];
for(int i = 1; i < n + 1; i++){
w[i] = w[i - 1] + boxes[i - 1][1];
diff[i] = i == n || boxes[i - 1][0] == boxes[i][0] ? diff[i - 1] : diff[i - 1] + 1;
}
// dp[i] 表示 i 个箱子的最少行程数
int[] dp = new int[n + 1];
Deque<Integer> deque = new ArrayDeque<>();
deque.offerLast(0);
for(int i = 1; i < n + 1; i++){
while(!deque.isEmpty() && (i - deque.peekFirst() > maxBoxes || w[i] - w[deque.peekFirst()] > maxWeight)){
deque.pollFirst();
}
int min = deque.peekFirst(); // If NPE, there is no legal solution.
dp[i] = dp[min] + diff[i - 1] - diff[min] + 2;
while(!deque.isEmpty() && (dp[i] - diff[i] < dp[deque.peekLast()] - diff[deque.peekLast()])){
deque.pollLast();
}
deque.offerLast(i);
}
return dp[n];
}
}

LC-1687.从仓库到码头运输箱子
https://wecgwm.github.io/2022/12/08/LC-1687-从仓库到码头运输箱子/
作者
yichen
发布于
2022年12月8日
许可协议