【单调队列优化 DP】

LeetCode 41 场双周赛 1687. 从仓库到码头运输箱子

算法:单调队列优化 DP

  1. 下文简称最大装货数量 m a x B o x e s maxBoxes maxBoxes m x c mxc mxc,简称最大载重量 m a x W e i g h t maxWeight maxWeight m x w mxw mxw(二者均大等于 1 1 1), w [ i ] w[i] w[i] 表示前 i i i 个物品重量的前缀和(注意 w w w 数组要开 l o n g   l o n g long \space long long long,因为长度 1 e 5 1e5 1e5,每个重量上限也是 1 e 5 1e5 1e5)。

  2. f ( i ) f(i) f(i) 表示运送前 i i i 个物品到指定码头,所需要的最少行程。则很容易想出状态转移方程 f ( i ) = f ( j ) + ∑ j = i − m x c i − 1 c o s t ( j , i ) f(i) = f(j) + \sum\limits_{j = i - mxc}^{i - 1}{cost(j, i)} f(i)=f(j)+j=imxci1cost(j,i) 其中 c o s t ( l , r ) cost(l, r) cost(l,r) 表示处理 ( l , r ] (l, r] (l,r](或 [ l + 1 , r ] [l + 1, r] [l+1,r]) 区间内的物品所需要的行程,同时上述方程也要满足 w [ i ] − w [ j ] ⩽ m x w w[i] - w[j] \leqslant mxw w[i]w[j]mxw

  3. 但是这样看下来,时间复杂度是 O ( n 2 ) O(n^2) O(n2),即使用前缀和把 c o s t cost cost 优化成 O ( 1 ) O(1) O(1),仍然要先枚举 i i i,再枚举 j j j,所以接下来需要想一个能够优化掉一维的方法。首先我们把 c o s t cost cost 的前缀和写出来。由于它需要按照顺序卸货,所以只需要看相邻两个物品是否需要送到同一个码头即可,若是相同的,则两个物品可以在同一码头被同一时间放下,否则就需要多一次行程。于是我们用 c ( x ) c(x) c(x) 表示第 x − 1 x - 1 x1 个物品与第 x x x 个物品是否在相同码头卸货,若不同则为 1 1 1,相同则为 0 0 0,将其求前缀和,用 s ( x ) s(x) s(x) 表示。这里有一个边界条件 —— 若 c o s t ( l , r ) cost(l, r) cost(l,r) 中的 l l l 满足 s ( l + 1 ) > s ( l ) s(l + 1) > s(l) s(l+1)>s(l) 说明从 l l l l + 1 l + 1 l+1 是需要多一次行程变码头的,但是此时的 l + 1 l + 1 l+1 才是我们处理最后一段的起点,意味着我们可以直接从货舱到达 l + 1 l + 1 l+1 所在的码头,即不需要算上 c ( l + 1 ) c(l + 1) c(l+1)。特别地,对于第一个物品,我们不做任何处理,因为一开始车就在货舱,可以直接到达第一个物品所在的码头,即 s ( 1 ) = s ( 0 ) = 0 s(1) = s(0) = 0 s(1)=s(0)=0 而每一次从货舱出发和卸完所有货回到货舱也要花两次行程,综合起来, c o s t cost cost 的返回值如下: c o s t ( l , r ) = s [ r ] − s [ l ] + 2 − ( s [ l + 1 ]   > s [ l ] ) cost(l, r) = s[r] - s[l] + 2 - (s[l + 1] \space \gt s[l]) cost(l,r)=s[r]s[l]+2(s[l+1] >s[l]) 下文中用 c h e c k ( x ) check(x) check(x) 表示 s ( x + 1 ) > s ( x ) s(x + 1) \gt s(x) s(x+1)>s(x) 是否成立。

  4. 有了前缀和优化,我们将公式写开,就变成以下两种形式 f ( i ) = f ( j ) + [ s ( i ) − s ( j ) − c h e c k ( j ) ] + 2 (1) f(i) = f(j) + [s(i) - s(j) - check(j)] + 2 \tag {1} f(i)=f(j)+[s(i)s(j)check(j)]+2(1) f ( i ) = s ( i ) + [ f ( j ) − s ( j ) − c h e c k ( j ) ] + 2 (2) f(i) = s(i) + [f(j) - s(j) - check(j)] + 2 \tag {2} f(i)=s(i)+[f(j)s(j)check(j)]+2(2) 而通过对公式 ( 2 ) (2) (2) 的观察,我们发现,中括号里的内容其实就是在求 min ⁡ 1 ⩽ j ⩽ i − 1 f ( j ) − s ( j ) − c h e c k ( j ) \min\limits_{1 \leqslant j \leqslant i - 1}{f(j) - s(j) - check(j)} 1ji1minf(j)s(j)check(j) s ( i ) s(i) s(i) 是定值,这样就能够把这个问题转化为单调队列问题,即在一次遍历 i i i 1 1 1 n n n 的过程中,不断求出 i i i 之前的一个表达式的最值。

  5. 单调队列的实现。注意刚开始时,不能把队列置为空,因为这样就没法更新 f ( 1 ) f(1) f(1),会造成错误的后续更新,所以一开始要在队尾加入元素 0 0 0(在代码上不用表示,初值即为 0 0 0)。而初始值为 0 0 0 的原因有以下两点 —— 一是由公式可知,能更新 f ( 1 ) f(1) f(1) 的只有 f ( 0 ) f(0) f(0),而一定有 f ( 0 ) − s ( 0 ) − c h e c k ( 0 ) = 0 ; f(0) - s(0) - check(0) = 0; f(0)s(0)check(0)=0; 二是要满足两个条件 m x c mxc mxc m x w mxw mxw,当初始队头元素为 0 0 0 时,一定有 1 − 0 < = m x c ,    w [ 1 ] − w [ 0 ] = w e i g h t [ 1 ] ⩽ m x w 1 - 0 <= mxc, \space \space w[1] - w[0] = weight[1] \leqslant mxw 10<=mxc,  w[1]w[0]=weight[1]mxw(根据题目条件所说,每个物品的重量都不会超过最大载重量),这样不会导致弹出队头元素把队列变空。接着求能更新当前 i i i j j j 时,先将 i − q . f r o n t ( ) > m x c    或    w [ i ] − w [ q . f r o n t ( ) ] > m x w i - q.front() \gt mxc \space \space 或 \space \space w[i] - w[q.front()] \gt mxw iq.front()>mxc    w[i]w[q.front()]>mxw 的队头元素弹出,这是为了满足最后一段要处理的区间满足题目的数量和重量要求,接下来就是更新 f ( i ) f(i) f(i),这里更新采用公式 ( 1 ) (1) (1),因为 c o s t cost cost 函数上面已经实现过,写起来比较简洁;最后就是将当前元素插入队尾,这步操作可以是保持严格单调递增,也可以保持单调不减,这个看你心情(逃),因为公式中的 s ( i ) s(i) s(i) 对于每次枚举到的 i i i 来说都是定值。

