题目
你有一辆货运卡车,你需要用这一辆车把一些箱子从仓库运送到码头。这辆卡车每次运输有 箱子数目的限制 和 总重量的限制 。
给你一个箱子数组 boxes 和三个整数 portsCount, maxBoxes 和 maxWeight ,其中 boxes[i] = [portsi, weighti] 。
- portsi 表示第 i 个箱子需要送达的码头, weightsi 是第 i 个箱子的重量。
- portsCount 是码头的数目。
- maxBoxes 和 maxWeight 分别是卡车每趟运输箱子数目和重量的限制。
箱子需要按照数组顺序运输,同时每次运输需要遵循以下步骤:
卡车从 boxes 队列中按顺序取出若干个箱子,但不能违反 maxBoxes 和 maxWeight 限制。
对于在卡车上的箱子,我们需要 按顺序 处理它们,卡车会通过 一趟行程 将最前面的箱子送到目的地码头并卸货。如果卡车已经在对应的码头,那么不需要 额外行程 ,箱子也会立马被卸货。
卡车上所有箱子都被卸货后,卡车需要 一趟行程 回到仓库,从箱子队列里再取出一些箱子。
卡车在将所有箱子运输并卸货后,最后必须回到仓库。
请你返回将所有箱子送到相应码头的 最少行程 次数。
题目示例
输入: boxes = [[1,1],[2,1],[1,1]], portsCount = 2, maxBoxes = 3, maxWeight = 3
输出: 4
解释: 最优策略如下:
- 卡车将所有箱子装上车,到达码头 1 ,然后去码头 2 ,然后再回到码头 1 ,最后回到仓库,总共需要 4 趟行程。
所以总行程数为 4 。
注意到第一个和第三个箱子不能同时被卸货,因为箱子需要按顺序处理(也就是第二个箱子需要先被送到码头 2 ,然后才能处理第三个箱子)。
解题思路
动态规划 + 单调队列求滑窗最值问题
由题意,在一次出车中,该次出车的行程多少,取决于车中所装箱子中有多少组相邻箱子的目的地不同,比如,车中所有箱子的目的地均相同,那么仅需2趟行程;若车中有x组相邻的箱子目的地不同,那么该次出车需x + 2趟行程;
- 为了快速得到某次出车所需要的行程,我们定义一个数组dif,dif[i]表示从box[0]到box[i]中,相邻箱子且目的地不同的组数;于是当某次出车的箱子区间为[x,y]时,该次出车的所需行程则可由dif[y]-dif[x] + 2快速得到;
- 因为每次出车均有重量限制,为了快速得到某次出车的箱子总重,仍需一个表示重量的前缀和数组w,那么箱子区间为[x+1,y]时(共x - y个),总重为w[y] - w[x];
我么定义dp[i]为将前i只箱子运送完毕所需要的最少行程,为了向前转移,我们假设之前的出车共运送了j只箱子(0~j-1),若能完全得到dp[i]与dp[j]的数学关系,便能得到转移方程;考虑如下几点:
- 易得该次出车的箱子区间为[j+1,i],于是在满足容量和重量的前提下,该次出车的行程为dif[i] - dif[j+1] + 2,总重为w[i] - w[j],总数为i - j;
- 该次出车装箱,不能超重量,即w[i] - w[j] 不能大于maxWeight;
- 该次出车装箱,不能超容量,即i - j不能大于maxBoxes,也就是说j不能小于i - maxBoxes,不能大于i-1;
于是,在满足容量和重量的前提下,对于在[i-maxBoxes,i-1]中的每一个j,有转移方程:
- dp[i] = min(dp[j] + dif[i] - dif[j+1] + 2;
由于[i-maxBoxes,i-1]是一个滑动区间,填表时,如果对于每一个i,都要枚举每一个区间内的j来求dp[j] -dif[j]的最小值,那么每一次找寻j都需要O(n)的时间,找寻所有滑动区间的总时间复杂度会达到O(n2),按本题所给的数据量105,会超时。
通过某种数据结构,优化滑动区间j的查找(某种数据结构?——> 优先队列 ——> 单调双端队列):
- 一般地,降低时间复杂度的常用方法,就是避免重复访问,将访问过的数据存储到数据结构中以便之后直接取用(而不用重复访问),即用空间换取时间。由于滑动区间每次仅仅滑动1,如果要按上述找寻j的方法,每次滑窗后都枚举区间中的每一个值,很显然会造成线性级的重复访问,因此我们在填表时,每填到一个i,就将其存储到数据结构中,以此来避免重复访问;
- 那么选取哪种数据结构呢?我们的需求是,希望能从数据结构中快速找到使dp[j] -dif[j]最小的j,另外,在找到它之后,若这个最优的j不能满足滑窗后容量和重量的要求,那么将它弹出数据结构,否则,将它与滑窗后的新区间的最后一个值i比较,将较优者排在数据结构的最前端;因此,自然就想到了优先队列;
- 在优先队列的基础上,能否继续优化?我们试想这样一种情况:滑窗过后,i增加了1,之前的i变成了i-1,进入到了滑动区间中,这个区间中的最后一个i,是最能满足题目的容量和重量需求的,因为它离滑窗后的i是最近的,换言之,它是最难因为容量和重量限制而被弹出队列的,在它入列之前,队列中如果存在值x,使得dp[x] - dif[x]比dp[i] - dif[i]大,而如果i不能满足之后的最优需求,那么x更加不会满足,于是将这样的x统统出列,直到遇到一个比i更优的值,再将i入列,以便之后的滑动区间使用(当它之前的值在滑窗后因不满足容量和重量条件弹出队列时)。这种情况下,需要优先队列拓展出【i入列前弹出比i更差的队尾元素】的功能,因此用单调双端队列代替优先队列。
代码
class Solution {
public:
int boxDelivering(vector<vector<int>>& boxes, int portsCount, int maxBoxes, int maxWeight) {
int n=boxes.size();
vector<int> dif(n+1),dp(n+1);//dif[i]表示从0~i有多少个相邻箱子是不同目的地的个数,dp[i]表示运前i个箱子的最小行程数
vector<long long> w(n+1);//总重的前缀和数组
deque<int> q{0};
for(int i=0;i<=n;i++)
{
if(i>1)dif[i]=dif[i-1]+(boxes[i-2][0]==boxes[i-1][0]?0:1);
if(i!=n)w[i+1]=w[i]+boxes[i][1];
}
for(int i=1;i<=n;i++)
{
while(i-q.front()>maxBoxes||w[i]-w[q.front()]>maxWeight) //若队首元素不满足容量或重量限制,弹出
{
q.pop_front();
}
//队首永远是最优的j。从队首到队尾,j 的值单调递增,且其dp[j]-dif[j+1]也递增,取用队首后一个值只会让dp[i]变得更大。
dp[i]=dp[q.front()]+dif[i]-dif[q.front()+1]+2;
if(i!=n)
{
while(!q.empty()&&dp[i]-dif[i+1]<dp[q.back()]-dif[q.back()+1])//i入列前,弹出大于等于i的队尾元素
{
q.pop_back();
}
q.push_back(i);
}
}
return dp[n];
}
};