题目描述
大意:公司让你负责m个冰激凌的运输。运输车的冷库只够装n个冰激凌,一次运输需要t分钟,返回也需要t分钟。每个冰激凌制作好有一个时间。求最短运输完所有冰激凌的时间,以及在时间最短的情况下最少运输次数。
(这题真的不知道怎么写个暴力了,如果写个暴力,不失正确性,发现不就是记忆化搜索???递推的顺序也不难发现,跟dp大同小意,所以此题我写暴力方法了)
算法一:动态规划
思路引入
- (此处默认已经将c数组排序过了)
- 一个很直观的思路,如果我当前有n个冰淇淋,并且我人已经在工厂了,我肯定是立马送出
- 因为如果我在原地还等待,就算制作出了n+1个冰淇淋,我也依旧不能运输出去
- 因为冰淇淋没有差别,如果第i个冰淇淋和第i+1个都生产出来了,所以我们没必要选择第i+1个冰淇淋运出去,而不运第i个
- 所以,基于这种思想,我们可以这样定义dp的状态
dp[i]代表运输完第i个冰淇淋并且回到工厂所需要的最短时间
- 注意此处还需要回到工厂,也就代表第m-1个也回去了,实际上他不需要回去,那么我们最后要的答案其实是 d p [ m − 1 ] − t dp[m-1]-t dp[m−1]−t
状态转移
-
因为我们一次可以运送的冰淇淋的数量为 [ 1 , n ] [1,n] [1,n],
-
所以那么对于 d p [ i ] dp[i] dp[i],我们已经知道了i的前面n-1个冰淇淋的状态。
-
即 d p [ j ] , j ∈ [ i − n , i − 1 ] , i ∈ [ n , m − 1 ] dp[j],j \in [i-n,i-1],i\in [n,m-1] dp[j],j∈[i−n,i−1],i∈[n,m−1]的时间基础上面,在送出 [ j + 1 , i ] [j+1,i] [j+1,i]这个区间所有的冰淇淋
-
这边的 i > = n i>=n i>=n可能有同学没看懂,因为要保证包括第i个冰淇淋的情况,前面还有 n − 1 n-1 n−1个冰淇淋,并且已经送完了第j个冰淇淋的情况,也就是说至少得有 n + 1 n+1 n+1个冰淇淋,但是下标是从0开始的,所以我们此处为 i ≥ n i\geq n i≥n
-
(注意:上面对于的是 i ≥ n i\geq n i≥n的情况,所以我们还需要考虑 i < n i<n i<n的情况)
-
显然我们还需要考虑的一种情况,就是一开始什么都不送,等到第 j j j个冰淇淋做好然后送出去,其中 j ∈ [ 0 , n − 1 ] j\in[0,n-1] j∈[0,n−1]
-
然后得到了j到i的转移方程
d p [ j ] , j ∈ [ m a x ( i − n + 1 , 0 ) , i − 1 ] dp[j],j \in [max(i-n+1,0),i-1] dp[j],j∈[max(i−n+1,0),i−1],(取max是因为下标要合法) -
接下来我们还要求总的趟数,我们只需要在dp的时候记录该状态是由哪一个转移过来的,倒序追踪的时候计数
-
借用
pre[]
数组来记录前驱,初始化为-1,如果为-1则代表它的前驱是在一开始原地等待然后一次性送出
代码实现
#define pb push_back
#define inf 0x3f3f3f3f
class Solution {
public:
/**
* 两个数表示答案
* @param n int整型 一次运输的冰激凌数量
* @param m int整型 总冰激凌数
* @param t int整型 一次运输的时间
* @param c int整型一维数组 表示每个冰激凌制作好时间<1e4
* @param cLen int c数组长度
* @return int整型vector
*/
vector<int> icecream(int n, int m, int t, int* c, int len) {
vector<int> ans;//记录答案
vector<int> dp(m,inf);//dp[i]代表运输完第i个物品并且回到工厂所需要的最短时间
//所以我们求解的也就是dp[m-1]-t,就是最后一趟不需要回到工厂
vector<int> pre(m,-1);//记录前驱
sort(c,c+m);//排序c数组
for(int i=0;i<len;i++){
if(i<n){//因为[0,n-1]的冰激淋可能没有前驱
dp[i]=min(dp[i],c[i]+2*t);//做完[0,i]个冰激淋统一一趟送过去
}
int l=max(i-n,0);//最多可能由前n个状态转移过来
for(int j=l;j<i;j++){
int tmp=max(dp[j],c[i])+2*t;//[j+1,i]之间的冰激淋全部都由这一趟运送过去
//第j个冰激淋运输
if(dp[i]>tmp){//假如比当前的dp[i]更优,则更新答案
dp[i]=tmp;
pre[i]=j;//记录前驱
//此处前驱用于计算运输次数
//也可以用于追踪最优方案,
}
}
}
int cnt=0,now=m-1;//cnt用于记录最少运输次数
//now代表当前是在第几个冰激凌制作好后去运输的
while(~now){//当前now合法,即now不等于-1,因为-1的反码是0所以可以这样写
cnt++;//去的趟数加一
now=pre[now];//追踪前驱
}
ans.pb(dp[len-1]-t);//最后一趟不需要返回到工厂
ans.pb(cnt);//
return ans;
}
};
复杂度分析
- 时间复杂度,排序为 O ( m log m ) O(m \log m) O(mlogm),动态规划两层循环为 O ( m n ) O(mn) O(mn),理论上应该是 O ( m n + m log m ) O(mn+m \log m) O(mn+mlogm),但是数据中 n n n比 log m \log m logm的阶大,所以总的时间复杂度为 O ( m n ) O(mn) O(mn)
- 空间复杂度,定义了动态数组ans,dp,pre,为 O ( m ) O(m) O(m)
算法二:贪心
引入
- 其实这题真的很难看出来是贪心,因为确实很难证明贪心(网上能找到的博客我瞅了一眼,发现没人证明……)
- 不过我这边尽量不用抽象的纯数学来证明,我尽量讲的通俗易懂一点
- 以下有好几个需要证明的,我尽量递进得来讲
证明:运送数量贪心,m<=n
- 上面的动态规划中,我已经讲过了,能如果当前有n个冰淇淋可以运出去,那么等下一个冰淇淋制作出来是毫无意义的
- 现在我们需要思考的问题,如果需要运送的数量 m ≤ n m\leq n m≤n,那么我们是否有可能花费两趟去比花费一趟去来的优?
- 答案是不可能的。为什么?
- 如果花费一趟去,我们最后花费的时间是 c [ m − 1 ] + t c[m-1]+t c[m−1]+t,而花费两趟的时间是 m a x ( c [ m − 1 ] , c [ j ] + t × 2 ) + t max(c[m-1],c[j]+t \times 2)+t max(c[m−1],c[j]+t×2)+t
- 显然有 c [ m − 1 ] + t ≤ m a x ( c [ m − 1 ] , c [ j ] + t × 2 ) + t c[m-1]+t\leq max(c[m-1],c[j]+t \times 2)+t c[m−1]+t≤max(c[m−1],c[j]+t×2)+t
- 就是说,花费一趟去的代价不劣于花两趟去的代价,想必聪明的你肯定知道这种情况选哪种情况去。
证明:贪心,n<m<=2n
- 当m的个数在 ( n , 2 n ] (n,2n] (n,2n]之间的时候,我们是否有可能花费三次去运输?
- 答案是不可能的。为什么?
- 因为,如果其中连续的两趟拼接起来小于n,根据上述结论,我们则会花费一趟运输
- 如果大于n,则设两趟的长度为 i i i和 m − i m-i m−i,显然二者都不能超过n
- 故有 i ∈ [ m − n , n ] i\in [m-n,n] i∈[m−n,n],答案为 m a x ( c [ i ] + 2 × t , c [ m − 1 ] ) + t max(c[i]+2\times t,c[m-1])+t max(c[i]+2×t,c[m−1])+t
- 因为我们以及排序过一遍了,显然c[m-n]是最小的,那么有: i = m − n i=m-n i=m−n不会劣于其他答案
证明:贪心,对于任意m
- 尽过上面的两个证明,相信大家已经有点感觉了
- 对于贪心法的证明,我们往往采用替换法,即该决策替换掉最优解,不会劣与最优解,即该决策是最优解,上面的max那句话我已经用到了
- 想必这里大家也应该知道了怎么做了,对的,数学归纳法
- 如果还不懂的话,我举个简单的例子,假设对于 m ∈ ( 2 n , 3 n ] m \in (2n,3n] m∈(2n,3n],我们取走第一段,剩下的不就是转换为 m ∈ ( n , 2 n ] m\in(n,2n] m∈(n,2n]的情况吗?
- 如果取走最后一段,也是 m ∈ ( n , 2 n ] m\in(n,2n] m∈(n,2n]的情况,为了满足第二个证明的结论,故唯一的分解方法为{m-2n,n,n}
- 到这里大家应该都懂了,分解方法为 { m % n , n , n , n , . . . , n , n } \{ m\%n,n,n,n,...,n,n \} {m%n,n,n,n,...,n,n}
- 感性理解一下,在不白白浪费趟数的情况下,第一段越早去,最后一个蛋糕白白等待的可能性就少了很多。
算法思路
- 到这里,聪明的你肯定已经知道怎么写了,假如 m % n ≠ 0 m\%n\neq 0 m%n=0,第一段取 m % n {m\%n} m%n,其余都取 n n n
- 对于 m % n = 0 m\%n= 0 m%n=0,全部都取 n n n
- 趟数也就为 ⌈ m n ⌉ \lceil \cfrac{m}{n} \rceil ⌈nm⌉
代码实现
class Solution {
public:
vector<int> icecream(int n, int m, int t, int* c, int len) {
sort(c,c+m);//排序c数组
int cnt,ans=0,sta;//趟数,最小时间值,第一趟从第几个冰淇淋开始运送
if(m%n)cnt=m/n+1,sta=m%n-1;//不能整除得多运送一趟
else cnt=m/n,sta=n-1;//下标0开始,所以是第n个物品实际为c[n-1]
for(int i=sta;i<m;i+=n){
ans=max(ans,c[i])+2*t;//上一次返回来的时间,和这个冰淇淋做好的时间,取最大值,来回需要加上2t
}
ans-=t;//最后一趟不需要回工厂
return {ans,cnt};
}
};
复杂度分析
- 时间复杂度,排序 O ( m log m ) O(m \log m) O(mlogm),n最小值为1,所以贪心遍历一遍的最坏情况为 O ( m ) O(m) O(m),总得为 O ( m log m ) O(m \log m) O(mlogm)
- 空间复杂度,定义了数字ans,cnt,sta,为 O ( 1 ) O(1) O(1)