时间复杂度 O ( n ) O(n) O(n)
C ++ 代码
#define fup(i, a, b) for (int i = a; i <= b; i ++ )
#define fdn(i, a, b) for (int i = a; i >= b; i -- )

typedef long long LL;

const int N = 100010;

class Solution {
public:
    int f[N], s[N];
    int q[N], hh, tt;
    LL w[N];

    bool check(int x) {
        return s[x + 1] > s[x];
    }

    int cost(int l, int r) {
        return s[r] - s[l] + 2 - check(l);
    }

    int boxDelivering(vector<vector<int>>& b, int tot, int mxc, int mxw) {
        int n = b.size();
        fup(i, 1, n) {
            int x = b[i - 1][0], y = b[i - 1][1];
            w[i] = w[i - 1] + y;
            if (i >= 2) s[i] = s[i - 1] + (x != b[i - 2][0]);
        }
        fup(i, 1, n) {
            while (hh <= tt && (i - q[hh] > mxc || w[i] - w[q[hh]] > mxw)) hh ++ ;
            if (hh <= tt) f[i] = f[q[hh]] + cost(q[hh], i);
            while (hh <= tt && f[q[tt]] - s[q[tt]] - check(q[tt]) >= f[i] - s[i] - check(i)) tt -- ;
            q[ ++ tt] = i;
        }
        return f[n];
    }
};
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值